From e9e163f5cb608732c655b585e774525326751c13 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 08:33:03 +0100 Subject: [PATCH 01/37] Add DCC26 workshop notebook suite and runbook --- workshops/dcc26/00_setup_api_warmup.ipynb | 74 +++++++++++++++++ workshops/dcc26/01_train_generate.ipynb | 83 +++++++++++++++++++ workshops/dcc26/02_evaluate_metrics.ipynb | 69 +++++++++++++++ .../dcc26/03_add_new_problem_scaffold.ipynb | 48 +++++++++++ workshops/dcc26/README.md | 66 +++++++++++++++ workshops/dcc26/requirements-colab.txt | 9 ++ workshops/dcc26/utils/__init__.py | 1 + workshops/dcc26/utils/notebook_helpers.py | 49 +++++++++++ 8 files changed, 399 insertions(+) create mode 100644 workshops/dcc26/00_setup_api_warmup.ipynb create mode 100644 workshops/dcc26/01_train_generate.ipynb create mode 100644 workshops/dcc26/02_evaluate_metrics.ipynb create mode 100644 workshops/dcc26/03_add_new_problem_scaffold.ipynb create mode 100644 workshops/dcc26/README.md create mode 100644 workshops/dcc26/requirements-colab.txt create mode 100644 workshops/dcc26/utils/__init__.py create mode 100644 workshops/dcc26/utils/notebook_helpers.py diff --git a/workshops/dcc26/00_setup_api_warmup.ipynb b/workshops/dcc26/00_setup_api_warmup.ipynb new file mode 100644 index 0000000..71f0e49 --- /dev/null +++ b/workshops/dcc26/00_setup_api_warmup.ipynb @@ -0,0 +1,74 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 00: Setup + API Warmup (DCC26)\n\nThis notebook covers the first workshop segment (10-15 minutes):\n\n1. Environment checks\n2. EngiBench problem instantiation (`Beams2D`)\n3. Design space / objectives / conditions inspection\n4. Dataset sample inspection\n5. Rendering and one explicit constraint check\n\nExpected outcome: participants can navigate the full EngiBench problem API without W&B setup.\n" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Optional install cell (fresh Colab)\n\nUncomment if your runtime does not already contain required packages.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# %pip install -q engibench[beams2d] engiopt matplotlib seaborn" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import random\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nimport engibench\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nprint('engibench version:', engibench.__version__)\nprint('seed:', SEED)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = Beams2D(seed=SEED)\n\nprint('Problem class:', type(problem).__name__)\nprint('Design space:', problem.design_space)\nprint('Objectives:', problem.objectives)\nprint('Conditions instance:', problem.conditions)\nprint('Condition keys:', problem.conditions_keys)\nprint('Dataset ID:', problem.dataset_id)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "dataset = problem.dataset\nprint(dataset)\n\nsample_idx = 0\ndesign = np.array(dataset['train']['optimal_design'][sample_idx])\nconfig = {k: dataset['train'][k][sample_idx] for k in problem.conditions_keys}\n\nprint('Sample design shape:', design.shape)\nprint('Sample config:', config)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "fig, ax = problem.render(design)\nax.set_title('Sample Beams2D design from training split')\nplt.show()" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# One explicit constraint check with intentionally mismatched volume fraction\nbad_config = dict(config)\nbad_config['volfrac'] = 0.2\nviolations = problem.check_constraints(design=design, config=bad_config)\n\nprint('Violation count:', len(violations))\nif violations:\n print(violations)\nelse:\n print('No violations found')" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Next\n\nContinue with **Notebook 01** to train a lightweight conditional generator and create designs for evaluation.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/workshops/dcc26/01_train_generate.ipynb b/workshops/dcc26/01_train_generate.ipynb new file mode 100644 index 0000000..7945f5a --- /dev/null +++ b/workshops/dcc26/01_train_generate.ipynb @@ -0,0 +1,83 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 01: Train + Generate (DCC26)\n\nThis notebook covers the second workshop segment (30 minutes):\n\n1. Build a lightweight conditional generator\n2. Train on a small subset for deterministic runtime\n3. Generate designs from sampled conditions\n4. Save artifacts for Notebook 02\n\nFallback options are included if you skip training.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# %pip install -q engibench[beams2d] torch torchvision matplotlib pandas" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import json\nimport os\nimport random\nfrom pathlib import Path\n\nimport numpy as np\nimport torch as th\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, TensorDataset\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\nth.manual_seed(SEED)\nif th.cuda.is_available():\n th.cuda.manual_seed_all(SEED)\n\nDEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\nprint('device:', DEVICE)\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\nCKPT_PATH = ARTIFACT_DIR / 'mini_cond_generator.pt'\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = Beams2D(seed=SEED)\ntrain_ds = problem.dataset['train']\ntest_ds = problem.dataset['test']\n\ncondition_keys = problem.conditions_keys\nprint('condition keys:', condition_keys)\n\n# Build compact train subset to keep runtime stable in workshop\nN_TRAIN = 512\nsubset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n\nconds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\ndesigns_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n\n# Downsample target to reduce model output size and speed up training\ndesigns_t = th.tensor(designs_np).unsqueeze(1)\nlowres_t = F.interpolate(designs_t, size=(25, 50), mode='bilinear', align_corners=False).squeeze(1)\ntargets_np = lowres_t.reshape(N_TRAIN, -1).numpy()\n\nprint('conditions shape:', conds_np.shape)\nprint('designs shape:', designs_np.shape)\nprint('lowres target shape:', targets_np.shape)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "class MiniCondGenerator(nn.Module):\n def __init__(self, in_dim: int, out_dim: int):\n super().__init__()\n self.net = nn.Sequential(\n nn.Linear(in_dim, 64),\n nn.ReLU(),\n nn.Linear(64, 128),\n nn.ReLU(),\n nn.Linear(128, out_dim),\n nn.Sigmoid(),\n )\n\n def forward(self, x):\n return self.net(x)\n\n\ndef upsample_to_design(y_flat: th.Tensor) -> th.Tensor:\n low = y_flat.reshape(-1, 1, 25, 50)\n high = F.interpolate(low, size=(50, 100), mode='bilinear', align_corners=False)\n return high.squeeze(1)\n\n\nmodel = MiniCondGenerator(in_dim=conds_np.shape[1], out_dim=targets_np.shape[1]).to(DEVICE)\noptimizer = th.optim.Adam(model.parameters(), lr=1e-3)\ncriterion = nn.MSELoss()" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "TRAIN_FROM_SCRATCH = True\nEPOCHS = 8\nBATCH_SIZE = 64\n\nif TRAIN_FROM_SCRATCH:\n ds = TensorDataset(th.tensor(conds_np), th.tensor(targets_np))\n dl = DataLoader(ds, batch_size=BATCH_SIZE, shuffle=True)\n\n for epoch in range(EPOCHS):\n model.train()\n epoch_loss = 0.0\n for xb, yb in dl:\n xb = xb.to(DEVICE)\n yb = yb.to(DEVICE)\n pred = model(xb)\n loss = criterion(pred, yb)\n optimizer.zero_grad()\n loss.backward()\n optimizer.step()\n epoch_loss += float(loss.item())\n\n print(f'epoch {epoch + 1:02d}/{EPOCHS} - loss: {epoch_loss / len(dl):.4f}')\n\n th.save({'model': model.state_dict(), 'condition_keys': condition_keys}, CKPT_PATH)\n print('saved checkpoint to', CKPT_PATH)\nelif CKPT_PATH.exists():\n ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n model.load_state_dict(ckpt['model'])\n model.eval()\n print('loaded checkpoint from', CKPT_PATH)\nelse:\n print('No checkpoint found. Use fallback generation cell below.')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Optional fallback: nearest-neighbor by condition vector\nUSE_NEAREST_NEIGHBOR_FALLBACK = False\n\nrng = np.random.default_rng(SEED)\nN_SAMPLES = 24\nselected = rng.choice(len(test_ds), size=N_SAMPLES, replace=False)\n\ntest_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\nbaseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n\nif USE_NEAREST_NEIGHBOR_FALLBACK:\n generated = []\n for c in test_conds:\n dists = np.linalg.norm(conds_np - c[None, :], axis=1)\n idx = int(np.argmin(dists))\n generated.append(designs_np[idx])\n gen_designs = np.array(generated, dtype=np.float32)\nelse:\n model.eval()\n with th.no_grad():\n pred_low = model(th.tensor(test_conds, device=DEVICE))\n gen_designs_t = upsample_to_design(pred_low).clamp(0.0, 1.0)\n gen_designs = gen_designs_t.detach().cpu().numpy().astype(np.float32)\n\nprint('generated shape:', gen_designs.shape)\nprint('baseline shape:', baseline_designs.shape)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "conditions_records = []\nfor i in range(N_SAMPLES):\n rec = {}\n for j, k in enumerate(condition_keys):\n v = test_conds[i, j]\n rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n conditions_records.append(rec)\n\nnp.save(ARTIFACT_DIR / 'generated_designs.npy', gen_designs)\nnp.save(ARTIFACT_DIR / 'baseline_designs.npy', baseline_designs)\nwith open(ARTIFACT_DIR / 'conditions.json', 'w', encoding='utf-8') as f:\n json.dump(conditions_records, f, indent=2)\n\nprint('Saved artifacts to', ARTIFACT_DIR)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Quick visual check of generated designs\nfig, axes = plt.subplots(2, 4, figsize=(12, 6))\nfor i, ax in enumerate(axes.ravel()):\n ax.imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n ax.axis('off')\n ax.set_title(f'gen {i}')\nplt.tight_layout()\nplt.show()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Next\n\nContinue with **Notebook 02** to validate and evaluate generated designs against baselines.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/workshops/dcc26/02_evaluate_metrics.ipynb b/workshops/dcc26/02_evaluate_metrics.ipynb new file mode 100644 index 0000000..0b3165f --- /dev/null +++ b/workshops/dcc26/02_evaluate_metrics.ipynb @@ -0,0 +1,69 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 02: Evaluation + Metrics (DCC26)\n\nThis notebook covers the evaluation segment (20 minutes):\n\n1. Constraint checks for generated designs\n2. Physics-based simulation via `problem.simulate`\n3. Baseline comparison\n4. Export metrics and plots\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# %pip install -q engibench[beams2d] pandas matplotlib" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nassert ARTIFACT_DIR.exists(), 'Run Notebook 01 first (artifacts folder missing)'\n\ngen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\nbaseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\nwith open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n conditions = json.load(f)\n\nprint('generated:', gen_designs.shape)\nprint('baseline:', baseline_designs.shape)\nprint('conditions:', len(conditions))" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = Beams2D(seed=7)\n\nrows = []\nfor i, (g, b, cfg) in enumerate(zip(gen_designs, baseline_designs, conditions, strict=True)):\n g_viol = problem.check_constraints(design=g, config=cfg)\n b_viol = problem.check_constraints(design=b, config=cfg)\n\n # Reset before simulator calls for reproducibility\n problem.reset(seed=7)\n g_obj = float(problem.simulate(g, config=cfg)[0])\n problem.reset(seed=7)\n b_obj = float(problem.simulate(b, config=cfg)[0])\n\n rows.append({\n 'sample': i,\n 'gen_obj': g_obj,\n 'base_obj': b_obj,\n 'gen_minus_base': g_obj - b_obj,\n 'gen_violations': len(g_viol),\n 'base_violations': len(b_viol),\n })\n\nresults = pd.DataFrame(rows)\nresults.head()" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "def mean_pairwise_l2(designs: np.ndarray) -> float:\n flat = designs.reshape(designs.shape[0], -1)\n n = flat.shape[0]\n if n < 2:\n return 0.0\n dists = []\n for i in range(n):\n for j in range(i + 1, n):\n dists.append(float(np.linalg.norm(flat[i] - flat[j])))\n return float(np.mean(dists))\n\nsummary = {\n 'n_samples': int(len(results)),\n 'gen_obj_mean': float(results['gen_obj'].mean()),\n 'base_obj_mean': float(results['base_obj'].mean()),\n 'improvement_rate': float((results['gen_obj'] < results['base_obj']).mean()),\n 'gen_violation_ratio': float((results['gen_violations'] > 0).mean()),\n 'base_violation_ratio': float((results['base_violations'] > 0).mean()),\n 'gen_diversity_l2': mean_pairwise_l2(gen_designs),\n}\n\nsummary_df = pd.DataFrame([summary])\nsummary_df" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "results.to_csv(ARTIFACT_DIR / 'per_sample_metrics.csv', index=False)\nsummary_df.to_csv(ARTIFACT_DIR / 'metrics_summary.csv', index=False)\n\nfig, ax = plt.subplots(figsize=(7, 4))\nax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\nax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\nax.set_xlabel('Compliance objective (lower is better)')\nax.set_ylabel('Count')\nax.set_title('Generated vs baseline objective distribution')\nax.legend()\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'objective_histogram.png', dpi=150)\nplt.show()\n\nprint('Saved:')\nprint('-', ARTIFACT_DIR / 'per_sample_metrics.csv')\nprint('-', ARTIFACT_DIR / 'metrics_summary.csv')\nprint('-', ARTIFACT_DIR / 'objective_histogram.png')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Visual side-by-side sample grid\nfig, axes = plt.subplots(3, 4, figsize=(12, 8))\nfor i, ax in enumerate(axes.ravel()):\n if i >= 12:\n break\n pair_idx = i // 2\n if i % 2 == 0:\n ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'gen {pair_idx}')\n else:\n ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'base {pair_idx}')\n ax.axis('off')\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'design_grid.png', dpi=150)\nplt.show()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Interpretation hints\n\n- `improvement_rate` shows how often generated designs beat baselines on objective value.\n- `gen_violation_ratio` tracks practical feasibility pressure from constraints.\n- `gen_diversity_l2` is a simple diversity proxy across generated designs.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/workshops/dcc26/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/03_add_new_problem_scaffold.ipynb new file mode 100644 index 0000000..77909c4 --- /dev/null +++ b/workshops/dcc26/03_add_new_problem_scaffold.ipynb @@ -0,0 +1,48 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 03: Add a New Problem Scaffold (DCC26)\n\nThis notebook is a contribution-oriented walkthrough showing a **minimal EngiBench-compatible problem**.\n\nIt uses a toy simulator so participants can understand the required interface before implementing real physics backends.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport numpy as np\nfrom gymnasium import spaces\n\nfrom engibench.constraint import bounded\nfrom engibench.core import ObjectiveDirection\nfrom engibench.core import OptiStep\nfrom engibench.core import Problem\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "class ToyDensityProblem(Problem[np.ndarray]):\n \"\"\"Minimal toy problem scaffold for workshop teaching.\"\"\"\n\n version = 0\n objectives = ((\"toy_cost\", ObjectiveDirection.MINIMIZE),)\n\n @dataclass\n class Conditions:\n target_density: Annotated[float, bounded(lower=0.0, upper=1.0)] = 0.5\n\n @dataclass\n class Config(Conditions):\n resolution: Annotated[int, bounded(lower=4, upper=128)] = 16\n max_iter: Annotated[int, bounded(lower=1, upper=200)] = 20\n\n dataset_id = \"IDEALLab/beams_2d_50_100_v0\" # placeholder to satisfy scaffold\n container_id = None\n\n def __init__(self, seed: int = 0, **kwargs):\n super().__init__(seed=seed)\n self.config = self.Config(**kwargs)\n self.conditions = self.Conditions(target_density=self.config.target_density)\n self.design_space = spaces.Box(\n low=0.0, high=1.0, shape=(self.config.resolution, self.config.resolution), dtype=np.float32\n )\n\n def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n cfg = {\"target_density\": self.config.target_density, **(config or {})}\n # Toy objective: mismatch to target density + smoothness penalty\n density_term = abs(float(design.mean()) - float(cfg[\"target_density\"]))\n smoothness = float(np.mean(np.abs(np.diff(design, axis=0))))\n return np.array([density_term + 0.1 * smoothness], dtype=np.float32)\n\n def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n cfg = {\"target_density\": self.config.target_density, **(config or {})}\n x = starting_point.copy().astype(np.float32)\n hist = []\n for step in range(self.config.max_iter):\n # Toy update toward target density (not a real optimizer)\n x = np.clip(x + 0.2 * (cfg[\"target_density\"] - x), 0.0, 1.0)\n hist.append(OptiStep(obj_values=self.simulate(x, cfg), step=step))\n return x, hist\n\n def render(self, design: np.ndarray, *, open_window: bool = False):\n import matplotlib.pyplot as plt\n\n fig, ax = plt.subplots(figsize=(4, 4))\n ax.imshow(design, cmap=\"viridis\", vmin=0, vmax=1)\n ax.set_title(\"ToyDensityProblem design\")\n ax.axis(\"off\")\n if open_window:\n plt.show()\n return fig, ax\n\n def random_design(self):\n d = self.np_random.random(self.design_space.shape).astype(np.float32)\n return d, -1\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = ToyDensityProblem(seed=42, resolution=16, target_density=0.4, max_iter=10)\nstart, _ = problem.random_design()\n\nprint('design space:', problem.design_space)\nprint('objectives:', problem.objectives)\nprint('conditions:', problem.conditions)\n\nviol = problem.check_constraints(start, config={'target_density': 0.4, 'resolution': 16, 'max_iter': 10})\nprint('constraint violations:', len(viol))\n\nobj0 = problem.simulate(start, config={'target_density': 0.4})\nopt_design, history = problem.optimize(start, config={'target_density': 0.4})\nobjf = problem.simulate(opt_design, config={'target_density': 0.4})\n\nprint('initial objective:', float(obj0[0]))\nprint('final objective:', float(objf[0]))\nprint('optimization steps:', len(history))\n\nproblem.render(opt_design)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Mapping to real EngiBench contributions\n\nThis toy scaffold demonstrates the interface shape only. For real contributions:\n\n1. Create `engibench/problems//v0.py`\n2. Implement real `simulate` and (optionally) `optimize`\n3. Provide `conditions`, `design_space`, and `dataset_id`\n4. Add docs and tests\n\nReference docs: [Adding a new problem](https://github.com/IDEALLab/EngiBench/blob/main/docs/tutorials/new_problem.md)\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md new file mode 100644 index 0000000..5beffaa --- /dev/null +++ b/workshops/dcc26/README.md @@ -0,0 +1,66 @@ +# DCC 2026 Workshop Notebook Suite + +This folder contains the DCC'26 hands-on notebook suite for benchmarking AI methods in engineering design with EngiBench and EngiOpt. + +## Workshop flow (3.5h) + +- `00_setup_api_warmup.ipynb` (10-15 min) + - Environment setup + - Problem + dataset inspection + - Rendering and constraint checks + +- `01_train_generate.ipynb` (30 min) + - Lightweight conditional generator training + - Deterministic seeds + - Fallback path with nearest-neighbor generation + +- `02_evaluate_metrics.ipynb` (20 min) + - Constraint validation + - Physics simulation + - Baseline comparison + - Metric and artifact export + +- `03_add_new_problem_scaffold.ipynb` (25 min) + - Minimal `Problem` scaffold + - Toy simulator and optimization loop + - Mapping to contribution docs + +## Runtime assumptions + +- Primary live problem: `Beams2D` +- No container-dependent problems are required during workshop exercises +- W&B integration is optional and disabled by default + +## Colab setup + +Use the pinned requirements in `requirements-colab.txt`. + +## Output artifacts + +By default, notebooks write generated artifacts to: + +- `workshops/dcc26/artifacts/` + +These include: + +- `generated_designs.npy` +- `baseline_designs.npy` +- `conditions.json` +- `metrics_summary.csv` +- `objective_histogram.png` +- `design_grid.png` + +## Facilitator fallback policy + +If runtime is constrained: + +1. Skip long training in `01_train_generate.ipynb` by enabling fallback mode. +2. Continue directly to `02_evaluate_metrics.ipynb` with fallback-generated designs. +3. Keep `03_add_new_problem_scaffold.ipynb` as the capstone for extensibility. + +## Suggested pre-workshop checks + +1. Run all notebooks once in fresh Colab runtime. +2. Confirm dataset download succeeds. +3. Confirm artifacts are generated in the expected folder. +4. Confirm no cell requires W&B auth unless explicitly enabled. diff --git a/workshops/dcc26/requirements-colab.txt b/workshops/dcc26/requirements-colab.txt new file mode 100644 index 0000000..75ffb20 --- /dev/null +++ b/workshops/dcc26/requirements-colab.txt @@ -0,0 +1,9 @@ +engibench[beams2d]>=0.1.0 +engiopt>=0.0.1 +torch>=2.5.0 +torchvision>=0.20.1 +numpy>=1.26 +pandas>=2.2 +matplotlib>=3.9 +seaborn>=0.13 +scikit-learn>=1.6 diff --git a/workshops/dcc26/utils/__init__.py b/workshops/dcc26/utils/__init__.py new file mode 100644 index 0000000..bb5dd63 --- /dev/null +++ b/workshops/dcc26/utils/__init__.py @@ -0,0 +1 @@ +"""Utilities for DCC26 workshop notebooks.""" diff --git a/workshops/dcc26/utils/notebook_helpers.py b/workshops/dcc26/utils/notebook_helpers.py new file mode 100644 index 0000000..c1df904 --- /dev/null +++ b/workshops/dcc26/utils/notebook_helpers.py @@ -0,0 +1,49 @@ +"""Helper utilities for DCC26 workshop notebooks.""" + +from __future__ import annotations + +import json +import os +import random +from typing import Any + +import numpy as np +import torch as th + + +def set_global_seed(seed: int) -> None: + """Set seeds for reproducibility across numpy, python, and torch.""" + random.seed(seed) + np.random.seed(seed) + th.manual_seed(seed) + if th.cuda.is_available(): + th.cuda.manual_seed_all(seed) + th.backends.cudnn.deterministic = True + th.backends.cudnn.benchmark = False + + +def pick_device() -> th.device: + """Pick an available torch device in priority order.""" + if th.backends.mps.is_available(): + return th.device("mps") + if th.cuda.is_available(): + return th.device("cuda") + return th.device("cpu") + + +def ensure_dir(path: str) -> str: + """Ensure a directory exists and return the path.""" + os.makedirs(path, exist_ok=True) + return path + + +def save_json(data: Any, path: str) -> None: + """Save Python data as a JSON file.""" + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + +def load_json(path: str) -> Any: + """Load Python data from a JSON file.""" + with open(path, encoding="utf-8") as f: + return json.load(f) From 4e16c9749206140bd72f4187f905bea55ee483f8 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 08:33:09 +0100 Subject: [PATCH 02/37] Document DCC26 workshop notebooks in EngiOpt README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 6b828b5..ff064ed 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,17 @@ We have some colab notebooks that show how to use some of the EngiBench/EngiOpt * [Example easy model (GAN)](https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/main/example_easy_model.ipynb) * [Example hard model (Diffusion)](https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/main/example_hard_model.ipynb) +## Workshop notebooks + +For the DCC'26 hands-on tutorial flow, see: + +- `workshops/dcc26/00_setup_api_warmup.ipynb` +- `workshops/dcc26/01_train_generate.ipynb` +- `workshops/dcc26/02_evaluate_metrics.ipynb` +- `workshops/dcc26/03_add_new_problem_scaffold.ipynb` + +See `workshops/dcc26/README.md` for the agenda mapping, fallback path, and artifact outputs. + ## Citing From 1ee4b4ba2898ba07444f9daef835c804743ccc37 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 09:08:27 +0100 Subject: [PATCH 03/37] Split DCC26 notebooks into participant and solution tracks --- README.md | 14 ++-- workshops/dcc26/README.md | 15 ++-- .../participant/00_setup_api_warmup.ipynb | 74 +++++++++++++++++ .../dcc26/participant/01_train_generate.ipynb | 83 +++++++++++++++++++ .../participant/02_evaluate_metrics.ipynb | 69 +++++++++++++++ .../03_add_new_problem_scaffold.ipynb | 48 +++++++++++ .../{ => solutions}/00_setup_api_warmup.ipynb | 0 .../{ => solutions}/01_train_generate.ipynb | 0 .../{ => solutions}/02_evaluate_metrics.ipynb | 0 .../03_add_new_problem_scaffold.ipynb | 0 10 files changed, 293 insertions(+), 10 deletions(-) create mode 100644 workshops/dcc26/participant/00_setup_api_warmup.ipynb create mode 100644 workshops/dcc26/participant/01_train_generate.ipynb create mode 100644 workshops/dcc26/participant/02_evaluate_metrics.ipynb create mode 100644 workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb rename workshops/dcc26/{ => solutions}/00_setup_api_warmup.ipynb (100%) rename workshops/dcc26/{ => solutions}/01_train_generate.ipynb (100%) rename workshops/dcc26/{ => solutions}/02_evaluate_metrics.ipynb (100%) rename workshops/dcc26/{ => solutions}/03_add_new_problem_scaffold.ipynb (100%) diff --git a/README.md b/README.md index ff064ed..0a793fc 100644 --- a/README.md +++ b/README.md @@ -113,12 +113,16 @@ We have some colab notebooks that show how to use some of the EngiBench/EngiOpt For the DCC'26 hands-on tutorial flow, see: -- `workshops/dcc26/00_setup_api_warmup.ipynb` -- `workshops/dcc26/01_train_generate.ipynb` -- `workshops/dcc26/02_evaluate_metrics.ipynb` -- `workshops/dcc26/03_add_new_problem_scaffold.ipynb` +- `workshops/dcc26/participant/00_setup_api_warmup.ipynb` +- `workshops/dcc26/participant/01_train_generate.ipynb` +- `workshops/dcc26/participant/02_evaluate_metrics.ipynb` +- `workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb` -See `workshops/dcc26/README.md` for the agenda mapping, fallback path, and artifact outputs. +Facilitator solutions are in: + +- `workshops/dcc26/solutions/` + +See `workshops/dcc26/README.md` for agenda mapping, fallback path, and artifact outputs. ## Citing diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index 5beffaa..86bb64d 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -2,25 +2,30 @@ This folder contains the DCC'26 hands-on notebook suite for benchmarking AI methods in engineering design with EngiBench and EngiOpt. +It is split into two tracks: + +- `participant/`: notebooks with `TODO` cells for attendees +- `solutions/`: fully completed facilitator notebooks + ## Workshop flow (3.5h) -- `00_setup_api_warmup.ipynb` (10-15 min) +- `participant/00_setup_api_warmup.ipynb` and `solutions/00_setup_api_warmup.ipynb` (10-15 min) - Environment setup - Problem + dataset inspection - Rendering and constraint checks -- `01_train_generate.ipynb` (30 min) +- `participant/01_train_generate.ipynb` and `solutions/01_train_generate.ipynb` (30 min) - Lightweight conditional generator training - Deterministic seeds - Fallback path with nearest-neighbor generation -- `02_evaluate_metrics.ipynb` (20 min) +- `participant/02_evaluate_metrics.ipynb` and `solutions/02_evaluate_metrics.ipynb` (20 min) - Constraint validation - Physics simulation - Baseline comparison - Metric and artifact export -- `03_add_new_problem_scaffold.ipynb` (25 min) +- `participant/03_add_new_problem_scaffold.ipynb` and `solutions/03_add_new_problem_scaffold.ipynb` (25 min) - Minimal `Problem` scaffold - Toy simulator and optimization loop - Mapping to contribution docs @@ -37,7 +42,7 @@ Use the pinned requirements in `requirements-colab.txt`. ## Output artifacts -By default, notebooks write generated artifacts to: +By default, solution notebooks write generated artifacts to: - `workshops/dcc26/artifacts/` diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb new file mode 100644 index 0000000..3ee9df9 --- /dev/null +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -0,0 +1,74 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 00 (Participant): Setup + API Warmup\n\nComplete the `TODO` sections.\n\nGoal: use `Beams2D` API to inspect problem metadata, sample data, rendering, and constraints.\n" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Optional install cell (fresh Colab)\n\nUncomment if your runtime does not already contain required packages.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# %pip install -q engibench[beams2d] engiopt matplotlib seaborn" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import random\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nimport engibench\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nprint('engibench version:', engibench.__version__)\nprint('seed:', SEED)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 1: instantiate the problem with the global SEED\n# problem = ...\n\n# TODO 2: print these fields\n# - type(problem).__name__\n# - problem.design_space\n# - problem.objectives\n# - problem.conditions\n# - problem.conditions_keys\n# - problem.dataset_id\n\nraise NotImplementedError('Complete TODO 1/2 in this cell')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 3: load dataset and extract one sample\n# dataset = problem.dataset\n# sample_idx = 0\n# design = ...\n# config = ... # dict over problem.conditions_keys\n\n# print dataset summary and sample shapes/values\n\nraise NotImplementedError('Complete TODO 3 in this cell')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Render the sampled design (run after TODO 3)\nfig, ax = problem.render(design)\nax.set_title('Participant: sampled Beams2D design')\nplt.show()" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 4: run one explicit constraint check with an intentionally mismatched volfrac\n# bad_config = dict(config)\n# bad_config['volfrac'] = 0.2\n# violations = ...\n# print(len(violations)); print(violations) if any\n\nraise NotImplementedError('Complete TODO 4 in this cell')" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Next\n\nContinue with **Notebook 01** to train a lightweight conditional generator and create designs for evaluation.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb new file mode 100644 index 0000000..d497890 --- /dev/null +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -0,0 +1,83 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 01 (Participant): Train + Generate\n\nComplete the `TODO` sections to train a lightweight conditional model and generate designs.\n\nFallback: set `USE_NEAREST_NEIGHBOR_FALLBACK = True` if training is too slow.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# %pip install -q engibench[beams2d] torch torchvision matplotlib pandas" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import json\nimport os\nimport random\nfrom pathlib import Path\n\nimport numpy as np\nimport torch as th\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, TensorDataset\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\nth.manual_seed(SEED)\nif th.cuda.is_available():\n th.cuda.manual_seed_all(SEED)\n\nDEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\nprint('device:', DEVICE)\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\nCKPT_PATH = ARTIFACT_DIR / 'mini_cond_generator.pt'\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = Beams2D(seed=SEED)\ntrain_ds = problem.dataset['train']\ntest_ds = problem.dataset['test']\n\ncondition_keys = problem.conditions_keys\nprint('condition keys:', condition_keys)\n\n# Build compact train subset to keep runtime stable in workshop\nN_TRAIN = 512\nsubset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n\nconds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\ndesigns_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n\n# Downsample target to reduce model output size and speed up training\ndesigns_t = th.tensor(designs_np).unsqueeze(1)\nlowres_t = F.interpolate(designs_t, size=(25, 50), mode='bilinear', align_corners=False).squeeze(1)\ntargets_np = lowres_t.reshape(N_TRAIN, -1).numpy()\n\nprint('conditions shape:', conds_np.shape)\nprint('designs shape:', designs_np.shape)\nprint('lowres target shape:', targets_np.shape)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 1: implement MiniCondGenerator\n# Suggested architecture:\n# Linear(in_dim, 64) -> ReLU -> Linear(64, 128) -> ReLU -> Linear(128, out_dim) -> Sigmoid\n\nclass MiniCondGenerator(nn.Module):\n def __init__(self, in_dim: int, out_dim: int):\n super().__init__()\n # TODO: define self.net\n raise NotImplementedError('Define model layers')\n\n def forward(self, x):\n # TODO: return forward pass\n raise NotImplementedError('Implement forward pass')\n\n\ndef upsample_to_design(y_flat: th.Tensor) -> th.Tensor:\n low = y_flat.reshape(-1, 1, 25, 50)\n high = F.interpolate(low, size=(50, 100), mode='bilinear', align_corners=False)\n return high.squeeze(1)\n\n# TODO 2: instantiate model/optimizer/loss\n# model = ...\n# optimizer = ...\n# criterion = ...\n\nraise NotImplementedError('Complete TODO 1/2 in this cell')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "TRAIN_FROM_SCRATCH = True\nEPOCHS = 8\nBATCH_SIZE = 64\n\nif TRAIN_FROM_SCRATCH:\n # TODO 3: implement training loop over DataLoader\n # - forward\n # - MSE loss\n # - backward/update\n # - print epoch loss\n # - save checkpoint to CKPT_PATH\n raise NotImplementedError('Complete TODO 3 training loop')\nelif CKPT_PATH.exists():\n ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n model.load_state_dict(ckpt['model'])\n model.eval()\n print('loaded checkpoint from', CKPT_PATH)\nelse:\n print('No checkpoint found. Use fallback generation cell below.')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 4: implement generation path\n# - sample N_SAMPLES test conditions\n# - if USE_NEAREST_NEIGHBOR_FALLBACK: nearest-neighbor designs from train subset\n# - else: model forward + upsample\n# - set `gen_designs`, `baseline_designs`, `test_conds`\n\nUSE_NEAREST_NEIGHBOR_FALLBACK = False\n\nraise NotImplementedError('Complete TODO 4 generation path')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 5: serialize artifacts for Notebook 02\n# - generated_designs.npy\n# - baseline_designs.npy\n# - conditions.json\n\nraise NotImplementedError('Complete TODO 5 artifact export')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Quick visual check of generated designs\nfig, axes = plt.subplots(2, 4, figsize=(12, 6))\nfor i, ax in enumerate(axes.ravel()):\n ax.imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n ax.axis('off')\n ax.set_title(f'gen {i}')\nplt.tight_layout()\nplt.show()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Next\n\nContinue with **Notebook 02** to validate and evaluate generated designs against baselines.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb new file mode 100644 index 0000000..a8f51e9 --- /dev/null +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -0,0 +1,69 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 02 (Participant): Evaluation + Metrics\n\nComplete the `TODO` sections to evaluate generated designs and export metrics.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# %pip install -q engibench[beams2d] pandas matplotlib" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nassert ARTIFACT_DIR.exists(), 'Run Notebook 01 first (artifacts folder missing)'\n\ngen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\nbaseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\nwith open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n conditions = json.load(f)\n\nprint('generated:', gen_designs.shape)\nprint('baseline:', baseline_designs.shape)\nprint('conditions:', len(conditions))" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = Beams2D(seed=7)\n\n# TODO 1: implement per-sample evaluation loop\n# For each (generated, baseline, config):\n# - check constraints for generated and baseline\n# - run problem.simulate for each\n# - append dict rows to `rows` with:\n# sample, gen_obj, base_obj, gen_minus_base, gen_violations, base_violations\n\nrows = []\nraise NotImplementedError('Complete TODO 1 evaluation loop')\n\nresults = pd.DataFrame(rows)\nresults.head()" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 2: implement summary metrics\n# - n_samples\n# - gen_obj_mean\n# - base_obj_mean\n# - improvement_rate\n# - gen_violation_ratio\n# - base_violation_ratio\n# - gen_diversity_l2 (use helper below)\n\ndef mean_pairwise_l2(designs: np.ndarray) -> float:\n flat = designs.reshape(designs.shape[0], -1)\n n = flat.shape[0]\n if n < 2:\n return 0.0\n dists = []\n for i in range(n):\n for j in range(i + 1, n):\n dists.append(float(np.linalg.norm(flat[i] - flat[j])))\n return float(np.mean(dists))\n\nraise NotImplementedError('Complete TODO 2 summary metrics')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "results.to_csv(ARTIFACT_DIR / 'per_sample_metrics.csv', index=False)\nsummary_df.to_csv(ARTIFACT_DIR / 'metrics_summary.csv', index=False)\n\nfig, ax = plt.subplots(figsize=(7, 4))\nax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\nax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\nax.set_xlabel('Compliance objective (lower is better)')\nax.set_ylabel('Count')\nax.set_title('Generated vs baseline objective distribution')\nax.legend()\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'objective_histogram.png', dpi=150)\nplt.show()\n\nprint('Saved:')\nprint('-', ARTIFACT_DIR / 'per_sample_metrics.csv')\nprint('-', ARTIFACT_DIR / 'metrics_summary.csv')\nprint('-', ARTIFACT_DIR / 'objective_histogram.png')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Visual side-by-side sample grid\nfig, axes = plt.subplots(3, 4, figsize=(12, 8))\nfor i, ax in enumerate(axes.ravel()):\n if i >= 12:\n break\n pair_idx = i // 2\n if i % 2 == 0:\n ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'gen {pair_idx}')\n else:\n ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'base {pair_idx}')\n ax.axis('off')\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'design_grid.png', dpi=150)\nplt.show()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Interpretation hints\n\n- `improvement_rate` shows how often generated designs beat baselines on objective value.\n- `gen_violation_ratio` tracks practical feasibility pressure from constraints.\n- `gen_diversity_l2` is a simple diversity proxy across generated designs.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb new file mode 100644 index 0000000..8c11a3c --- /dev/null +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -0,0 +1,48 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 03 (Participant): Add a New Problem Scaffold\n\nComplete the `TODO` sections to build a minimal EngiBench-compatible toy problem.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport numpy as np\nfrom gymnasium import spaces\n\nfrom engibench.constraint import bounded\nfrom engibench.core import ObjectiveDirection\nfrom engibench.core import OptiStep\nfrom engibench.core import Problem\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "class ToyDensityProblem(Problem[np.ndarray]):\n \"\"\"Minimal toy problem scaffold for workshop teaching.\"\"\"\n\n version = 0\n objectives = ((\"toy_cost\", ObjectiveDirection.MINIMIZE),)\n\n @dataclass\n class Conditions:\n target_density: Annotated[float, bounded(lower=0.0, upper=1.0)] = 0.5\n\n @dataclass\n class Config(Conditions):\n resolution: Annotated[int, bounded(lower=4, upper=128)] = 16\n max_iter: Annotated[int, bounded(lower=1, upper=200)] = 20\n\n dataset_id = \"IDEALLab/beams_2d_50_100_v0\" # placeholder to satisfy scaffold\n container_id = None\n\n def __init__(self, seed: int = 0, **kwargs):\n super().__init__(seed=seed)\n self.config = self.Config(**kwargs)\n self.conditions = self.Conditions(target_density=self.config.target_density)\n self.design_space = spaces.Box(\n low=0.0, high=1.0, shape=(self.config.resolution, self.config.resolution), dtype=np.float32\n )\n\n def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n # TODO 1: implement toy objective\n # Suggested terms:\n # - density mismatch to target_density\n # - smoothness penalty based on np.diff\n raise NotImplementedError('Implement simulate')\n\n def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n # TODO 2: implement simple iterative optimizer\n # - update design toward target density\n # - append OptiStep each iteration\n raise NotImplementedError('Implement optimize')\n\n def render(self, design: np.ndarray, *, open_window: bool = False):\n import matplotlib.pyplot as plt\n\n fig, ax = plt.subplots(figsize=(4, 4))\n ax.imshow(design, cmap=\"viridis\", vmin=0, vmax=1)\n ax.set_title(\"ToyDensityProblem design\")\n ax.axis(\"off\")\n if open_window:\n plt.show()\n return fig, ax\n\n def random_design(self):\n d = self.np_random.random(self.design_space.shape).astype(np.float32)\n return d, -1" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Run this cell after finishing TODOs in ToyDensityProblem\nproblem = ToyDensityProblem(seed=42, resolution=16, target_density=0.4, max_iter=10)\nstart, _ = problem.random_design()\n\nprint('design space:', problem.design_space)\nprint('objectives:', problem.objectives)\nprint('conditions:', problem.conditions)\n\nviol = problem.check_constraints(start, config={'target_density': 0.4, 'resolution': 16, 'max_iter': 10})\nprint('constraint violations:', len(viol))\n\nobj0 = problem.simulate(start, config={'target_density': 0.4})\nopt_design, history = problem.optimize(start, config={'target_density': 0.4})\nobjf = problem.simulate(opt_design, config={'target_density': 0.4})\n\nprint('initial objective:', float(obj0[0]))\nprint('final objective:', float(objf[0]))\nprint('optimization steps:', len(history))\n\nproblem.render(opt_design)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Mapping to real EngiBench contributions\n\nThis toy scaffold demonstrates the interface shape only. For real contributions:\n\n1. Create `engibench/problems//v0.py`\n2. Implement real `simulate` and (optionally) `optimize`\n3. Provide `conditions`, `design_space`, and `dataset_id`\n4. Add docs and tests\n\nReference docs: [Adding a new problem](https://github.com/IDEALLab/EngiBench/blob/main/docs/tutorials/new_problem.md)\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/workshops/dcc26/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb similarity index 100% rename from workshops/dcc26/00_setup_api_warmup.ipynb rename to workshops/dcc26/solutions/00_setup_api_warmup.ipynb diff --git a/workshops/dcc26/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb similarity index 100% rename from workshops/dcc26/01_train_generate.ipynb rename to workshops/dcc26/solutions/01_train_generate.ipynb diff --git a/workshops/dcc26/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb similarity index 100% rename from workshops/dcc26/02_evaluate_metrics.ipynb rename to workshops/dcc26/solutions/02_evaluate_metrics.ipynb diff --git a/workshops/dcc26/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb similarity index 100% rename from workshops/dcc26/03_add_new_problem_scaffold.ipynb rename to workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb From 5cc0c8be097ab6204ec0d80d8dc7a098788dc68f Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 09:23:20 +0100 Subject: [PATCH 04/37] Ignore generated DCC26 workshop artifacts --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 463236b..6c3a452 100644 --- a/.gitignore +++ b/.gitignore @@ -171,3 +171,4 @@ logs/* .ruff_cache/ engibench_studies/* +workshops/dcc26/artifacts/* From db93175f688cb05f6f774f2061a45518a8a0520b Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 09:41:46 +0100 Subject: [PATCH 05/37] Make DCC26 notebooks Colab-aware with conditional installs --- workshops/dcc26/README.md | 18 ++ .../participant/00_setup_api_warmup.ipynb | 157 +++++++++------- .../dcc26/participant/01_train_generate.ipynb | 175 ++++++++++-------- .../participant/02_evaluate_metrics.ipynb | 147 ++++++++------- .../03_add_new_problem_scaffold.ipynb | 112 ++++++----- .../dcc26/solutions/00_setup_api_warmup.ipynb | 157 +++++++++------- .../dcc26/solutions/01_train_generate.ipynb | 175 ++++++++++-------- .../dcc26/solutions/02_evaluate_metrics.ipynb | 147 ++++++++------- .../03_add_new_problem_scaffold.ipynb | 112 ++++++----- 9 files changed, 676 insertions(+), 524 deletions(-) diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index 86bb64d..890a04a 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -40,6 +40,24 @@ It is split into two tracks: Use the pinned requirements in `requirements-colab.txt`. +All notebooks now include a conditional dependency bootstrap cell: + +- On Colab: installs required packages automatically. +- On local envs: skips install by default (`FORCE_INSTALL = False`). + +## Open in Colab + +Pre-merge (current branch) links: + +- Participant 00: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/00_setup_api_warmup.ipynb +- Participant 01: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/01_train_generate.ipynb +- Participant 02: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/02_evaluate_metrics.ipynb +- Participant 03: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +- Solution 00: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +- Solution 01: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/01_train_generate.ipynb +- Solution 02: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +- Solution 03: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb + ## Output artifacts By default, solution notebooks write generated artifacts to: diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 3ee9df9..c59d9e4 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -1,74 +1,89 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": "# Notebook 00 (Participant): Setup + API Warmup\n\nComplete the `TODO` sections.\n\nGoal: use `Beams2D` API to inspect problem metadata, sample data, rendering, and constraints.\n" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## Optional install cell (fresh Colab)\n\nUncomment if your runtime does not already contain required packages.\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# %pip install -q engibench[beams2d] engiopt matplotlib seaborn" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "import random\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nimport engibench\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nprint('engibench version:', engibench.__version__)\nprint('seed:', SEED)" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# TODO 1: instantiate the problem with the global SEED\n# problem = ...\n\n# TODO 2: print these fields\n# - type(problem).__name__\n# - problem.design_space\n# - problem.objectives\n# - problem.conditions\n# - problem.conditions_keys\n# - problem.dataset_id\n\nraise NotImplementedError('Complete TODO 1/2 in this cell')" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# TODO 3: load dataset and extract one sample\n# dataset = problem.dataset\n# sample_idx = 0\n# design = ...\n# config = ... # dict over problem.conditions_keys\n\n# print dataset summary and sample shapes/values\n\nraise NotImplementedError('Complete TODO 3 in this cell')" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# Render the sampled design (run after TODO 3)\nfig, ax = problem.render(design)\nax.set_title('Participant: sampled Beams2D design')\nplt.show()" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# TODO 4: run one explicit constraint check with an intentionally mismatched volfrac\n# bad_config = dict(config)\n# bad_config['volfrac'] = 0.2\n# violations = ...\n# print(len(violations)); print(violations) if any\n\nraise NotImplementedError('Complete TODO 4 in this cell')" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## Next\n\nContinue with **Notebook 01** to train a lightweight conditional generator and create designs for evaluation.\n" - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.10" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 00 (Participant): Setup + API Warmup\n\nComplete the `TODO` sections.\n\nGoal: use `Beams2D` API to inspect problem metadata, sample data, rendering, and constraints.\n" }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Optional install cell (fresh Colab)\n\nUncomment if your runtime does not already contain required packages.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Colab/local dependency bootstrap\n", + "import subprocess\n", + "import sys\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", + "PACKAGES = ['engibench[beams2d]', 'engiopt', 'matplotlib', 'seaborn']\n", + "\n", + "if IN_COLAB or FORCE_INSTALL:\n", + " print('Installing dependencies...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " print('Dependency install complete.')\n", + "else:\n", + " print('Skipping install (using current environment).')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import random\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nimport engibench\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nprint('engibench version:', engibench.__version__)\nprint('seed:', SEED)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 1: instantiate the problem with the global SEED\n# problem = ...\n\n# TODO 2: print these fields\n# - type(problem).__name__\n# - problem.design_space\n# - problem.objectives\n# - problem.conditions\n# - problem.conditions_keys\n# - problem.dataset_id\n\nraise NotImplementedError('Complete TODO 1/2 in this cell')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 3: load dataset and extract one sample\n# dataset = problem.dataset\n# sample_idx = 0\n# design = ...\n# config = ... # dict over problem.conditions_keys\n\n# print dataset summary and sample shapes/values\n\nraise NotImplementedError('Complete TODO 3 in this cell')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Render the sampled design (run after TODO 3)\nfig, ax = problem.render(design)\nax.set_title('Participant: sampled Beams2D design')\nplt.show()" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 4: run one explicit constraint check with an intentionally mismatched volfrac\n# bad_config = dict(config)\n# bad_config['volfrac'] = 0.2\n# violations = ...\n# print(len(violations)); print(violations) if any\n\nraise NotImplementedError('Complete TODO 4 in this cell')" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Next\n\nContinue with **Notebook 01** to train a lightweight conditional generator and create designs for evaluation.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index d497890..2ca330c 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -1,83 +1,98 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": "# Notebook 01 (Participant): Train + Generate\n\nComplete the `TODO` sections to train a lightweight conditional model and generate designs.\n\nFallback: set `USE_NEAREST_NEIGHBOR_FALLBACK = True` if training is too slow.\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# %pip install -q engibench[beams2d] torch torchvision matplotlib pandas" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "import json\nimport os\nimport random\nfrom pathlib import Path\n\nimport numpy as np\nimport torch as th\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, TensorDataset\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\nth.manual_seed(SEED)\nif th.cuda.is_available():\n th.cuda.manual_seed_all(SEED)\n\nDEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\nprint('device:', DEVICE)\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\nCKPT_PATH = ARTIFACT_DIR / 'mini_cond_generator.pt'\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "problem = Beams2D(seed=SEED)\ntrain_ds = problem.dataset['train']\ntest_ds = problem.dataset['test']\n\ncondition_keys = problem.conditions_keys\nprint('condition keys:', condition_keys)\n\n# Build compact train subset to keep runtime stable in workshop\nN_TRAIN = 512\nsubset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n\nconds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\ndesigns_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n\n# Downsample target to reduce model output size and speed up training\ndesigns_t = th.tensor(designs_np).unsqueeze(1)\nlowres_t = F.interpolate(designs_t, size=(25, 50), mode='bilinear', align_corners=False).squeeze(1)\ntargets_np = lowres_t.reshape(N_TRAIN, -1).numpy()\n\nprint('conditions shape:', conds_np.shape)\nprint('designs shape:', designs_np.shape)\nprint('lowres target shape:', targets_np.shape)" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# TODO 1: implement MiniCondGenerator\n# Suggested architecture:\n# Linear(in_dim, 64) -> ReLU -> Linear(64, 128) -> ReLU -> Linear(128, out_dim) -> Sigmoid\n\nclass MiniCondGenerator(nn.Module):\n def __init__(self, in_dim: int, out_dim: int):\n super().__init__()\n # TODO: define self.net\n raise NotImplementedError('Define model layers')\n\n def forward(self, x):\n # TODO: return forward pass\n raise NotImplementedError('Implement forward pass')\n\n\ndef upsample_to_design(y_flat: th.Tensor) -> th.Tensor:\n low = y_flat.reshape(-1, 1, 25, 50)\n high = F.interpolate(low, size=(50, 100), mode='bilinear', align_corners=False)\n return high.squeeze(1)\n\n# TODO 2: instantiate model/optimizer/loss\n# model = ...\n# optimizer = ...\n# criterion = ...\n\nraise NotImplementedError('Complete TODO 1/2 in this cell')" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "TRAIN_FROM_SCRATCH = True\nEPOCHS = 8\nBATCH_SIZE = 64\n\nif TRAIN_FROM_SCRATCH:\n # TODO 3: implement training loop over DataLoader\n # - forward\n # - MSE loss\n # - backward/update\n # - print epoch loss\n # - save checkpoint to CKPT_PATH\n raise NotImplementedError('Complete TODO 3 training loop')\nelif CKPT_PATH.exists():\n ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n model.load_state_dict(ckpt['model'])\n model.eval()\n print('loaded checkpoint from', CKPT_PATH)\nelse:\n print('No checkpoint found. Use fallback generation cell below.')" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# TODO 4: implement generation path\n# - sample N_SAMPLES test conditions\n# - if USE_NEAREST_NEIGHBOR_FALLBACK: nearest-neighbor designs from train subset\n# - else: model forward + upsample\n# - set `gen_designs`, `baseline_designs`, `test_conds`\n\nUSE_NEAREST_NEIGHBOR_FALLBACK = False\n\nraise NotImplementedError('Complete TODO 4 generation path')" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# TODO 5: serialize artifacts for Notebook 02\n# - generated_designs.npy\n# - baseline_designs.npy\n# - conditions.json\n\nraise NotImplementedError('Complete TODO 5 artifact export')" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# Quick visual check of generated designs\nfig, axes = plt.subplots(2, 4, figsize=(12, 6))\nfor i, ax in enumerate(axes.ravel()):\n ax.imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n ax.axis('off')\n ax.set_title(f'gen {i}')\nplt.tight_layout()\nplt.show()" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## Next\n\nContinue with **Notebook 02** to validate and evaluate generated designs against baselines.\n" - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.10" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 01 (Participant): Train + Generate\n\nComplete the `TODO` sections to train a lightweight conditional model and generate designs.\n\nFallback: set `USE_NEAREST_NEIGHBOR_FALLBACK = True` if training is too slow.\n" }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Colab/local dependency bootstrap\n", + "import subprocess\n", + "import sys\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", + "PACKAGES = ['engibench[beams2d]', 'torch', 'torchvision', 'matplotlib', 'pandas']\n", + "\n", + "if IN_COLAB or FORCE_INSTALL:\n", + " print('Installing dependencies...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " print('Dependency install complete.')\n", + "else:\n", + " print('Skipping install (using current environment).')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import json\nimport os\nimport random\nfrom pathlib import Path\n\nimport numpy as np\nimport torch as th\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, TensorDataset\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\nth.manual_seed(SEED)\nif th.cuda.is_available():\n th.cuda.manual_seed_all(SEED)\n\nDEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\nprint('device:', DEVICE)\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\nCKPT_PATH = ARTIFACT_DIR / 'mini_cond_generator.pt'\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = Beams2D(seed=SEED)\ntrain_ds = problem.dataset['train']\ntest_ds = problem.dataset['test']\n\ncondition_keys = problem.conditions_keys\nprint('condition keys:', condition_keys)\n\n# Build compact train subset to keep runtime stable in workshop\nN_TRAIN = 512\nsubset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n\nconds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\ndesigns_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n\n# Downsample target to reduce model output size and speed up training\ndesigns_t = th.tensor(designs_np).unsqueeze(1)\nlowres_t = F.interpolate(designs_t, size=(25, 50), mode='bilinear', align_corners=False).squeeze(1)\ntargets_np = lowres_t.reshape(N_TRAIN, -1).numpy()\n\nprint('conditions shape:', conds_np.shape)\nprint('designs shape:', designs_np.shape)\nprint('lowres target shape:', targets_np.shape)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 1: implement MiniCondGenerator\n# Suggested architecture:\n# Linear(in_dim, 64) -> ReLU -> Linear(64, 128) -> ReLU -> Linear(128, out_dim) -> Sigmoid\n\nclass MiniCondGenerator(nn.Module):\n def __init__(self, in_dim: int, out_dim: int):\n super().__init__()\n # TODO: define self.net\n raise NotImplementedError('Define model layers')\n\n def forward(self, x):\n # TODO: return forward pass\n raise NotImplementedError('Implement forward pass')\n\n\ndef upsample_to_design(y_flat: th.Tensor) -> th.Tensor:\n low = y_flat.reshape(-1, 1, 25, 50)\n high = F.interpolate(low, size=(50, 100), mode='bilinear', align_corners=False)\n return high.squeeze(1)\n\n# TODO 2: instantiate model/optimizer/loss\n# model = ...\n# optimizer = ...\n# criterion = ...\n\nraise NotImplementedError('Complete TODO 1/2 in this cell')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "TRAIN_FROM_SCRATCH = True\nEPOCHS = 8\nBATCH_SIZE = 64\n\nif TRAIN_FROM_SCRATCH:\n # TODO 3: implement training loop over DataLoader\n # - forward\n # - MSE loss\n # - backward/update\n # - print epoch loss\n # - save checkpoint to CKPT_PATH\n raise NotImplementedError('Complete TODO 3 training loop')\nelif CKPT_PATH.exists():\n ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n model.load_state_dict(ckpt['model'])\n model.eval()\n print('loaded checkpoint from', CKPT_PATH)\nelse:\n print('No checkpoint found. Use fallback generation cell below.')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 4: implement generation path\n# - sample N_SAMPLES test conditions\n# - if USE_NEAREST_NEIGHBOR_FALLBACK: nearest-neighbor designs from train subset\n# - else: model forward + upsample\n# - set `gen_designs`, `baseline_designs`, `test_conds`\n\nUSE_NEAREST_NEIGHBOR_FALLBACK = False\n\nraise NotImplementedError('Complete TODO 4 generation path')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 5: serialize artifacts for Notebook 02\n# - generated_designs.npy\n# - baseline_designs.npy\n# - conditions.json\n\nraise NotImplementedError('Complete TODO 5 artifact export')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Quick visual check of generated designs\nfig, axes = plt.subplots(2, 4, figsize=(12, 6))\nfor i, ax in enumerate(axes.ravel()):\n ax.imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n ax.axis('off')\n ax.set_title(f'gen {i}')\nplt.tight_layout()\nplt.show()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Next\n\nContinue with **Notebook 02** to validate and evaluate generated designs against baselines.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index a8f51e9..ae901b7 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -1,69 +1,84 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": "# Notebook 02 (Participant): Evaluation + Metrics\n\nComplete the `TODO` sections to evaluate generated designs and export metrics.\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# %pip install -q engibench[beams2d] pandas matplotlib" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "import json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nassert ARTIFACT_DIR.exists(), 'Run Notebook 01 first (artifacts folder missing)'\n\ngen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\nbaseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\nwith open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n conditions = json.load(f)\n\nprint('generated:', gen_designs.shape)\nprint('baseline:', baseline_designs.shape)\nprint('conditions:', len(conditions))" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "problem = Beams2D(seed=7)\n\n# TODO 1: implement per-sample evaluation loop\n# For each (generated, baseline, config):\n# - check constraints for generated and baseline\n# - run problem.simulate for each\n# - append dict rows to `rows` with:\n# sample, gen_obj, base_obj, gen_minus_base, gen_violations, base_violations\n\nrows = []\nraise NotImplementedError('Complete TODO 1 evaluation loop')\n\nresults = pd.DataFrame(rows)\nresults.head()" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# TODO 2: implement summary metrics\n# - n_samples\n# - gen_obj_mean\n# - base_obj_mean\n# - improvement_rate\n# - gen_violation_ratio\n# - base_violation_ratio\n# - gen_diversity_l2 (use helper below)\n\ndef mean_pairwise_l2(designs: np.ndarray) -> float:\n flat = designs.reshape(designs.shape[0], -1)\n n = flat.shape[0]\n if n < 2:\n return 0.0\n dists = []\n for i in range(n):\n for j in range(i + 1, n):\n dists.append(float(np.linalg.norm(flat[i] - flat[j])))\n return float(np.mean(dists))\n\nraise NotImplementedError('Complete TODO 2 summary metrics')" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "results.to_csv(ARTIFACT_DIR / 'per_sample_metrics.csv', index=False)\nsummary_df.to_csv(ARTIFACT_DIR / 'metrics_summary.csv', index=False)\n\nfig, ax = plt.subplots(figsize=(7, 4))\nax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\nax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\nax.set_xlabel('Compliance objective (lower is better)')\nax.set_ylabel('Count')\nax.set_title('Generated vs baseline objective distribution')\nax.legend()\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'objective_histogram.png', dpi=150)\nplt.show()\n\nprint('Saved:')\nprint('-', ARTIFACT_DIR / 'per_sample_metrics.csv')\nprint('-', ARTIFACT_DIR / 'metrics_summary.csv')\nprint('-', ARTIFACT_DIR / 'objective_histogram.png')" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# Visual side-by-side sample grid\nfig, axes = plt.subplots(3, 4, figsize=(12, 8))\nfor i, ax in enumerate(axes.ravel()):\n if i >= 12:\n break\n pair_idx = i // 2\n if i % 2 == 0:\n ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'gen {pair_idx}')\n else:\n ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'base {pair_idx}')\n ax.axis('off')\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'design_grid.png', dpi=150)\nplt.show()" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## Interpretation hints\n\n- `improvement_rate` shows how often generated designs beat baselines on objective value.\n- `gen_violation_ratio` tracks practical feasibility pressure from constraints.\n- `gen_diversity_l2` is a simple diversity proxy across generated designs.\n" - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.10" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 02 (Participant): Evaluation + Metrics\n\nComplete the `TODO` sections to evaluate generated designs and export metrics.\n" }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Colab/local dependency bootstrap\n", + "import subprocess\n", + "import sys\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", + "PACKAGES = ['engibench[beams2d]', 'pandas', 'matplotlib']\n", + "\n", + "if IN_COLAB or FORCE_INSTALL:\n", + " print('Installing dependencies...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " print('Dependency install complete.')\n", + "else:\n", + " print('Skipping install (using current environment).')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nassert ARTIFACT_DIR.exists(), 'Run Notebook 01 first (artifacts folder missing)'\n\ngen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\nbaseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\nwith open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n conditions = json.load(f)\n\nprint('generated:', gen_designs.shape)\nprint('baseline:', baseline_designs.shape)\nprint('conditions:', len(conditions))" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = Beams2D(seed=7)\n\n# TODO 1: implement per-sample evaluation loop\n# For each (generated, baseline, config):\n# - check constraints for generated and baseline\n# - run problem.simulate for each\n# - append dict rows to `rows` with:\n# sample, gen_obj, base_obj, gen_minus_base, gen_violations, base_violations\n\nrows = []\nraise NotImplementedError('Complete TODO 1 evaluation loop')\n\nresults = pd.DataFrame(rows)\nresults.head()" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# TODO 2: implement summary metrics\n# - n_samples\n# - gen_obj_mean\n# - base_obj_mean\n# - improvement_rate\n# - gen_violation_ratio\n# - base_violation_ratio\n# - gen_diversity_l2 (use helper below)\n\ndef mean_pairwise_l2(designs: np.ndarray) -> float:\n flat = designs.reshape(designs.shape[0], -1)\n n = flat.shape[0]\n if n < 2:\n return 0.0\n dists = []\n for i in range(n):\n for j in range(i + 1, n):\n dists.append(float(np.linalg.norm(flat[i] - flat[j])))\n return float(np.mean(dists))\n\nraise NotImplementedError('Complete TODO 2 summary metrics')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "results.to_csv(ARTIFACT_DIR / 'per_sample_metrics.csv', index=False)\nsummary_df.to_csv(ARTIFACT_DIR / 'metrics_summary.csv', index=False)\n\nfig, ax = plt.subplots(figsize=(7, 4))\nax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\nax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\nax.set_xlabel('Compliance objective (lower is better)')\nax.set_ylabel('Count')\nax.set_title('Generated vs baseline objective distribution')\nax.legend()\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'objective_histogram.png', dpi=150)\nplt.show()\n\nprint('Saved:')\nprint('-', ARTIFACT_DIR / 'per_sample_metrics.csv')\nprint('-', ARTIFACT_DIR / 'metrics_summary.csv')\nprint('-', ARTIFACT_DIR / 'objective_histogram.png')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Visual side-by-side sample grid\nfig, axes = plt.subplots(3, 4, figsize=(12, 8))\nfor i, ax in enumerate(axes.ravel()):\n if i >= 12:\n break\n pair_idx = i // 2\n if i % 2 == 0:\n ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'gen {pair_idx}')\n else:\n ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'base {pair_idx}')\n ax.axis('off')\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'design_grid.png', dpi=150)\nplt.show()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Interpretation hints\n\n- `improvement_rate` shows how often generated designs beat baselines on objective value.\n- `gen_violation_ratio` tracks practical feasibility pressure from constraints.\n- `gen_diversity_l2` is a simple diversity proxy across generated designs.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 8c11a3c..09f1745 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -1,48 +1,70 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": "# Notebook 03 (Participant): Add a New Problem Scaffold\n\nComplete the `TODO` sections to build a minimal EngiBench-compatible toy problem.\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport numpy as np\nfrom gymnasium import spaces\n\nfrom engibench.constraint import bounded\nfrom engibench.core import ObjectiveDirection\nfrom engibench.core import OptiStep\nfrom engibench.core import Problem\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "class ToyDensityProblem(Problem[np.ndarray]):\n \"\"\"Minimal toy problem scaffold for workshop teaching.\"\"\"\n\n version = 0\n objectives = ((\"toy_cost\", ObjectiveDirection.MINIMIZE),)\n\n @dataclass\n class Conditions:\n target_density: Annotated[float, bounded(lower=0.0, upper=1.0)] = 0.5\n\n @dataclass\n class Config(Conditions):\n resolution: Annotated[int, bounded(lower=4, upper=128)] = 16\n max_iter: Annotated[int, bounded(lower=1, upper=200)] = 20\n\n dataset_id = \"IDEALLab/beams_2d_50_100_v0\" # placeholder to satisfy scaffold\n container_id = None\n\n def __init__(self, seed: int = 0, **kwargs):\n super().__init__(seed=seed)\n self.config = self.Config(**kwargs)\n self.conditions = self.Conditions(target_density=self.config.target_density)\n self.design_space = spaces.Box(\n low=0.0, high=1.0, shape=(self.config.resolution, self.config.resolution), dtype=np.float32\n )\n\n def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n # TODO 1: implement toy objective\n # Suggested terms:\n # - density mismatch to target_density\n # - smoothness penalty based on np.diff\n raise NotImplementedError('Implement simulate')\n\n def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n # TODO 2: implement simple iterative optimizer\n # - update design toward target density\n # - append OptiStep each iteration\n raise NotImplementedError('Implement optimize')\n\n def render(self, design: np.ndarray, *, open_window: bool = False):\n import matplotlib.pyplot as plt\n\n fig, ax = plt.subplots(figsize=(4, 4))\n ax.imshow(design, cmap=\"viridis\", vmin=0, vmax=1)\n ax.set_title(\"ToyDensityProblem design\")\n ax.axis(\"off\")\n if open_window:\n plt.show()\n return fig, ax\n\n def random_design(self):\n d = self.np_random.random(self.design_space.shape).astype(np.float32)\n return d, -1" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# Run this cell after finishing TODOs in ToyDensityProblem\nproblem = ToyDensityProblem(seed=42, resolution=16, target_density=0.4, max_iter=10)\nstart, _ = problem.random_design()\n\nprint('design space:', problem.design_space)\nprint('objectives:', problem.objectives)\nprint('conditions:', problem.conditions)\n\nviol = problem.check_constraints(start, config={'target_density': 0.4, 'resolution': 16, 'max_iter': 10})\nprint('constraint violations:', len(viol))\n\nobj0 = problem.simulate(start, config={'target_density': 0.4})\nopt_design, history = problem.optimize(start, config={'target_density': 0.4})\nobjf = problem.simulate(opt_design, config={'target_density': 0.4})\n\nprint('initial objective:', float(obj0[0]))\nprint('final objective:', float(objf[0]))\nprint('optimization steps:', len(history))\n\nproblem.render(opt_design)" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## Mapping to real EngiBench contributions\n\nThis toy scaffold demonstrates the interface shape only. For real contributions:\n\n1. Create `engibench/problems//v0.py`\n2. Implement real `simulate` and (optionally) `optimize`\n3. Provide `conditions`, `design_space`, and `dataset_id`\n4. Add docs and tests\n\nReference docs: [Adding a new problem](https://github.com/IDEALLab/EngiBench/blob/main/docs/tutorials/new_problem.md)\n" - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.10" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 03 (Participant): Add a New Problem Scaffold\n\nComplete the `TODO` sections to build a minimal EngiBench-compatible toy problem.\n" }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Colab/local dependency bootstrap\n", + "import subprocess\n", + "import sys\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", + "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium']\n", + "\n", + "if IN_COLAB or FORCE_INSTALL:\n", + " print('Installing dependencies...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " print('Dependency install complete.')\n", + "else:\n", + " print('Skipping install (using current environment).')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport numpy as np\nfrom gymnasium import spaces\n\nfrom engibench.constraint import bounded\nfrom engibench.core import ObjectiveDirection\nfrom engibench.core import OptiStep\nfrom engibench.core import Problem\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "class ToyDensityProblem(Problem[np.ndarray]):\n \"\"\"Minimal toy problem scaffold for workshop teaching.\"\"\"\n\n version = 0\n objectives = ((\"toy_cost\", ObjectiveDirection.MINIMIZE),)\n\n @dataclass\n class Conditions:\n target_density: Annotated[float, bounded(lower=0.0, upper=1.0)] = 0.5\n\n @dataclass\n class Config(Conditions):\n resolution: Annotated[int, bounded(lower=4, upper=128)] = 16\n max_iter: Annotated[int, bounded(lower=1, upper=200)] = 20\n\n dataset_id = \"IDEALLab/beams_2d_50_100_v0\" # placeholder to satisfy scaffold\n container_id = None\n\n def __init__(self, seed: int = 0, **kwargs):\n super().__init__(seed=seed)\n self.config = self.Config(**kwargs)\n self.conditions = self.Conditions(target_density=self.config.target_density)\n self.design_space = spaces.Box(\n low=0.0, high=1.0, shape=(self.config.resolution, self.config.resolution), dtype=np.float32\n )\n\n def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n # TODO 1: implement toy objective\n # Suggested terms:\n # - density mismatch to target_density\n # - smoothness penalty based on np.diff\n raise NotImplementedError('Implement simulate')\n\n def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n # TODO 2: implement simple iterative optimizer\n # - update design toward target density\n # - append OptiStep each iteration\n raise NotImplementedError('Implement optimize')\n\n def render(self, design: np.ndarray, *, open_window: bool = False):\n import matplotlib.pyplot as plt\n\n fig, ax = plt.subplots(figsize=(4, 4))\n ax.imshow(design, cmap=\"viridis\", vmin=0, vmax=1)\n ax.set_title(\"ToyDensityProblem design\")\n ax.axis(\"off\")\n if open_window:\n plt.show()\n return fig, ax\n\n def random_design(self):\n d = self.np_random.random(self.design_space.shape).astype(np.float32)\n return d, -1" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Run this cell after finishing TODOs in ToyDensityProblem\nproblem = ToyDensityProblem(seed=42, resolution=16, target_density=0.4, max_iter=10)\nstart, _ = problem.random_design()\n\nprint('design space:', problem.design_space)\nprint('objectives:', problem.objectives)\nprint('conditions:', problem.conditions)\n\nviol = problem.check_constraints(start, config={'target_density': 0.4, 'resolution': 16, 'max_iter': 10})\nprint('constraint violations:', len(viol))\n\nobj0 = problem.simulate(start, config={'target_density': 0.4})\nopt_design, history = problem.optimize(start, config={'target_density': 0.4})\nobjf = problem.simulate(opt_design, config={'target_density': 0.4})\n\nprint('initial objective:', float(obj0[0]))\nprint('final objective:', float(objf[0]))\nprint('optimization steps:', len(history))\n\nproblem.render(opt_design)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Mapping to real EngiBench contributions\n\nThis toy scaffold demonstrates the interface shape only. For real contributions:\n\n1. Create `engibench/problems//v0.py`\n2. Implement real `simulate` and (optionally) `optimize`\n3. Provide `conditions`, `design_space`, and `dataset_id`\n4. Add docs and tests\n\nReference docs: [Adding a new problem](https://github.com/IDEALLab/EngiBench/blob/main/docs/tutorials/new_problem.md)\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index 71f0e49..420bce2 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -1,74 +1,89 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": "# Notebook 00: Setup + API Warmup (DCC26)\n\nThis notebook covers the first workshop segment (10-15 minutes):\n\n1. Environment checks\n2. EngiBench problem instantiation (`Beams2D`)\n3. Design space / objectives / conditions inspection\n4. Dataset sample inspection\n5. Rendering and one explicit constraint check\n\nExpected outcome: participants can navigate the full EngiBench problem API without W&B setup.\n" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## Optional install cell (fresh Colab)\n\nUncomment if your runtime does not already contain required packages.\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# %pip install -q engibench[beams2d] engiopt matplotlib seaborn" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "import random\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nimport engibench\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nprint('engibench version:', engibench.__version__)\nprint('seed:', SEED)" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "problem = Beams2D(seed=SEED)\n\nprint('Problem class:', type(problem).__name__)\nprint('Design space:', problem.design_space)\nprint('Objectives:', problem.objectives)\nprint('Conditions instance:', problem.conditions)\nprint('Condition keys:', problem.conditions_keys)\nprint('Dataset ID:', problem.dataset_id)" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "dataset = problem.dataset\nprint(dataset)\n\nsample_idx = 0\ndesign = np.array(dataset['train']['optimal_design'][sample_idx])\nconfig = {k: dataset['train'][k][sample_idx] for k in problem.conditions_keys}\n\nprint('Sample design shape:', design.shape)\nprint('Sample config:', config)" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "fig, ax = problem.render(design)\nax.set_title('Sample Beams2D design from training split')\nplt.show()" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# One explicit constraint check with intentionally mismatched volume fraction\nbad_config = dict(config)\nbad_config['volfrac'] = 0.2\nviolations = problem.check_constraints(design=design, config=bad_config)\n\nprint('Violation count:', len(violations))\nif violations:\n print(violations)\nelse:\n print('No violations found')" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## Next\n\nContinue with **Notebook 01** to train a lightweight conditional generator and create designs for evaluation.\n" - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.10" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 00: Setup + API Warmup (DCC26)\n\nThis notebook covers the first workshop segment (10-15 minutes):\n\n1. Environment checks\n2. EngiBench problem instantiation (`Beams2D`)\n3. Design space / objectives / conditions inspection\n4. Dataset sample inspection\n5. Rendering and one explicit constraint check\n\nExpected outcome: participants can navigate the full EngiBench problem API without W&B setup.\n" }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Optional install cell (fresh Colab)\n\nUncomment if your runtime does not already contain required packages.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Colab/local dependency bootstrap\n", + "import subprocess\n", + "import sys\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", + "PACKAGES = ['engibench[beams2d]', 'engiopt', 'matplotlib', 'seaborn']\n", + "\n", + "if IN_COLAB or FORCE_INSTALL:\n", + " print('Installing dependencies...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " print('Dependency install complete.')\n", + "else:\n", + " print('Skipping install (using current environment).')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import random\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nimport engibench\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nprint('engibench version:', engibench.__version__)\nprint('seed:', SEED)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = Beams2D(seed=SEED)\n\nprint('Problem class:', type(problem).__name__)\nprint('Design space:', problem.design_space)\nprint('Objectives:', problem.objectives)\nprint('Conditions instance:', problem.conditions)\nprint('Condition keys:', problem.conditions_keys)\nprint('Dataset ID:', problem.dataset_id)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "dataset = problem.dataset\nprint(dataset)\n\nsample_idx = 0\ndesign = np.array(dataset['train']['optimal_design'][sample_idx])\nconfig = {k: dataset['train'][k][sample_idx] for k in problem.conditions_keys}\n\nprint('Sample design shape:', design.shape)\nprint('Sample config:', config)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "fig, ax = problem.render(design)\nax.set_title('Sample Beams2D design from training split')\nplt.show()" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# One explicit constraint check with intentionally mismatched volume fraction\nbad_config = dict(config)\nbad_config['volfrac'] = 0.2\nviolations = problem.check_constraints(design=design, config=bad_config)\n\nprint('Violation count:', len(violations))\nif violations:\n print(violations)\nelse:\n print('No violations found')" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Next\n\nContinue with **Notebook 01** to train a lightweight conditional generator and create designs for evaluation.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 7945f5a..26a204b 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -1,83 +1,98 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": "# Notebook 01: Train + Generate (DCC26)\n\nThis notebook covers the second workshop segment (30 minutes):\n\n1. Build a lightweight conditional generator\n2. Train on a small subset for deterministic runtime\n3. Generate designs from sampled conditions\n4. Save artifacts for Notebook 02\n\nFallback options are included if you skip training.\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# %pip install -q engibench[beams2d] torch torchvision matplotlib pandas" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "import json\nimport os\nimport random\nfrom pathlib import Path\n\nimport numpy as np\nimport torch as th\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, TensorDataset\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\nth.manual_seed(SEED)\nif th.cuda.is_available():\n th.cuda.manual_seed_all(SEED)\n\nDEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\nprint('device:', DEVICE)\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\nCKPT_PATH = ARTIFACT_DIR / 'mini_cond_generator.pt'\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "problem = Beams2D(seed=SEED)\ntrain_ds = problem.dataset['train']\ntest_ds = problem.dataset['test']\n\ncondition_keys = problem.conditions_keys\nprint('condition keys:', condition_keys)\n\n# Build compact train subset to keep runtime stable in workshop\nN_TRAIN = 512\nsubset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n\nconds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\ndesigns_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n\n# Downsample target to reduce model output size and speed up training\ndesigns_t = th.tensor(designs_np).unsqueeze(1)\nlowres_t = F.interpolate(designs_t, size=(25, 50), mode='bilinear', align_corners=False).squeeze(1)\ntargets_np = lowres_t.reshape(N_TRAIN, -1).numpy()\n\nprint('conditions shape:', conds_np.shape)\nprint('designs shape:', designs_np.shape)\nprint('lowres target shape:', targets_np.shape)" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "class MiniCondGenerator(nn.Module):\n def __init__(self, in_dim: int, out_dim: int):\n super().__init__()\n self.net = nn.Sequential(\n nn.Linear(in_dim, 64),\n nn.ReLU(),\n nn.Linear(64, 128),\n nn.ReLU(),\n nn.Linear(128, out_dim),\n nn.Sigmoid(),\n )\n\n def forward(self, x):\n return self.net(x)\n\n\ndef upsample_to_design(y_flat: th.Tensor) -> th.Tensor:\n low = y_flat.reshape(-1, 1, 25, 50)\n high = F.interpolate(low, size=(50, 100), mode='bilinear', align_corners=False)\n return high.squeeze(1)\n\n\nmodel = MiniCondGenerator(in_dim=conds_np.shape[1], out_dim=targets_np.shape[1]).to(DEVICE)\noptimizer = th.optim.Adam(model.parameters(), lr=1e-3)\ncriterion = nn.MSELoss()" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "TRAIN_FROM_SCRATCH = True\nEPOCHS = 8\nBATCH_SIZE = 64\n\nif TRAIN_FROM_SCRATCH:\n ds = TensorDataset(th.tensor(conds_np), th.tensor(targets_np))\n dl = DataLoader(ds, batch_size=BATCH_SIZE, shuffle=True)\n\n for epoch in range(EPOCHS):\n model.train()\n epoch_loss = 0.0\n for xb, yb in dl:\n xb = xb.to(DEVICE)\n yb = yb.to(DEVICE)\n pred = model(xb)\n loss = criterion(pred, yb)\n optimizer.zero_grad()\n loss.backward()\n optimizer.step()\n epoch_loss += float(loss.item())\n\n print(f'epoch {epoch + 1:02d}/{EPOCHS} - loss: {epoch_loss / len(dl):.4f}')\n\n th.save({'model': model.state_dict(), 'condition_keys': condition_keys}, CKPT_PATH)\n print('saved checkpoint to', CKPT_PATH)\nelif CKPT_PATH.exists():\n ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n model.load_state_dict(ckpt['model'])\n model.eval()\n print('loaded checkpoint from', CKPT_PATH)\nelse:\n print('No checkpoint found. Use fallback generation cell below.')" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# Optional fallback: nearest-neighbor by condition vector\nUSE_NEAREST_NEIGHBOR_FALLBACK = False\n\nrng = np.random.default_rng(SEED)\nN_SAMPLES = 24\nselected = rng.choice(len(test_ds), size=N_SAMPLES, replace=False)\n\ntest_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\nbaseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n\nif USE_NEAREST_NEIGHBOR_FALLBACK:\n generated = []\n for c in test_conds:\n dists = np.linalg.norm(conds_np - c[None, :], axis=1)\n idx = int(np.argmin(dists))\n generated.append(designs_np[idx])\n gen_designs = np.array(generated, dtype=np.float32)\nelse:\n model.eval()\n with th.no_grad():\n pred_low = model(th.tensor(test_conds, device=DEVICE))\n gen_designs_t = upsample_to_design(pred_low).clamp(0.0, 1.0)\n gen_designs = gen_designs_t.detach().cpu().numpy().astype(np.float32)\n\nprint('generated shape:', gen_designs.shape)\nprint('baseline shape:', baseline_designs.shape)" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "conditions_records = []\nfor i in range(N_SAMPLES):\n rec = {}\n for j, k in enumerate(condition_keys):\n v = test_conds[i, j]\n rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n conditions_records.append(rec)\n\nnp.save(ARTIFACT_DIR / 'generated_designs.npy', gen_designs)\nnp.save(ARTIFACT_DIR / 'baseline_designs.npy', baseline_designs)\nwith open(ARTIFACT_DIR / 'conditions.json', 'w', encoding='utf-8') as f:\n json.dump(conditions_records, f, indent=2)\n\nprint('Saved artifacts to', ARTIFACT_DIR)" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# Quick visual check of generated designs\nfig, axes = plt.subplots(2, 4, figsize=(12, 6))\nfor i, ax in enumerate(axes.ravel()):\n ax.imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n ax.axis('off')\n ax.set_title(f'gen {i}')\nplt.tight_layout()\nplt.show()" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## Next\n\nContinue with **Notebook 02** to validate and evaluate generated designs against baselines.\n" - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.10" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 01: Train + Generate (DCC26)\n\nThis notebook covers the second workshop segment (30 minutes):\n\n1. Build a lightweight conditional generator\n2. Train on a small subset for deterministic runtime\n3. Generate designs from sampled conditions\n4. Save artifacts for Notebook 02\n\nFallback options are included if you skip training.\n" }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Colab/local dependency bootstrap\n", + "import subprocess\n", + "import sys\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", + "PACKAGES = ['engibench[beams2d]', 'torch', 'torchvision', 'matplotlib', 'pandas']\n", + "\n", + "if IN_COLAB or FORCE_INSTALL:\n", + " print('Installing dependencies...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " print('Dependency install complete.')\n", + "else:\n", + " print('Skipping install (using current environment).')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import json\nimport os\nimport random\nfrom pathlib import Path\n\nimport numpy as np\nimport torch as th\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, TensorDataset\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\nth.manual_seed(SEED)\nif th.cuda.is_available():\n th.cuda.manual_seed_all(SEED)\n\nDEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\nprint('device:', DEVICE)\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\nCKPT_PATH = ARTIFACT_DIR / 'mini_cond_generator.pt'\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = Beams2D(seed=SEED)\ntrain_ds = problem.dataset['train']\ntest_ds = problem.dataset['test']\n\ncondition_keys = problem.conditions_keys\nprint('condition keys:', condition_keys)\n\n# Build compact train subset to keep runtime stable in workshop\nN_TRAIN = 512\nsubset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n\nconds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\ndesigns_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n\n# Downsample target to reduce model output size and speed up training\ndesigns_t = th.tensor(designs_np).unsqueeze(1)\nlowres_t = F.interpolate(designs_t, size=(25, 50), mode='bilinear', align_corners=False).squeeze(1)\ntargets_np = lowres_t.reshape(N_TRAIN, -1).numpy()\n\nprint('conditions shape:', conds_np.shape)\nprint('designs shape:', designs_np.shape)\nprint('lowres target shape:', targets_np.shape)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "class MiniCondGenerator(nn.Module):\n def __init__(self, in_dim: int, out_dim: int):\n super().__init__()\n self.net = nn.Sequential(\n nn.Linear(in_dim, 64),\n nn.ReLU(),\n nn.Linear(64, 128),\n nn.ReLU(),\n nn.Linear(128, out_dim),\n nn.Sigmoid(),\n )\n\n def forward(self, x):\n return self.net(x)\n\n\ndef upsample_to_design(y_flat: th.Tensor) -> th.Tensor:\n low = y_flat.reshape(-1, 1, 25, 50)\n high = F.interpolate(low, size=(50, 100), mode='bilinear', align_corners=False)\n return high.squeeze(1)\n\n\nmodel = MiniCondGenerator(in_dim=conds_np.shape[1], out_dim=targets_np.shape[1]).to(DEVICE)\noptimizer = th.optim.Adam(model.parameters(), lr=1e-3)\ncriterion = nn.MSELoss()" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "TRAIN_FROM_SCRATCH = True\nEPOCHS = 8\nBATCH_SIZE = 64\n\nif TRAIN_FROM_SCRATCH:\n ds = TensorDataset(th.tensor(conds_np), th.tensor(targets_np))\n dl = DataLoader(ds, batch_size=BATCH_SIZE, shuffle=True)\n\n for epoch in range(EPOCHS):\n model.train()\n epoch_loss = 0.0\n for xb, yb in dl:\n xb = xb.to(DEVICE)\n yb = yb.to(DEVICE)\n pred = model(xb)\n loss = criterion(pred, yb)\n optimizer.zero_grad()\n loss.backward()\n optimizer.step()\n epoch_loss += float(loss.item())\n\n print(f'epoch {epoch + 1:02d}/{EPOCHS} - loss: {epoch_loss / len(dl):.4f}')\n\n th.save({'model': model.state_dict(), 'condition_keys': condition_keys}, CKPT_PATH)\n print('saved checkpoint to', CKPT_PATH)\nelif CKPT_PATH.exists():\n ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n model.load_state_dict(ckpt['model'])\n model.eval()\n print('loaded checkpoint from', CKPT_PATH)\nelse:\n print('No checkpoint found. Use fallback generation cell below.')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Optional fallback: nearest-neighbor by condition vector\nUSE_NEAREST_NEIGHBOR_FALLBACK = False\n\nrng = np.random.default_rng(SEED)\nN_SAMPLES = 24\nselected = rng.choice(len(test_ds), size=N_SAMPLES, replace=False)\n\ntest_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\nbaseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n\nif USE_NEAREST_NEIGHBOR_FALLBACK:\n generated = []\n for c in test_conds:\n dists = np.linalg.norm(conds_np - c[None, :], axis=1)\n idx = int(np.argmin(dists))\n generated.append(designs_np[idx])\n gen_designs = np.array(generated, dtype=np.float32)\nelse:\n model.eval()\n with th.no_grad():\n pred_low = model(th.tensor(test_conds, device=DEVICE))\n gen_designs_t = upsample_to_design(pred_low).clamp(0.0, 1.0)\n gen_designs = gen_designs_t.detach().cpu().numpy().astype(np.float32)\n\nprint('generated shape:', gen_designs.shape)\nprint('baseline shape:', baseline_designs.shape)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "conditions_records = []\nfor i in range(N_SAMPLES):\n rec = {}\n for j, k in enumerate(condition_keys):\n v = test_conds[i, j]\n rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n conditions_records.append(rec)\n\nnp.save(ARTIFACT_DIR / 'generated_designs.npy', gen_designs)\nnp.save(ARTIFACT_DIR / 'baseline_designs.npy', baseline_designs)\nwith open(ARTIFACT_DIR / 'conditions.json', 'w', encoding='utf-8') as f:\n json.dump(conditions_records, f, indent=2)\n\nprint('Saved artifacts to', ARTIFACT_DIR)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Quick visual check of generated designs\nfig, axes = plt.subplots(2, 4, figsize=(12, 6))\nfor i, ax in enumerate(axes.ravel()):\n ax.imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n ax.axis('off')\n ax.set_title(f'gen {i}')\nplt.tight_layout()\nplt.show()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Next\n\nContinue with **Notebook 02** to validate and evaluate generated designs against baselines.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 0b3165f..1d5c565 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -1,69 +1,84 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": "# Notebook 02: Evaluation + Metrics (DCC26)\n\nThis notebook covers the evaluation segment (20 minutes):\n\n1. Constraint checks for generated designs\n2. Physics-based simulation via `problem.simulate`\n3. Baseline comparison\n4. Export metrics and plots\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# %pip install -q engibench[beams2d] pandas matplotlib" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "import json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nassert ARTIFACT_DIR.exists(), 'Run Notebook 01 first (artifacts folder missing)'\n\ngen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\nbaseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\nwith open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n conditions = json.load(f)\n\nprint('generated:', gen_designs.shape)\nprint('baseline:', baseline_designs.shape)\nprint('conditions:', len(conditions))" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "problem = Beams2D(seed=7)\n\nrows = []\nfor i, (g, b, cfg) in enumerate(zip(gen_designs, baseline_designs, conditions, strict=True)):\n g_viol = problem.check_constraints(design=g, config=cfg)\n b_viol = problem.check_constraints(design=b, config=cfg)\n\n # Reset before simulator calls for reproducibility\n problem.reset(seed=7)\n g_obj = float(problem.simulate(g, config=cfg)[0])\n problem.reset(seed=7)\n b_obj = float(problem.simulate(b, config=cfg)[0])\n\n rows.append({\n 'sample': i,\n 'gen_obj': g_obj,\n 'base_obj': b_obj,\n 'gen_minus_base': g_obj - b_obj,\n 'gen_violations': len(g_viol),\n 'base_violations': len(b_viol),\n })\n\nresults = pd.DataFrame(rows)\nresults.head()" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "def mean_pairwise_l2(designs: np.ndarray) -> float:\n flat = designs.reshape(designs.shape[0], -1)\n n = flat.shape[0]\n if n < 2:\n return 0.0\n dists = []\n for i in range(n):\n for j in range(i + 1, n):\n dists.append(float(np.linalg.norm(flat[i] - flat[j])))\n return float(np.mean(dists))\n\nsummary = {\n 'n_samples': int(len(results)),\n 'gen_obj_mean': float(results['gen_obj'].mean()),\n 'base_obj_mean': float(results['base_obj'].mean()),\n 'improvement_rate': float((results['gen_obj'] < results['base_obj']).mean()),\n 'gen_violation_ratio': float((results['gen_violations'] > 0).mean()),\n 'base_violation_ratio': float((results['base_violations'] > 0).mean()),\n 'gen_diversity_l2': mean_pairwise_l2(gen_designs),\n}\n\nsummary_df = pd.DataFrame([summary])\nsummary_df" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "results.to_csv(ARTIFACT_DIR / 'per_sample_metrics.csv', index=False)\nsummary_df.to_csv(ARTIFACT_DIR / 'metrics_summary.csv', index=False)\n\nfig, ax = plt.subplots(figsize=(7, 4))\nax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\nax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\nax.set_xlabel('Compliance objective (lower is better)')\nax.set_ylabel('Count')\nax.set_title('Generated vs baseline objective distribution')\nax.legend()\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'objective_histogram.png', dpi=150)\nplt.show()\n\nprint('Saved:')\nprint('-', ARTIFACT_DIR / 'per_sample_metrics.csv')\nprint('-', ARTIFACT_DIR / 'metrics_summary.csv')\nprint('-', ARTIFACT_DIR / 'objective_histogram.png')" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# Visual side-by-side sample grid\nfig, axes = plt.subplots(3, 4, figsize=(12, 8))\nfor i, ax in enumerate(axes.ravel()):\n if i >= 12:\n break\n pair_idx = i // 2\n if i % 2 == 0:\n ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'gen {pair_idx}')\n else:\n ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'base {pair_idx}')\n ax.axis('off')\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'design_grid.png', dpi=150)\nplt.show()" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## Interpretation hints\n\n- `improvement_rate` shows how often generated designs beat baselines on objective value.\n- `gen_violation_ratio` tracks practical feasibility pressure from constraints.\n- `gen_diversity_l2` is a simple diversity proxy across generated designs.\n" - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.10" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 02: Evaluation + Metrics (DCC26)\n\nThis notebook covers the evaluation segment (20 minutes):\n\n1. Constraint checks for generated designs\n2. Physics-based simulation via `problem.simulate`\n3. Baseline comparison\n4. Export metrics and plots\n" }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Colab/local dependency bootstrap\n", + "import subprocess\n", + "import sys\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", + "PACKAGES = ['engibench[beams2d]', 'pandas', 'matplotlib']\n", + "\n", + "if IN_COLAB or FORCE_INSTALL:\n", + " print('Installing dependencies...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " print('Dependency install complete.')\n", + "else:\n", + " print('Skipping install (using current environment).')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "import json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nassert ARTIFACT_DIR.exists(), 'Run Notebook 01 first (artifacts folder missing)'\n\ngen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\nbaseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\nwith open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n conditions = json.load(f)\n\nprint('generated:', gen_designs.shape)\nprint('baseline:', baseline_designs.shape)\nprint('conditions:', len(conditions))" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = Beams2D(seed=7)\n\nrows = []\nfor i, (g, b, cfg) in enumerate(zip(gen_designs, baseline_designs, conditions, strict=True)):\n g_viol = problem.check_constraints(design=g, config=cfg)\n b_viol = problem.check_constraints(design=b, config=cfg)\n\n # Reset before simulator calls for reproducibility\n problem.reset(seed=7)\n g_obj = float(problem.simulate(g, config=cfg)[0])\n problem.reset(seed=7)\n b_obj = float(problem.simulate(b, config=cfg)[0])\n\n rows.append({\n 'sample': i,\n 'gen_obj': g_obj,\n 'base_obj': b_obj,\n 'gen_minus_base': g_obj - b_obj,\n 'gen_violations': len(g_viol),\n 'base_violations': len(b_viol),\n })\n\nresults = pd.DataFrame(rows)\nresults.head()" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "def mean_pairwise_l2(designs: np.ndarray) -> float:\n flat = designs.reshape(designs.shape[0], -1)\n n = flat.shape[0]\n if n < 2:\n return 0.0\n dists = []\n for i in range(n):\n for j in range(i + 1, n):\n dists.append(float(np.linalg.norm(flat[i] - flat[j])))\n return float(np.mean(dists))\n\nsummary = {\n 'n_samples': int(len(results)),\n 'gen_obj_mean': float(results['gen_obj'].mean()),\n 'base_obj_mean': float(results['base_obj'].mean()),\n 'improvement_rate': float((results['gen_obj'] < results['base_obj']).mean()),\n 'gen_violation_ratio': float((results['gen_violations'] > 0).mean()),\n 'base_violation_ratio': float((results['base_violations'] > 0).mean()),\n 'gen_diversity_l2': mean_pairwise_l2(gen_designs),\n}\n\nsummary_df = pd.DataFrame([summary])\nsummary_df" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "results.to_csv(ARTIFACT_DIR / 'per_sample_metrics.csv', index=False)\nsummary_df.to_csv(ARTIFACT_DIR / 'metrics_summary.csv', index=False)\n\nfig, ax = plt.subplots(figsize=(7, 4))\nax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\nax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\nax.set_xlabel('Compliance objective (lower is better)')\nax.set_ylabel('Count')\nax.set_title('Generated vs baseline objective distribution')\nax.legend()\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'objective_histogram.png', dpi=150)\nplt.show()\n\nprint('Saved:')\nprint('-', ARTIFACT_DIR / 'per_sample_metrics.csv')\nprint('-', ARTIFACT_DIR / 'metrics_summary.csv')\nprint('-', ARTIFACT_DIR / 'objective_histogram.png')" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Visual side-by-side sample grid\nfig, axes = plt.subplots(3, 4, figsize=(12, 8))\nfor i, ax in enumerate(axes.ravel()):\n if i >= 12:\n break\n pair_idx = i // 2\n if i % 2 == 0:\n ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'gen {pair_idx}')\n else:\n ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'base {pair_idx}')\n ax.axis('off')\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'design_grid.png', dpi=150)\nplt.show()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Interpretation hints\n\n- `improvement_rate` shows how often generated designs beat baselines on objective value.\n- `gen_violation_ratio` tracks practical feasibility pressure from constraints.\n- `gen_diversity_l2` is a simple diversity proxy across generated designs.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 77909c4..a61142a 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -1,48 +1,70 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": "# Notebook 03: Add a New Problem Scaffold (DCC26)\n\nThis notebook is a contribution-oriented walkthrough showing a **minimal EngiBench-compatible problem**.\n\nIt uses a toy simulator so participants can understand the required interface before implementing real physics backends.\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport numpy as np\nfrom gymnasium import spaces\n\nfrom engibench.constraint import bounded\nfrom engibench.core import ObjectiveDirection\nfrom engibench.core import OptiStep\nfrom engibench.core import Problem\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "class ToyDensityProblem(Problem[np.ndarray]):\n \"\"\"Minimal toy problem scaffold for workshop teaching.\"\"\"\n\n version = 0\n objectives = ((\"toy_cost\", ObjectiveDirection.MINIMIZE),)\n\n @dataclass\n class Conditions:\n target_density: Annotated[float, bounded(lower=0.0, upper=1.0)] = 0.5\n\n @dataclass\n class Config(Conditions):\n resolution: Annotated[int, bounded(lower=4, upper=128)] = 16\n max_iter: Annotated[int, bounded(lower=1, upper=200)] = 20\n\n dataset_id = \"IDEALLab/beams_2d_50_100_v0\" # placeholder to satisfy scaffold\n container_id = None\n\n def __init__(self, seed: int = 0, **kwargs):\n super().__init__(seed=seed)\n self.config = self.Config(**kwargs)\n self.conditions = self.Conditions(target_density=self.config.target_density)\n self.design_space = spaces.Box(\n low=0.0, high=1.0, shape=(self.config.resolution, self.config.resolution), dtype=np.float32\n )\n\n def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n cfg = {\"target_density\": self.config.target_density, **(config or {})}\n # Toy objective: mismatch to target density + smoothness penalty\n density_term = abs(float(design.mean()) - float(cfg[\"target_density\"]))\n smoothness = float(np.mean(np.abs(np.diff(design, axis=0))))\n return np.array([density_term + 0.1 * smoothness], dtype=np.float32)\n\n def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n cfg = {\"target_density\": self.config.target_density, **(config or {})}\n x = starting_point.copy().astype(np.float32)\n hist = []\n for step in range(self.config.max_iter):\n # Toy update toward target density (not a real optimizer)\n x = np.clip(x + 0.2 * (cfg[\"target_density\"] - x), 0.0, 1.0)\n hist.append(OptiStep(obj_values=self.simulate(x, cfg), step=step))\n return x, hist\n\n def render(self, design: np.ndarray, *, open_window: bool = False):\n import matplotlib.pyplot as plt\n\n fig, ax = plt.subplots(figsize=(4, 4))\n ax.imshow(design, cmap=\"viridis\", vmin=0, vmax=1)\n ax.set_title(\"ToyDensityProblem design\")\n ax.axis(\"off\")\n if open_window:\n plt.show()\n return fig, ax\n\n def random_design(self):\n d = self.np_random.random(self.design_space.shape).astype(np.float32)\n return d, -1\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "problem = ToyDensityProblem(seed=42, resolution=16, target_density=0.4, max_iter=10)\nstart, _ = problem.random_design()\n\nprint('design space:', problem.design_space)\nprint('objectives:', problem.objectives)\nprint('conditions:', problem.conditions)\n\nviol = problem.check_constraints(start, config={'target_density': 0.4, 'resolution': 16, 'max_iter': 10})\nprint('constraint violations:', len(viol))\n\nobj0 = problem.simulate(start, config={'target_density': 0.4})\nopt_design, history = problem.optimize(start, config={'target_density': 0.4})\nobjf = problem.simulate(opt_design, config={'target_density': 0.4})\n\nprint('initial objective:', float(obj0[0]))\nprint('final objective:', float(objf[0]))\nprint('optimization steps:', len(history))\n\nproblem.render(opt_design)" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## Mapping to real EngiBench contributions\n\nThis toy scaffold demonstrates the interface shape only. For real contributions:\n\n1. Create `engibench/problems//v0.py`\n2. Implement real `simulate` and (optionally) `optimize`\n3. Provide `conditions`, `design_space`, and `dataset_id`\n4. Add docs and tests\n\nReference docs: [Adding a new problem](https://github.com/IDEALLab/EngiBench/blob/main/docs/tutorials/new_problem.md)\n" - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.10" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook 03: Add a New Problem Scaffold (DCC26)\n\nThis notebook is a contribution-oriented walkthrough showing a **minimal EngiBench-compatible problem**.\n\nIt uses a toy simulator so participants can understand the required interface before implementing real physics backends.\n" }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Colab/local dependency bootstrap\n", + "import subprocess\n", + "import sys\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", + "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium']\n", + "\n", + "if IN_COLAB or FORCE_INSTALL:\n", + " print('Installing dependencies...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " print('Dependency install complete.')\n", + "else:\n", + " print('Skipping install (using current environment).')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport numpy as np\nfrom gymnasium import spaces\n\nfrom engibench.constraint import bounded\nfrom engibench.core import ObjectiveDirection\nfrom engibench.core import OptiStep\nfrom engibench.core import Problem\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "class ToyDensityProblem(Problem[np.ndarray]):\n \"\"\"Minimal toy problem scaffold for workshop teaching.\"\"\"\n\n version = 0\n objectives = ((\"toy_cost\", ObjectiveDirection.MINIMIZE),)\n\n @dataclass\n class Conditions:\n target_density: Annotated[float, bounded(lower=0.0, upper=1.0)] = 0.5\n\n @dataclass\n class Config(Conditions):\n resolution: Annotated[int, bounded(lower=4, upper=128)] = 16\n max_iter: Annotated[int, bounded(lower=1, upper=200)] = 20\n\n dataset_id = \"IDEALLab/beams_2d_50_100_v0\" # placeholder to satisfy scaffold\n container_id = None\n\n def __init__(self, seed: int = 0, **kwargs):\n super().__init__(seed=seed)\n self.config = self.Config(**kwargs)\n self.conditions = self.Conditions(target_density=self.config.target_density)\n self.design_space = spaces.Box(\n low=0.0, high=1.0, shape=(self.config.resolution, self.config.resolution), dtype=np.float32\n )\n\n def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n cfg = {\"target_density\": self.config.target_density, **(config or {})}\n # Toy objective: mismatch to target density + smoothness penalty\n density_term = abs(float(design.mean()) - float(cfg[\"target_density\"]))\n smoothness = float(np.mean(np.abs(np.diff(design, axis=0))))\n return np.array([density_term + 0.1 * smoothness], dtype=np.float32)\n\n def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n cfg = {\"target_density\": self.config.target_density, **(config or {})}\n x = starting_point.copy().astype(np.float32)\n hist = []\n for step in range(self.config.max_iter):\n # Toy update toward target density (not a real optimizer)\n x = np.clip(x + 0.2 * (cfg[\"target_density\"] - x), 0.0, 1.0)\n hist.append(OptiStep(obj_values=self.simulate(x, cfg), step=step))\n return x, hist\n\n def render(self, design: np.ndarray, *, open_window: bool = False):\n import matplotlib.pyplot as plt\n\n fig, ax = plt.subplots(figsize=(4, 4))\n ax.imshow(design, cmap=\"viridis\", vmin=0, vmax=1)\n ax.set_title(\"ToyDensityProblem design\")\n ax.axis(\"off\")\n if open_window:\n plt.show()\n return fig, ax\n\n def random_design(self):\n d = self.np_random.random(self.design_space.shape).astype(np.float32)\n return d, -1\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "problem = ToyDensityProblem(seed=42, resolution=16, target_density=0.4, max_iter=10)\nstart, _ = problem.random_design()\n\nprint('design space:', problem.design_space)\nprint('objectives:', problem.objectives)\nprint('conditions:', problem.conditions)\n\nviol = problem.check_constraints(start, config={'target_density': 0.4, 'resolution': 16, 'max_iter': 10})\nprint('constraint violations:', len(viol))\n\nobj0 = problem.simulate(start, config={'target_density': 0.4})\nopt_design, history = problem.optimize(start, config={'target_density': 0.4})\nobjf = problem.simulate(opt_design, config={'target_density': 0.4})\n\nprint('initial objective:', float(obj0[0]))\nprint('final objective:', float(objf[0]))\nprint('optimization steps:', len(history))\n\nproblem.render(opt_design)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Mapping to real EngiBench contributions\n\nThis toy scaffold demonstrates the interface shape only. For real contributions:\n\n1. Create `engibench/problems//v0.py`\n2. Implement real `simulate` and (optionally) `optimize`\n3. Provide `conditions`, `design_space`, and `dataset_id`\n4. Add docs and tests\n\nReference docs: [Adding a new problem](https://github.com/IDEALLab/EngiBench/blob/main/docs/tutorials/new_problem.md)\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } From b9cd9e0544890a6594be2541e467668802f209f1 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 10:03:34 +0100 Subject: [PATCH 06/37] Fix Colab bootstrap by removing non-PyPI engiopt install --- workshops/dcc26/README.md | 1 + workshops/dcc26/participant/00_setup_api_warmup.ipynb | 2 +- workshops/dcc26/solutions/00_setup_api_warmup.ipynb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index 890a04a..15d7e32 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -44,6 +44,7 @@ All notebooks now include a conditional dependency bootstrap cell: - On Colab: installs required packages automatically. - On local envs: skips install by default (`FORCE_INSTALL = False`). +- Note: `engiopt` is not installed from PyPI in these notebooks; workshop execution relies on `engibench` + standard ML libraries. ## Open in Colab diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index c59d9e4..dbda4ae 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -22,7 +22,7 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'engiopt', 'matplotlib', 'seaborn']\n", + "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index 420bce2..1a5aca1 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -22,7 +22,7 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'engiopt', 'matplotlib', 'seaborn']\n", + "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", From 27f91d065b626511c28cdafeee7e1ca3d28c9599 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 10:04:57 +0100 Subject: [PATCH 07/37] Show full pip logs in Colab bootstrap cells --- workshops/dcc26/participant/00_setup_api_warmup.ipynb | 2 +- workshops/dcc26/participant/01_train_generate.ipynb | 2 +- workshops/dcc26/participant/02_evaluate_metrics.ipynb | 2 +- workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb | 2 +- workshops/dcc26/solutions/00_setup_api_warmup.ipynb | 2 +- workshops/dcc26/solutions/01_train_generate.ipynb | 2 +- workshops/dcc26/solutions/02_evaluate_metrics.ipynb | 2 +- workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index dbda4ae..f975f94 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -26,7 +26,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 2ca330c..259b162 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -21,7 +21,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index ae901b7..d44818a 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -21,7 +21,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 09f1745..94a2544 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -21,7 +21,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index 1a5aca1..17c1681 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -26,7 +26,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 26a204b..3aa26c0 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -21,7 +21,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 1d5c565..ca7447e 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -21,7 +21,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index a61142a..99a0dac 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -21,7 +21,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', *PACKAGES])\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" From 1c029f8f7b639a724c9861c0d7a7098179f4e7ae Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 10:18:59 +0100 Subject: [PATCH 08/37] Use EngiOpt CGAN model in DCC26 Notebook 01 --- workshops/dcc26/README.md | 2 +- .../dcc26/participant/01_train_generate.ipynb | 128 +++++++++++++- .../dcc26/solutions/01_train_generate.ipynb | 167 +++++++++++++++++- 3 files changed, 280 insertions(+), 17 deletions(-) diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index 15d7e32..ce2c8c0 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -15,7 +15,7 @@ It is split into two tracks: - Rendering and constraint checks - `participant/01_train_generate.ipynb` and `solutions/01_train_generate.ipynb` (30 min) - - Lightweight conditional generator training + - Lightweight training using `engiopt.cgan_2d.Generator` - Deterministic seeds - Fallback path with nearest-neighbor generation diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 259b162..fb34fee 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -3,7 +3,9 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Notebook 01 (Participant): Train + Generate\n\nComplete the `TODO` sections to train a lightweight conditional model and generate designs.\n\nFallback: set `USE_NEAREST_NEIGHBOR_FALLBACK = True` if training is too slow.\n" + "source": [ + "# Notebook 01 (Participant): Train + Generate with EngiOpt CGAN-2D\n" + ] }, { "cell_type": "code", @@ -14,17 +16,28 @@ "# Colab/local dependency bootstrap\n", "import subprocess\n", "import sys\n", + "from pathlib import Path\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'torch', 'torchvision', 'matplotlib', 'pandas']\n", + "PACKAGES = ['engibench[beams2d]', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n" + " print('Skipping install (using current environment).')\n", + "\n", + "# EngiOpt model source (for Colab users without local editable install)\n", + "if IN_COLAB:\n", + " repo_dir = Path('/content/EngiOpt')\n", + " if not repo_dir.exists():\n", + " print('Cloning EngiOpt source...')\n", + " subprocess.check_call(['git', 'clone', '--depth', '1', 'https://github.com/IDEALLab/EngiOpt.git', str(repo_dir)])\n", + " if str(repo_dir) not in sys.path:\n", + " sys.path.insert(0, str(repo_dir))\n", + " print('EngiOpt source path:', repo_dir)\n" ] }, { @@ -32,35 +45,134 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "import json\nimport os\nimport random\nfrom pathlib import Path\n\nimport numpy as np\nimport torch as th\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, TensorDataset\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\nth.manual_seed(SEED)\nif th.cuda.is_available():\n th.cuda.manual_seed_all(SEED)\n\nDEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\nprint('device:', DEVICE)\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\nCKPT_PATH = ARTIFACT_DIR / 'mini_cond_generator.pt'\n" + "source": [ + "import json\n", + "import random\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch as th\n", + "import torch.nn as nn\n", + "from torch.utils.data import DataLoader, TensorDataset\n", + "\n", + "from engibench.problems.beams2d.v0 import Beams2D\n", + "\n", + "try:\n", + " from engiopt.cgan_2d.cgan_2d import Generator as EngiOptCGAN2DGenerator\n", + "except ModuleNotFoundError as exc:\n", + " raise ModuleNotFoundError(\n", + " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", + " ) from exc\n", + "\n", + "SEED = 7\n", + "random.seed(SEED)\n", + "np.random.seed(SEED)\n", + "th.manual_seed(SEED)\n", + "if th.cuda.is_available():\n", + " th.cuda.manual_seed_all(SEED)\n", + "\n", + "DEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", + "print('device:', DEVICE)\n", + "\n", + "ARTIFACT_DIR = Path('workshops/dcc26/artifacts')\n", + "ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", + "CKPT_PATH = ARTIFACT_DIR / 'engiopt_cgan2d_generator_supervised.pt'\n", + "LATENT_DIM = 32\n" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "problem = Beams2D(seed=SEED)\ntrain_ds = problem.dataset['train']\ntest_ds = problem.dataset['test']\n\ncondition_keys = problem.conditions_keys\nprint('condition keys:', condition_keys)\n\n# Build compact train subset to keep runtime stable in workshop\nN_TRAIN = 512\nsubset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n\nconds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\ndesigns_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n\n# Downsample target to reduce model output size and speed up training\ndesigns_t = th.tensor(designs_np).unsqueeze(1)\nlowres_t = F.interpolate(designs_t, size=(25, 50), mode='bilinear', align_corners=False).squeeze(1)\ntargets_np = lowres_t.reshape(N_TRAIN, -1).numpy()\n\nprint('conditions shape:', conds_np.shape)\nprint('designs shape:', designs_np.shape)\nprint('lowres target shape:', targets_np.shape)" + "source": [ + "problem = Beams2D(seed=SEED)\n", + "train_ds = problem.dataset['train']\n", + "test_ds = problem.dataset['test']\n", + "\n", + "condition_keys = problem.conditions_keys\n", + "print('condition keys:', condition_keys)\n", + "\n", + "# Build compact train subset to keep runtime stable in workshop\n", + "N_TRAIN = 512\n", + "subset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n", + "\n", + "conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", + "designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", + "\n", + "# EngiOpt CGAN generator emits tanh-scaled outputs in [-1, 1]\n", + "targets_np = (designs_np * 2.0) - 1.0\n", + "\n", + "print('conditions shape:', conds_np.shape)\n", + "print('designs shape:', designs_np.shape)\n", + "print('target range:', float(targets_np.min()), 'to', float(targets_np.max()))\n" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "# TODO 1: implement MiniCondGenerator\n# Suggested architecture:\n# Linear(in_dim, 64) -> ReLU -> Linear(64, 128) -> ReLU -> Linear(128, out_dim) -> Sigmoid\n\nclass MiniCondGenerator(nn.Module):\n def __init__(self, in_dim: int, out_dim: int):\n super().__init__()\n # TODO: define self.net\n raise NotImplementedError('Define model layers')\n\n def forward(self, x):\n # TODO: return forward pass\n raise NotImplementedError('Implement forward pass')\n\n\ndef upsample_to_design(y_flat: th.Tensor) -> th.Tensor:\n low = y_flat.reshape(-1, 1, 25, 50)\n high = F.interpolate(low, size=(50, 100), mode='bilinear', align_corners=False)\n return high.squeeze(1)\n\n# TODO 2: instantiate model/optimizer/loss\n# model = ...\n# optimizer = ...\n# criterion = ...\n\nraise NotImplementedError('Complete TODO 1/2 in this cell')" + "source": [ + "# TODO 1: instantiate EngiOpt CGAN-2D generator + optimizer/loss\n", + "# Required class is already imported as EngiOptCGAN2DGenerator\n", + "#\n", + "# Suggested:\n", + "# model = EngiOptCGAN2DGenerator(latent_dim=LATENT_DIM, n_conds=conds_np.shape[1], design_shape=problem.design_space.shape).to(DEVICE)\n", + "# optimizer = th.optim.Adam(model.parameters(), lr=1e-3)\n", + "# criterion = nn.MSELoss()\n", + "#\n", + "# def sample_noise(batch_size: int) -> th.Tensor:\n", + "# return th.randn((batch_size, LATENT_DIM), device=DEVICE, dtype=th.float32)\n", + "\n", + "raise NotImplementedError('Complete TODO 1 with the EngiOpt model setup')\n" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "TRAIN_FROM_SCRATCH = True\nEPOCHS = 8\nBATCH_SIZE = 64\n\nif TRAIN_FROM_SCRATCH:\n # TODO 3: implement training loop over DataLoader\n # - forward\n # - MSE loss\n # - backward/update\n # - print epoch loss\n # - save checkpoint to CKPT_PATH\n raise NotImplementedError('Complete TODO 3 training loop')\nelif CKPT_PATH.exists():\n ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n model.load_state_dict(ckpt['model'])\n model.eval()\n print('loaded checkpoint from', CKPT_PATH)\nelse:\n print('No checkpoint found. Use fallback generation cell below.')" + "source": [ + "TRAIN_FROM_SCRATCH = True\n", + "EPOCHS = 8\n", + "BATCH_SIZE = 64\n", + "\n", + "if TRAIN_FROM_SCRATCH:\n", + " # TODO 2: implement lightweight supervised training loop for EngiOpt generator\n", + " # - dataset tensors: conds_np -> input conditions, targets_np -> tanh-scaled targets in [-1,1]\n", + " # - sample latent noise each batch with sample_noise(...)\n", + " # - loss = criterion(pred, target_batch)\n", + " # - optimizer step\n", + " # - save checkpoint dict with keys: model, condition_keys, latent_dim, model_family\n", + " raise NotImplementedError('Complete TODO 2 training loop')\n", + "elif CKPT_PATH.exists():\n", + " ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n", + " model.load_state_dict(ckpt['model'])\n", + " model.eval()\n", + " print('loaded checkpoint from', CKPT_PATH)\n", + "else:\n", + " print('No checkpoint found. Use fallback generation cell below.')\n" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "# TODO 4: implement generation path\n# - sample N_SAMPLES test conditions\n# - if USE_NEAREST_NEIGHBOR_FALLBACK: nearest-neighbor designs from train subset\n# - else: model forward + upsample\n# - set `gen_designs`, `baseline_designs`, `test_conds`\n\nUSE_NEAREST_NEIGHBOR_FALLBACK = False\n\nraise NotImplementedError('Complete TODO 4 generation path')" + "source": [ + "# TODO 3: implement generation path with EngiOpt generator\n", + "# - sample N_SAMPLES test conditions and baseline designs\n", + "# - if USE_NEAREST_NEIGHBOR_FALLBACK: nearest-neighbor designs from training subset\n", + "# - else: run model(sample_noise(...), condition_tensor)\n", + "# and map tanh outputs to design space with ((x + 1)/2).clamp(0, 1)\n", + "# - set gen_designs, baseline_designs, test_conds\n", + "\n", + "USE_NEAREST_NEIGHBOR_FALLBACK = False\n", + "\n", + "raise NotImplementedError('Complete TODO 3 generation path')\n" + ] }, { "cell_type": "code", diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 3aa26c0..601b66c 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -3,7 +3,9 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Notebook 01: Train + Generate (DCC26)\n\nThis notebook covers the second workshop segment (30 minutes):\n\n1. Build a lightweight conditional generator\n2. Train on a small subset for deterministic runtime\n3. Generate designs from sampled conditions\n4. Save artifacts for Notebook 02\n\nFallback options are included if you skip training.\n" + "source": [ + "# Notebook 01: Train + Generate with EngiOpt CGAN-2D (DCC26)\n" + ] }, { "cell_type": "code", @@ -14,17 +16,28 @@ "# Colab/local dependency bootstrap\n", "import subprocess\n", "import sys\n", + "from pathlib import Path\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'torch', 'torchvision', 'matplotlib', 'pandas']\n", + "PACKAGES = ['engibench[beams2d]', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n" + " print('Skipping install (using current environment).')\n", + "\n", + "# EngiOpt model source (for Colab users without local editable install)\n", + "if IN_COLAB:\n", + " repo_dir = Path('/content/EngiOpt')\n", + " if not repo_dir.exists():\n", + " print('Cloning EngiOpt source...')\n", + " subprocess.check_call(['git', 'clone', '--depth', '1', 'https://github.com/IDEALLab/EngiOpt.git', str(repo_dir)])\n", + " if str(repo_dir) not in sys.path:\n", + " sys.path.insert(0, str(repo_dir))\n", + " print('EngiOpt source path:', repo_dir)\n" ] }, { @@ -32,35 +45,173 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "import json\nimport os\nimport random\nfrom pathlib import Path\n\nimport numpy as np\nimport torch as th\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, TensorDataset\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\nth.manual_seed(SEED)\nif th.cuda.is_available():\n th.cuda.manual_seed_all(SEED)\n\nDEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\nprint('device:', DEVICE)\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\nCKPT_PATH = ARTIFACT_DIR / 'mini_cond_generator.pt'\n" + "source": [ + "import json\n", + "import random\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch as th\n", + "import torch.nn as nn\n", + "from torch.utils.data import DataLoader, TensorDataset\n", + "\n", + "from engibench.problems.beams2d.v0 import Beams2D\n", + "\n", + "try:\n", + " from engiopt.cgan_2d.cgan_2d import Generator as EngiOptCGAN2DGenerator\n", + "except ModuleNotFoundError as exc:\n", + " raise ModuleNotFoundError(\n", + " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", + " ) from exc\n", + "\n", + "SEED = 7\n", + "random.seed(SEED)\n", + "np.random.seed(SEED)\n", + "th.manual_seed(SEED)\n", + "if th.cuda.is_available():\n", + " th.cuda.manual_seed_all(SEED)\n", + "\n", + "DEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", + "print('device:', DEVICE)\n", + "\n", + "ARTIFACT_DIR = Path('workshops/dcc26/artifacts')\n", + "ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", + "CKPT_PATH = ARTIFACT_DIR / 'engiopt_cgan2d_generator_supervised.pt'\n", + "LATENT_DIM = 32\n" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "problem = Beams2D(seed=SEED)\ntrain_ds = problem.dataset['train']\ntest_ds = problem.dataset['test']\n\ncondition_keys = problem.conditions_keys\nprint('condition keys:', condition_keys)\n\n# Build compact train subset to keep runtime stable in workshop\nN_TRAIN = 512\nsubset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n\nconds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\ndesigns_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n\n# Downsample target to reduce model output size and speed up training\ndesigns_t = th.tensor(designs_np).unsqueeze(1)\nlowres_t = F.interpolate(designs_t, size=(25, 50), mode='bilinear', align_corners=False).squeeze(1)\ntargets_np = lowres_t.reshape(N_TRAIN, -1).numpy()\n\nprint('conditions shape:', conds_np.shape)\nprint('designs shape:', designs_np.shape)\nprint('lowres target shape:', targets_np.shape)" + "source": [ + "problem = Beams2D(seed=SEED)\n", + "train_ds = problem.dataset['train']\n", + "test_ds = problem.dataset['test']\n", + "\n", + "condition_keys = problem.conditions_keys\n", + "print('condition keys:', condition_keys)\n", + "\n", + "# Build compact train subset to keep runtime stable in workshop\n", + "N_TRAIN = 512\n", + "subset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n", + "\n", + "conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", + "designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", + "\n", + "# EngiOpt CGAN generator emits tanh-scaled outputs in [-1, 1]\n", + "targets_np = (designs_np * 2.0) - 1.0\n", + "\n", + "print('conditions shape:', conds_np.shape)\n", + "print('designs shape:', designs_np.shape)\n", + "print('target range:', float(targets_np.min()), 'to', float(targets_np.max()))\n" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "class MiniCondGenerator(nn.Module):\n def __init__(self, in_dim: int, out_dim: int):\n super().__init__()\n self.net = nn.Sequential(\n nn.Linear(in_dim, 64),\n nn.ReLU(),\n nn.Linear(64, 128),\n nn.ReLU(),\n nn.Linear(128, out_dim),\n nn.Sigmoid(),\n )\n\n def forward(self, x):\n return self.net(x)\n\n\ndef upsample_to_design(y_flat: th.Tensor) -> th.Tensor:\n low = y_flat.reshape(-1, 1, 25, 50)\n high = F.interpolate(low, size=(50, 100), mode='bilinear', align_corners=False)\n return high.squeeze(1)\n\n\nmodel = MiniCondGenerator(in_dim=conds_np.shape[1], out_dim=targets_np.shape[1]).to(DEVICE)\noptimizer = th.optim.Adam(model.parameters(), lr=1e-3)\ncriterion = nn.MSELoss()" + "source": [ + "model = EngiOptCGAN2DGenerator(\n", + " latent_dim=LATENT_DIM,\n", + " n_conds=conds_np.shape[1],\n", + " design_shape=problem.design_space.shape,\n", + ").to(DEVICE)\n", + "\n", + "optimizer = th.optim.Adam(model.parameters(), lr=1e-3)\n", + "criterion = nn.MSELoss()\n", + "\n", + "\n", + "def sample_noise(batch_size: int) -> th.Tensor:\n", + " return th.randn((batch_size, LATENT_DIM), device=DEVICE, dtype=th.float32)\n" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "TRAIN_FROM_SCRATCH = True\nEPOCHS = 8\nBATCH_SIZE = 64\n\nif TRAIN_FROM_SCRATCH:\n ds = TensorDataset(th.tensor(conds_np), th.tensor(targets_np))\n dl = DataLoader(ds, batch_size=BATCH_SIZE, shuffle=True)\n\n for epoch in range(EPOCHS):\n model.train()\n epoch_loss = 0.0\n for xb, yb in dl:\n xb = xb.to(DEVICE)\n yb = yb.to(DEVICE)\n pred = model(xb)\n loss = criterion(pred, yb)\n optimizer.zero_grad()\n loss.backward()\n optimizer.step()\n epoch_loss += float(loss.item())\n\n print(f'epoch {epoch + 1:02d}/{EPOCHS} - loss: {epoch_loss / len(dl):.4f}')\n\n th.save({'model': model.state_dict(), 'condition_keys': condition_keys}, CKPT_PATH)\n print('saved checkpoint to', CKPT_PATH)\nelif CKPT_PATH.exists():\n ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n model.load_state_dict(ckpt['model'])\n model.eval()\n print('loaded checkpoint from', CKPT_PATH)\nelse:\n print('No checkpoint found. Use fallback generation cell below.')" + "source": [ + "TRAIN_FROM_SCRATCH = True\n", + "EPOCHS = 8\n", + "BATCH_SIZE = 64\n", + "\n", + "if TRAIN_FROM_SCRATCH:\n", + " ds = TensorDataset(th.tensor(conds_np), th.tensor(targets_np))\n", + " dl = DataLoader(ds, batch_size=BATCH_SIZE, shuffle=True)\n", + "\n", + " for epoch in range(EPOCHS):\n", + " model.train()\n", + " epoch_loss = 0.0\n", + " for cond_batch, target_batch in dl:\n", + " cond_batch = cond_batch.to(DEVICE)\n", + " target_batch = target_batch.to(DEVICE)\n", + "\n", + " pred = model(sample_noise(cond_batch.shape[0]), cond_batch)\n", + " loss = criterion(pred, target_batch)\n", + "\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + " epoch_loss += float(loss.item())\n", + "\n", + " print(f'epoch {epoch + 1:02d}/{EPOCHS} - loss: {epoch_loss / len(dl):.4f}')\n", + "\n", + " th.save(\n", + " {\n", + " 'model': model.state_dict(),\n", + " 'condition_keys': condition_keys,\n", + " 'latent_dim': LATENT_DIM,\n", + " 'model_family': 'engiopt.cgan_2d.Generator',\n", + " },\n", + " CKPT_PATH,\n", + " )\n", + " print('saved checkpoint to', CKPT_PATH)\n", + "elif CKPT_PATH.exists():\n", + " ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n", + " model.load_state_dict(ckpt['model'])\n", + " model.eval()\n", + " print('loaded checkpoint from', CKPT_PATH)\n", + "else:\n", + " print('No checkpoint found. Use fallback generation cell below.')\n" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Optional fallback: nearest-neighbor by condition vector\nUSE_NEAREST_NEIGHBOR_FALLBACK = False\n\nrng = np.random.default_rng(SEED)\nN_SAMPLES = 24\nselected = rng.choice(len(test_ds), size=N_SAMPLES, replace=False)\n\ntest_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\nbaseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n\nif USE_NEAREST_NEIGHBOR_FALLBACK:\n generated = []\n for c in test_conds:\n dists = np.linalg.norm(conds_np - c[None, :], axis=1)\n idx = int(np.argmin(dists))\n generated.append(designs_np[idx])\n gen_designs = np.array(generated, dtype=np.float32)\nelse:\n model.eval()\n with th.no_grad():\n pred_low = model(th.tensor(test_conds, device=DEVICE))\n gen_designs_t = upsample_to_design(pred_low).clamp(0.0, 1.0)\n gen_designs = gen_designs_t.detach().cpu().numpy().astype(np.float32)\n\nprint('generated shape:', gen_designs.shape)\nprint('baseline shape:', baseline_designs.shape)" + "source": [ + "# Optional fallback: nearest-neighbor by condition vector\n", + "USE_NEAREST_NEIGHBOR_FALLBACK = False\n", + "\n", + "rng = np.random.default_rng(SEED)\n", + "N_SAMPLES = 24\n", + "selected = rng.choice(len(test_ds), size=N_SAMPLES, replace=False)\n", + "\n", + "test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", + "baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", + "\n", + "if USE_NEAREST_NEIGHBOR_FALLBACK:\n", + " generated = []\n", + " for c in test_conds:\n", + " dists = np.linalg.norm(conds_np - c[None, :], axis=1)\n", + " idx = int(np.argmin(dists))\n", + " generated.append(designs_np[idx])\n", + " gen_designs = np.array(generated, dtype=np.float32)\n", + "else:\n", + " model.eval()\n", + " with th.no_grad():\n", + " tanh_out = model(sample_noise(N_SAMPLES), th.tensor(test_conds, device=DEVICE))\n", + " gen_designs_t = ((tanh_out.clamp(-1.0, 1.0) + 1.0) / 2.0).clamp(0.0, 1.0)\n", + " gen_designs = gen_designs_t.detach().cpu().numpy().astype(np.float32)\n", + "\n", + "print('generated shape:', gen_designs.shape)\n", + "print('baseline shape:', baseline_designs.shape)\n" + ] }, { "cell_type": "code", From 3429ad5fe5f42cc35127a9bd9913bb04578270c2 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 10:25:04 +0100 Subject: [PATCH 09/37] Fix Colab EngiOpt import via editable install and sqlitedict --- workshops/dcc26/participant/01_train_generate.ipynb | 9 +++++---- workshops/dcc26/solutions/01_train_generate.ipynb | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index fb34fee..956e576 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -20,7 +20,7 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", @@ -29,14 +29,15 @@ "else:\n", " print('Skipping install (using current environment).')\n", "\n", - "# EngiOpt model source (for Colab users without local editable install)\n", + "# EngiOpt source install for Colab (matches local editable-install workflow)\n", "if IN_COLAB:\n", " repo_dir = Path('/content/EngiOpt')\n", " if not repo_dir.exists():\n", " print('Cloning EngiOpt source...')\n", " subprocess.check_call(['git', 'clone', '--depth', '1', 'https://github.com/IDEALLab/EngiOpt.git', str(repo_dir)])\n", - " if str(repo_dir) not in sys.path:\n", - " sys.path.insert(0, str(repo_dir))\n", + " print('Installing EngiOpt editable package from source...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-e', str(repo_dir), '--no-deps'])\n", + " print('EngiOpt editable install complete.')\n", " print('EngiOpt source path:', repo_dir)\n" ] }, diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 601b66c..c790d73 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -20,7 +20,7 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", @@ -29,14 +29,15 @@ "else:\n", " print('Skipping install (using current environment).')\n", "\n", - "# EngiOpt model source (for Colab users without local editable install)\n", + "# EngiOpt source install for Colab (matches local editable-install workflow)\n", "if IN_COLAB:\n", " repo_dir = Path('/content/EngiOpt')\n", " if not repo_dir.exists():\n", " print('Cloning EngiOpt source...')\n", " subprocess.check_call(['git', 'clone', '--depth', '1', 'https://github.com/IDEALLab/EngiOpt.git', str(repo_dir)])\n", - " if str(repo_dir) not in sys.path:\n", - " sys.path.insert(0, str(repo_dir))\n", + " print('Installing EngiOpt editable package from source...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-e', str(repo_dir), '--no-deps'])\n", + " print('EngiOpt editable install complete.')\n", " print('EngiOpt source path:', repo_dir)\n" ] }, From ef66c046afcde7ed7bd162e12ada616aac409c20 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 10:33:30 +0100 Subject: [PATCH 10/37] Install EngiOpt from GitHub in Colab bootstrap --- .../dcc26/participant/01_train_generate.ipynb | 19 +++++-------------- .../dcc26/solutions/01_train_generate.ipynb | 19 +++++-------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 956e576..6192b3b 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -16,29 +16,20 @@ "# Colab/local dependency bootstrap\n", "import subprocess\n", "import sys\n", - "from pathlib import Path\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", "PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print('Installing base dependencies...')\n", " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", + " print('Installing EngiOpt from GitHub branch...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--no-deps', ENGIOPT_GIT])\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n", - "\n", - "# EngiOpt source install for Colab (matches local editable-install workflow)\n", - "if IN_COLAB:\n", - " repo_dir = Path('/content/EngiOpt')\n", - " if not repo_dir.exists():\n", - " print('Cloning EngiOpt source...')\n", - " subprocess.check_call(['git', 'clone', '--depth', '1', 'https://github.com/IDEALLab/EngiOpt.git', str(repo_dir)])\n", - " print('Installing EngiOpt editable package from source...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-e', str(repo_dir), '--no-deps'])\n", - " print('EngiOpt editable install complete.')\n", - " print('EngiOpt source path:', repo_dir)\n" + " print('Skipping install (using current environment).')\n" ] }, { diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index c790d73..aee9d22 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -16,29 +16,20 @@ "# Colab/local dependency bootstrap\n", "import subprocess\n", "import sys\n", - "from pathlib import Path\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", "PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print('Installing base dependencies...')\n", " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", + " print('Installing EngiOpt from GitHub branch...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--no-deps', ENGIOPT_GIT])\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n", - "\n", - "# EngiOpt source install for Colab (matches local editable-install workflow)\n", - "if IN_COLAB:\n", - " repo_dir = Path('/content/EngiOpt')\n", - " if not repo_dir.exists():\n", - " print('Cloning EngiOpt source...')\n", - " subprocess.check_call(['git', 'clone', '--depth', '1', 'https://github.com/IDEALLab/EngiOpt.git', str(repo_dir)])\n", - " print('Installing EngiOpt editable package from source...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-e', str(repo_dir), '--no-deps'])\n", - " print('EngiOpt editable install complete.')\n", - " print('EngiOpt source path:', repo_dir)\n" + " print('Skipping install (using current environment).')\n" ] }, { From 88b08015b404a47a4d97f6395fc6fe23e7e5f3c9 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 14:36:06 +0100 Subject: [PATCH 11/37] Install EngiOpt git package with dependencies in Notebook 01 --- workshops/dcc26/participant/01_train_generate.ipynb | 2 +- workshops/dcc26/solutions/01_train_generate.ipynb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 6192b3b..2b07555 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -26,7 +26,7 @@ " print('Installing base dependencies...')\n", " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Installing EngiOpt from GitHub branch...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--no-deps', ENGIOPT_GIT])\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', ENGIOPT_GIT])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index aee9d22..e63fc9e 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -26,7 +26,7 @@ " print('Installing base dependencies...')\n", " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", " print('Installing EngiOpt from GitHub branch...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--no-deps', ENGIOPT_GIT])\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', ENGIOPT_GIT])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" From 42e00bba532fe0b004a087062d16957a13908cd2 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 14:54:12 +0100 Subject: [PATCH 12/37] Persist DCC26 artifacts via Drive-friendly Colab paths --- workshops/dcc26/README.md | 3 +- .../dcc26/participant/01_train_generate.ipynb | 34 ++++++++++- .../participant/02_evaluate_metrics.ipynb | 58 ++++++++++++++++++- .../dcc26/solutions/01_train_generate.ipynb | 34 ++++++++++- .../dcc26/solutions/02_evaluate_metrics.ipynb | 58 ++++++++++++++++++- 5 files changed, 180 insertions(+), 7 deletions(-) diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index ce2c8c0..6125653 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -63,7 +63,8 @@ Pre-merge (current branch) links: By default, solution notebooks write generated artifacts to: -- `workshops/dcc26/artifacts/` +- Local/Jupyter: `workshops/dcc26/artifacts/` +- Google Colab: `/content/drive/MyDrive/dcc26_workshop/artifacts/` (auto-mounted when possible) These include: diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 2b07555..8d81884 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -40,6 +40,7 @@ "source": [ "import json\n", "import random\n", + "import sys\n", "from pathlib import Path\n", "\n", "import matplotlib.pyplot as plt\n", @@ -57,6 +58,34 @@ " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", " ) from exc\n", "\n", + "\n", + "def resolve_artifact_dir(create: bool = False) -> Path:\n", + " in_colab = 'google.colab' in sys.modules\n", + "\n", + " if in_colab:\n", + " try:\n", + " from google.colab import drive\n", + "\n", + " if not Path('/content/drive/MyDrive').exists():\n", + " print('Mounting Google Drive for persistent workshop artifacts...')\n", + " drive.mount('/content/drive', force_remount=False)\n", + " except Exception as exc:\n", + " print('Drive mount skipped/failed, falling back to runtime storage:', exc)\n", + "\n", + " drive_artifacts = Path('/content/drive/MyDrive/dcc26_workshop/artifacts')\n", + " runtime_artifacts = Path('/content/workshops/dcc26/artifacts')\n", + "\n", + " target = drive_artifacts if Path('/content/drive/MyDrive').exists() else runtime_artifacts\n", + " if create:\n", + " target.mkdir(parents=True, exist_ok=True)\n", + " return target\n", + "\n", + " local_artifacts = Path('workshops/dcc26/artifacts')\n", + " if create:\n", + " local_artifacts.mkdir(parents=True, exist_ok=True)\n", + " return local_artifacts\n", + "\n", + "\n", "SEED = 7\n", "random.seed(SEED)\n", "np.random.seed(SEED)\n", @@ -67,8 +96,9 @@ "DEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", "print('device:', DEVICE)\n", "\n", - "ARTIFACT_DIR = Path('workshops/dcc26/artifacts')\n", - "ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", + "ARTIFACT_DIR = resolve_artifact_dir(create=True)\n", + "print('artifact dir:', ARTIFACT_DIR)\n", + "\n", "CKPT_PATH = ARTIFACT_DIR / 'engiopt_cgan2d_generator_supervised.pt'\n", "LATENT_DIM = 32\n" ] diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index d44818a..54c63cd 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -32,7 +32,63 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "import json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nassert ARTIFACT_DIR.exists(), 'Run Notebook 01 first (artifacts folder missing)'\n\ngen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\nbaseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\nwith open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n conditions = json.load(f)\n\nprint('generated:', gen_designs.shape)\nprint('baseline:', baseline_designs.shape)\nprint('conditions:', len(conditions))" + "source": [ + "import json\n", + "import sys\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from engibench.problems.beams2d.v0 import Beams2D\n", + "\n", + "\n", + "def resolve_artifact_dir() -> Path:\n", + " in_colab = 'google.colab' in sys.modules\n", + "\n", + " candidates = []\n", + " if in_colab:\n", + " try:\n", + " from google.colab import drive\n", + "\n", + " if not Path('/content/drive/MyDrive').exists():\n", + " print('Mounting Google Drive to look for Notebook 01 artifacts...')\n", + " drive.mount('/content/drive', force_remount=False)\n", + " except Exception as exc:\n", + " print('Drive mount skipped/failed, trying runtime paths:', exc)\n", + "\n", + " candidates.extend([\n", + " Path('/content/drive/MyDrive/dcc26_workshop/artifacts'),\n", + " Path('/content/workshops/dcc26/artifacts'),\n", + " Path('workshops/dcc26/artifacts'),\n", + " ])\n", + " else:\n", + " candidates.append(Path('workshops/dcc26/artifacts'))\n", + "\n", + " for cand in candidates:\n", + " if cand.exists():\n", + " return cand\n", + "\n", + " expected = '\\n'.join(f'- {p}' for p in candidates)\n", + " raise FileNotFoundError(\n", + " 'Notebook 01 artifacts not found. Run Notebook 01 first (including its export cell), '\n", + " 'or keep artifacts in Google Drive. Checked:\\n' + expected\n", + " )\n", + "\n", + "\n", + "ARTIFACT_DIR = resolve_artifact_dir()\n", + "print('using artifact dir:', ARTIFACT_DIR)\n", + "\n", + "gen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\n", + "baseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\n", + "with open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n", + " conditions = json.load(f)\n", + "\n", + "print('generated:', gen_designs.shape)\n", + "print('baseline:', baseline_designs.shape)\n", + "print('conditions:', len(conditions))\n" + ] }, { "cell_type": "code", diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index e63fc9e..3c3c190 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -40,6 +40,7 @@ "source": [ "import json\n", "import random\n", + "import sys\n", "from pathlib import Path\n", "\n", "import matplotlib.pyplot as plt\n", @@ -57,6 +58,34 @@ " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", " ) from exc\n", "\n", + "\n", + "def resolve_artifact_dir(create: bool = False) -> Path:\n", + " in_colab = 'google.colab' in sys.modules\n", + "\n", + " if in_colab:\n", + " try:\n", + " from google.colab import drive\n", + "\n", + " if not Path('/content/drive/MyDrive').exists():\n", + " print('Mounting Google Drive for persistent workshop artifacts...')\n", + " drive.mount('/content/drive', force_remount=False)\n", + " except Exception as exc:\n", + " print('Drive mount skipped/failed, falling back to runtime storage:', exc)\n", + "\n", + " drive_artifacts = Path('/content/drive/MyDrive/dcc26_workshop/artifacts')\n", + " runtime_artifacts = Path('/content/workshops/dcc26/artifacts')\n", + "\n", + " target = drive_artifacts if Path('/content/drive/MyDrive').exists() else runtime_artifacts\n", + " if create:\n", + " target.mkdir(parents=True, exist_ok=True)\n", + " return target\n", + "\n", + " local_artifacts = Path('workshops/dcc26/artifacts')\n", + " if create:\n", + " local_artifacts.mkdir(parents=True, exist_ok=True)\n", + " return local_artifacts\n", + "\n", + "\n", "SEED = 7\n", "random.seed(SEED)\n", "np.random.seed(SEED)\n", @@ -67,8 +96,9 @@ "DEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", "print('device:', DEVICE)\n", "\n", - "ARTIFACT_DIR = Path('workshops/dcc26/artifacts')\n", - "ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", + "ARTIFACT_DIR = resolve_artifact_dir(create=True)\n", + "print('artifact dir:', ARTIFACT_DIR)\n", + "\n", "CKPT_PATH = ARTIFACT_DIR / 'engiopt_cgan2d_generator_supervised.pt'\n", "LATENT_DIM = 32\n" ] diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index ca7447e..146cd79 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -32,7 +32,63 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "import json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nARTIFACT_DIR = Path('workshops/dcc26/artifacts')\nassert ARTIFACT_DIR.exists(), 'Run Notebook 01 first (artifacts folder missing)'\n\ngen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\nbaseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\nwith open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n conditions = json.load(f)\n\nprint('generated:', gen_designs.shape)\nprint('baseline:', baseline_designs.shape)\nprint('conditions:', len(conditions))" + "source": [ + "import json\n", + "import sys\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from engibench.problems.beams2d.v0 import Beams2D\n", + "\n", + "\n", + "def resolve_artifact_dir() -> Path:\n", + " in_colab = 'google.colab' in sys.modules\n", + "\n", + " candidates = []\n", + " if in_colab:\n", + " try:\n", + " from google.colab import drive\n", + "\n", + " if not Path('/content/drive/MyDrive').exists():\n", + " print('Mounting Google Drive to look for Notebook 01 artifacts...')\n", + " drive.mount('/content/drive', force_remount=False)\n", + " except Exception as exc:\n", + " print('Drive mount skipped/failed, trying runtime paths:', exc)\n", + "\n", + " candidates.extend([\n", + " Path('/content/drive/MyDrive/dcc26_workshop/artifacts'),\n", + " Path('/content/workshops/dcc26/artifacts'),\n", + " Path('workshops/dcc26/artifacts'),\n", + " ])\n", + " else:\n", + " candidates.append(Path('workshops/dcc26/artifacts'))\n", + "\n", + " for cand in candidates:\n", + " if cand.exists():\n", + " return cand\n", + "\n", + " expected = '\\n'.join(f'- {p}' for p in candidates)\n", + " raise FileNotFoundError(\n", + " 'Notebook 01 artifacts not found. Run Notebook 01 first (including its export cell), '\n", + " 'or keep artifacts in Google Drive. Checked:\\n' + expected\n", + " )\n", + "\n", + "\n", + "ARTIFACT_DIR = resolve_artifact_dir()\n", + "print('using artifact dir:', ARTIFACT_DIR)\n", + "\n", + "gen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\n", + "baseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\n", + "with open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n", + " conditions = json.load(f)\n", + "\n", + "print('generated:', gen_designs.shape)\n", + "print('baseline:', baseline_designs.shape)\n", + "print('conditions:', len(conditions))\n" + ] }, { "cell_type": "code", From c0e243ab7026ba608c97ab7b6976df8bc1ceab7d Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 15:10:18 +0100 Subject: [PATCH 13/37] Use no-auth Colab artifact flow with optional W&B --- workshops/dcc26/README.md | 7 +- .../dcc26/participant/01_train_generate.ipynb | 45 ++++++------ .../participant/02_evaluate_metrics.ipynb | 72 +++++++++++-------- .../dcc26/solutions/01_train_generate.ipynb | 68 ++++++++++++------ .../dcc26/solutions/02_evaluate_metrics.ipynb | 72 +++++++++++-------- 5 files changed, 163 insertions(+), 101 deletions(-) diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index 6125653..eaa5c6a 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -64,7 +64,12 @@ Pre-merge (current branch) links: By default, solution notebooks write generated artifacts to: - Local/Jupyter: `workshops/dcc26/artifacts/` -- Google Colab: `/content/drive/MyDrive/dcc26_workshop/artifacts/` (auto-mounted when possible) +- Google Colab runtime: `/content/dcc26_artifacts/` (no auth required) + +Optional: + +- You can enable W&B artifact upload/download in Notebook 01/02 by setting `USE_WANDB_ARTIFACTS = True`. +- W&B is disabled by default so participants can run without account setup. These include: diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 8d81884..db0510d 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -61,30 +61,22 @@ "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", " in_colab = 'google.colab' in sys.modules\n", - "\n", " if in_colab:\n", - " try:\n", - " from google.colab import drive\n", - "\n", - " if not Path('/content/drive/MyDrive').exists():\n", - " print('Mounting Google Drive for persistent workshop artifacts...')\n", - " drive.mount('/content/drive', force_remount=False)\n", - " except Exception as exc:\n", - " print('Drive mount skipped/failed, falling back to runtime storage:', exc)\n", - "\n", - " drive_artifacts = Path('/content/drive/MyDrive/dcc26_workshop/artifacts')\n", - " runtime_artifacts = Path('/content/workshops/dcc26/artifacts')\n", + " path = Path('/content/dcc26_artifacts')\n", + " else:\n", + " path = Path('workshops/dcc26/artifacts')\n", "\n", - " target = drive_artifacts if Path('/content/drive/MyDrive').exists() else runtime_artifacts\n", - " if create:\n", - " target.mkdir(parents=True, exist_ok=True)\n", - " return target\n", - "\n", - " local_artifacts = Path('workshops/dcc26/artifacts')\n", " if create:\n", - " local_artifacts.mkdir(parents=True, exist_ok=True)\n", - " return local_artifacts\n", + " path.mkdir(parents=True, exist_ok=True)\n", + " return path\n", + "\n", "\n", + "# Optional W&B artifact flow (disabled by default)\n", + "USE_WANDB_ARTIFACTS = False\n", + "WANDB_PROJECT = 'dcc26-workshop'\n", + "WANDB_ENTITY = None\n", + "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", + "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", "SEED = 7\n", "random.seed(SEED)\n", @@ -201,7 +193,18 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# TODO 5: serialize artifacts for Notebook 02\n# - generated_designs.npy\n# - baseline_designs.npy\n# - conditions.json\n\nraise NotImplementedError('Complete TODO 5 artifact export')" + "source": [ + "# TODO 5: serialize artifacts for Notebook 02\n", + "# Required files:\n", + "# - ARTIFACT_DIR / 'generated_designs.npy'\n", + "# - ARTIFACT_DIR / 'baseline_designs.npy'\n", + "# - ARTIFACT_DIR / 'conditions.json'\n", + "#\n", + "# Optional (advanced): if USE_WANDB_ARTIFACTS is True,\n", + "# upload these files as a W&B artifact named WANDB_ARTIFACT_NAME.\n", + "\n", + "raise NotImplementedError('Complete TODO 5 artifact export')\n" + ] }, { "cell_type": "code", diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index 54c63cd..d1f66a8 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -17,7 +17,7 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'pandas', 'matplotlib']\n", + "PACKAGES = ['engibench[beams2d]', 'pandas', 'matplotlib', 'wandb']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", @@ -43,41 +43,55 @@ "\n", "from engibench.problems.beams2d.v0 import Beams2D\n", "\n", + "# Optional W&B artifact flow (disabled by default)\n", + "USE_WANDB_ARTIFACTS = False\n", + "WANDB_PROJECT = 'dcc26-workshop'\n", + "WANDB_ENTITY = None\n", + "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", + "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", - "def resolve_artifact_dir() -> Path:\n", - " in_colab = 'google.colab' in sys.modules\n", "\n", - " candidates = []\n", - " if in_colab:\n", - " try:\n", - " from google.colab import drive\n", - "\n", - " if not Path('/content/drive/MyDrive').exists():\n", - " print('Mounting Google Drive to look for Notebook 01 artifacts...')\n", - " drive.mount('/content/drive', force_remount=False)\n", - " except Exception as exc:\n", - " print('Drive mount skipped/failed, trying runtime paths:', exc)\n", + "def preferred_artifact_dir() -> Path:\n", + " if 'google.colab' in sys.modules:\n", + " return Path('/content/dcc26_artifacts')\n", + " return Path('workshops/dcc26/artifacts')\n", "\n", - " candidates.extend([\n", - " Path('/content/drive/MyDrive/dcc26_workshop/artifacts'),\n", - " Path('/content/workshops/dcc26/artifacts'),\n", - " Path('workshops/dcc26/artifacts'),\n", - " ])\n", - " else:\n", - " candidates.append(Path('workshops/dcc26/artifacts'))\n", "\n", - " for cand in candidates:\n", - " if cand.exists():\n", - " return cand\n", + "ARTIFACT_DIR = preferred_artifact_dir()\n", + "required = [\n", + " ARTIFACT_DIR / 'generated_designs.npy',\n", + " ARTIFACT_DIR / 'baseline_designs.npy',\n", + " ARTIFACT_DIR / 'conditions.json',\n", + "]\n", "\n", - " expected = '\\n'.join(f'- {p}' for p in candidates)\n", - " raise FileNotFoundError(\n", - " 'Notebook 01 artifacts not found. Run Notebook 01 first (including its export cell), '\n", - " 'or keep artifacts in Google Drive. Checked:\\n' + expected\n", - " )\n", + "if not all(p.exists() for p in required):\n", + " if USE_WANDB_ARTIFACTS:\n", + " try:\n", + " import wandb\n", "\n", + " ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", + " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='artifact-download', reinit=True)\n", + " if WANDB_ENTITY:\n", + " artifact_ref = f\"{WANDB_ENTITY}/{WANDB_PROJECT}/{WANDB_ARTIFACT_NAME}:{WANDB_ARTIFACT_ALIAS}\"\n", + " else:\n", + " artifact_ref = f\"{WANDB_PROJECT}/{WANDB_ARTIFACT_NAME}:{WANDB_ARTIFACT_ALIAS}\"\n", + " artifact = run.use_artifact(artifact_ref, type='dataset')\n", + " artifact.download(root=str(ARTIFACT_DIR))\n", + " run.finish()\n", + " print('Downloaded artifacts from W&B to', ARTIFACT_DIR)\n", + " except Exception as exc:\n", + " raise FileNotFoundError(\n", + " 'Artifacts missing locally and W&B download failed. '\n", + " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", + " f'Details: {exc}'\n", + " ) from exc\n", + " else:\n", + " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", + " raise FileNotFoundError(\n", + " 'Notebook 01 artifacts not found. Run Notebook 01 first (including export cell). '\n", + " 'If you want remote restore, enable USE_WANDB_ARTIFACTS. Missing files:\\n' + missing\n", + " )\n", "\n", - "ARTIFACT_DIR = resolve_artifact_dir()\n", "print('using artifact dir:', ARTIFACT_DIR)\n", "\n", "gen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\n", diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 3c3c190..4604e61 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -61,31 +61,23 @@ "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", " in_colab = 'google.colab' in sys.modules\n", - "\n", " if in_colab:\n", - " try:\n", - " from google.colab import drive\n", - "\n", - " if not Path('/content/drive/MyDrive').exists():\n", - " print('Mounting Google Drive for persistent workshop artifacts...')\n", - " drive.mount('/content/drive', force_remount=False)\n", - " except Exception as exc:\n", - " print('Drive mount skipped/failed, falling back to runtime storage:', exc)\n", - "\n", - " drive_artifacts = Path('/content/drive/MyDrive/dcc26_workshop/artifacts')\n", - " runtime_artifacts = Path('/content/workshops/dcc26/artifacts')\n", - "\n", - " target = drive_artifacts if Path('/content/drive/MyDrive').exists() else runtime_artifacts\n", - " if create:\n", - " target.mkdir(parents=True, exist_ok=True)\n", - " return target\n", + " path = Path('/content/dcc26_artifacts')\n", + " else:\n", + " path = Path('workshops/dcc26/artifacts')\n", "\n", - " local_artifacts = Path('workshops/dcc26/artifacts')\n", " if create:\n", - " local_artifacts.mkdir(parents=True, exist_ok=True)\n", - " return local_artifacts\n", + " path.mkdir(parents=True, exist_ok=True)\n", + " return path\n", "\n", "\n", + "# Optional W&B artifact flow (disabled by default)\n", + "USE_WANDB_ARTIFACTS = False\n", + "WANDB_PROJECT = 'dcc26-workshop'\n", + "WANDB_ENTITY = None\n", + "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", + "WANDB_ARTIFACT_ALIAS = 'latest'\n", + "\n", "SEED = 7\n", "random.seed(SEED)\n", "np.random.seed(SEED)\n", @@ -240,7 +232,41 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "conditions_records = []\nfor i in range(N_SAMPLES):\n rec = {}\n for j, k in enumerate(condition_keys):\n v = test_conds[i, j]\n rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n conditions_records.append(rec)\n\nnp.save(ARTIFACT_DIR / 'generated_designs.npy', gen_designs)\nnp.save(ARTIFACT_DIR / 'baseline_designs.npy', baseline_designs)\nwith open(ARTIFACT_DIR / 'conditions.json', 'w', encoding='utf-8') as f:\n json.dump(conditions_records, f, indent=2)\n\nprint('Saved artifacts to', ARTIFACT_DIR)" + "source": [ + "conditions_records = []\n", + "for i in range(N_SAMPLES):\n", + " rec = {}\n", + " for j, k in enumerate(condition_keys):\n", + " v = test_conds[i, j]\n", + " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", + " conditions_records.append(rec)\n", + "\n", + "generated_path = ARTIFACT_DIR / 'generated_designs.npy'\n", + "baseline_path = ARTIFACT_DIR / 'baseline_designs.npy'\n", + "conditions_path = ARTIFACT_DIR / 'conditions.json'\n", + "\n", + "np.save(generated_path, gen_designs)\n", + "np.save(baseline_path, baseline_designs)\n", + "with open(conditions_path, 'w', encoding='utf-8') as f:\n", + " json.dump(conditions_records, f, indent=2)\n", + "\n", + "print('Saved artifacts to', ARTIFACT_DIR)\n", + "\n", + "if USE_WANDB_ARTIFACTS:\n", + " try:\n", + " import wandb\n", + "\n", + " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='artifact-upload', reinit=True)\n", + " artifact = wandb.Artifact(WANDB_ARTIFACT_NAME, type='dataset', description='DCC26 Notebook 01 generated artifacts')\n", + " artifact.add_file(str(generated_path))\n", + " artifact.add_file(str(baseline_path))\n", + " artifact.add_file(str(conditions_path))\n", + " run.log_artifact(artifact, aliases=[WANDB_ARTIFACT_ALIAS])\n", + " run.finish()\n", + " print('Uploaded artifacts to W&B:', WANDB_ARTIFACT_NAME)\n", + " except Exception as exc:\n", + " print('W&B upload failed (continuing with local artifacts only):', exc)\n" + ] }, { "cell_type": "code", diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 146cd79..6879a97 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -17,7 +17,7 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'pandas', 'matplotlib']\n", + "PACKAGES = ['engibench[beams2d]', 'pandas', 'matplotlib', 'wandb']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", @@ -43,41 +43,55 @@ "\n", "from engibench.problems.beams2d.v0 import Beams2D\n", "\n", + "# Optional W&B artifact flow (disabled by default)\n", + "USE_WANDB_ARTIFACTS = False\n", + "WANDB_PROJECT = 'dcc26-workshop'\n", + "WANDB_ENTITY = None\n", + "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", + "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", - "def resolve_artifact_dir() -> Path:\n", - " in_colab = 'google.colab' in sys.modules\n", "\n", - " candidates = []\n", - " if in_colab:\n", - " try:\n", - " from google.colab import drive\n", - "\n", - " if not Path('/content/drive/MyDrive').exists():\n", - " print('Mounting Google Drive to look for Notebook 01 artifacts...')\n", - " drive.mount('/content/drive', force_remount=False)\n", - " except Exception as exc:\n", - " print('Drive mount skipped/failed, trying runtime paths:', exc)\n", + "def preferred_artifact_dir() -> Path:\n", + " if 'google.colab' in sys.modules:\n", + " return Path('/content/dcc26_artifacts')\n", + " return Path('workshops/dcc26/artifacts')\n", "\n", - " candidates.extend([\n", - " Path('/content/drive/MyDrive/dcc26_workshop/artifacts'),\n", - " Path('/content/workshops/dcc26/artifacts'),\n", - " Path('workshops/dcc26/artifacts'),\n", - " ])\n", - " else:\n", - " candidates.append(Path('workshops/dcc26/artifacts'))\n", "\n", - " for cand in candidates:\n", - " if cand.exists():\n", - " return cand\n", + "ARTIFACT_DIR = preferred_artifact_dir()\n", + "required = [\n", + " ARTIFACT_DIR / 'generated_designs.npy',\n", + " ARTIFACT_DIR / 'baseline_designs.npy',\n", + " ARTIFACT_DIR / 'conditions.json',\n", + "]\n", "\n", - " expected = '\\n'.join(f'- {p}' for p in candidates)\n", - " raise FileNotFoundError(\n", - " 'Notebook 01 artifacts not found. Run Notebook 01 first (including its export cell), '\n", - " 'or keep artifacts in Google Drive. Checked:\\n' + expected\n", - " )\n", + "if not all(p.exists() for p in required):\n", + " if USE_WANDB_ARTIFACTS:\n", + " try:\n", + " import wandb\n", "\n", + " ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", + " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='artifact-download', reinit=True)\n", + " if WANDB_ENTITY:\n", + " artifact_ref = f\"{WANDB_ENTITY}/{WANDB_PROJECT}/{WANDB_ARTIFACT_NAME}:{WANDB_ARTIFACT_ALIAS}\"\n", + " else:\n", + " artifact_ref = f\"{WANDB_PROJECT}/{WANDB_ARTIFACT_NAME}:{WANDB_ARTIFACT_ALIAS}\"\n", + " artifact = run.use_artifact(artifact_ref, type='dataset')\n", + " artifact.download(root=str(ARTIFACT_DIR))\n", + " run.finish()\n", + " print('Downloaded artifacts from W&B to', ARTIFACT_DIR)\n", + " except Exception as exc:\n", + " raise FileNotFoundError(\n", + " 'Artifacts missing locally and W&B download failed. '\n", + " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", + " f'Details: {exc}'\n", + " ) from exc\n", + " else:\n", + " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", + " raise FileNotFoundError(\n", + " 'Notebook 01 artifacts not found. Run Notebook 01 first (including export cell). '\n", + " 'If you want remote restore, enable USE_WANDB_ARTIFACTS. Missing files:\\n' + missing\n", + " )\n", "\n", - "ARTIFACT_DIR = resolve_artifact_dir()\n", "print('using artifact dir:', ARTIFACT_DIR)\n", "\n", "gen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\n", From 5f898c81aa456ae26b46f9542953b3ccf11241d9 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 15:18:26 +0100 Subject: [PATCH 14/37] Auto-regenerate missing artifacts in Notebook 02 --- workshops/dcc26/README.md | 1 + .../participant/02_evaluate_metrics.ipynb | 59 +++++++++++++++++-- .../dcc26/solutions/02_evaluate_metrics.ipynb | 59 +++++++++++++++++-- 3 files changed, 109 insertions(+), 10 deletions(-) diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index eaa5c6a..f8050dc 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -70,6 +70,7 @@ Optional: - You can enable W&B artifact upload/download in Notebook 01/02 by setting `USE_WANDB_ARTIFACTS = True`. - W&B is disabled by default so participants can run without account setup. +- Notebook 02 auto-regenerates Notebook 01-style artifacts if they are missing in a fresh Colab runtime. These include: diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index d1f66a8..4dd2fdf 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -50,6 +50,9 @@ "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", + "# No-auth fallback: regenerate artifacts locally if missing\n", + "AUTO_REGENERATE_ARTIFACTS_IF_MISSING = True\n", + "\n", "\n", "def preferred_artifact_dir() -> Path:\n", " if 'google.colab' in sys.modules:\n", @@ -57,6 +60,46 @@ " return Path('workshops/dcc26/artifacts')\n", "\n", "\n", + "def regenerate_artifacts(artifact_dir: Path, seed: int = 7, n_train: int = 512, n_samples: int = 24) -> None:\n", + " print('Regenerating Notebook 01 artifacts (nearest-neighbor fallback)...')\n", + " rng = np.random.default_rng(seed)\n", + " problem = Beams2D(seed=seed)\n", + " train_ds = problem.dataset['train']\n", + " test_ds = problem.dataset['test']\n", + " condition_keys = problem.conditions_keys\n", + "\n", + " subset_idx = rng.choice(len(train_ds), size=n_train, replace=False)\n", + " conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", + " designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", + "\n", + " selected = rng.choice(len(test_ds), size=n_samples, replace=False)\n", + " test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", + " baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", + "\n", + " generated = []\n", + " for c in test_conds:\n", + " dists = np.linalg.norm(conds_np - c[None, :], axis=1)\n", + " idx = int(np.argmin(dists))\n", + " generated.append(designs_np[idx])\n", + " gen_designs = np.array(generated, dtype=np.float32)\n", + "\n", + " conditions_records = []\n", + " for i in range(n_samples):\n", + " rec = {}\n", + " for j, k in enumerate(condition_keys):\n", + " v = test_conds[i, j]\n", + " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", + " conditions_records.append(rec)\n", + "\n", + " artifact_dir.mkdir(parents=True, exist_ok=True)\n", + " np.save(artifact_dir / 'generated_designs.npy', gen_designs)\n", + " np.save(artifact_dir / 'baseline_designs.npy', baseline_designs)\n", + " with open(artifact_dir / 'conditions.json', 'w', encoding='utf-8') as f:\n", + " json.dump(conditions_records, f, indent=2)\n", + "\n", + " print('Regenerated artifacts at', artifact_dir)\n", + "\n", + "\n", "ARTIFACT_DIR = preferred_artifact_dir()\n", "required = [\n", " ARTIFACT_DIR / 'generated_designs.npy',\n", @@ -80,11 +123,17 @@ " run.finish()\n", " print('Downloaded artifacts from W&B to', ARTIFACT_DIR)\n", " except Exception as exc:\n", - " raise FileNotFoundError(\n", - " 'Artifacts missing locally and W&B download failed. '\n", - " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", - " f'Details: {exc}'\n", - " ) from exc\n", + " if AUTO_REGENERATE_ARTIFACTS_IF_MISSING:\n", + " print('W&B download failed, switching to local regeneration:', exc)\n", + " regenerate_artifacts(ARTIFACT_DIR)\n", + " else:\n", + " raise FileNotFoundError(\n", + " 'Artifacts missing locally and W&B download failed. '\n", + " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", + " f'Details: {exc}'\n", + " ) from exc\n", + " elif AUTO_REGENERATE_ARTIFACTS_IF_MISSING:\n", + " regenerate_artifacts(ARTIFACT_DIR)\n", " else:\n", " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", " raise FileNotFoundError(\n", diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 6879a97..a662f2a 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -50,6 +50,9 @@ "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", + "# No-auth fallback: regenerate artifacts locally if missing\n", + "AUTO_REGENERATE_ARTIFACTS_IF_MISSING = True\n", + "\n", "\n", "def preferred_artifact_dir() -> Path:\n", " if 'google.colab' in sys.modules:\n", @@ -57,6 +60,46 @@ " return Path('workshops/dcc26/artifacts')\n", "\n", "\n", + "def regenerate_artifacts(artifact_dir: Path, seed: int = 7, n_train: int = 512, n_samples: int = 24) -> None:\n", + " print('Regenerating Notebook 01 artifacts (nearest-neighbor fallback)...')\n", + " rng = np.random.default_rng(seed)\n", + " problem = Beams2D(seed=seed)\n", + " train_ds = problem.dataset['train']\n", + " test_ds = problem.dataset['test']\n", + " condition_keys = problem.conditions_keys\n", + "\n", + " subset_idx = rng.choice(len(train_ds), size=n_train, replace=False)\n", + " conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", + " designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", + "\n", + " selected = rng.choice(len(test_ds), size=n_samples, replace=False)\n", + " test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", + " baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", + "\n", + " generated = []\n", + " for c in test_conds:\n", + " dists = np.linalg.norm(conds_np - c[None, :], axis=1)\n", + " idx = int(np.argmin(dists))\n", + " generated.append(designs_np[idx])\n", + " gen_designs = np.array(generated, dtype=np.float32)\n", + "\n", + " conditions_records = []\n", + " for i in range(n_samples):\n", + " rec = {}\n", + " for j, k in enumerate(condition_keys):\n", + " v = test_conds[i, j]\n", + " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", + " conditions_records.append(rec)\n", + "\n", + " artifact_dir.mkdir(parents=True, exist_ok=True)\n", + " np.save(artifact_dir / 'generated_designs.npy', gen_designs)\n", + " np.save(artifact_dir / 'baseline_designs.npy', baseline_designs)\n", + " with open(artifact_dir / 'conditions.json', 'w', encoding='utf-8') as f:\n", + " json.dump(conditions_records, f, indent=2)\n", + "\n", + " print('Regenerated artifacts at', artifact_dir)\n", + "\n", + "\n", "ARTIFACT_DIR = preferred_artifact_dir()\n", "required = [\n", " ARTIFACT_DIR / 'generated_designs.npy',\n", @@ -80,11 +123,17 @@ " run.finish()\n", " print('Downloaded artifacts from W&B to', ARTIFACT_DIR)\n", " except Exception as exc:\n", - " raise FileNotFoundError(\n", - " 'Artifacts missing locally and W&B download failed. '\n", - " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", - " f'Details: {exc}'\n", - " ) from exc\n", + " if AUTO_REGENERATE_ARTIFACTS_IF_MISSING:\n", + " print('W&B download failed, switching to local regeneration:', exc)\n", + " regenerate_artifacts(ARTIFACT_DIR)\n", + " else:\n", + " raise FileNotFoundError(\n", + " 'Artifacts missing locally and W&B download failed. '\n", + " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", + " f'Details: {exc}'\n", + " ) from exc\n", + " elif AUTO_REGENERATE_ARTIFACTS_IF_MISSING:\n", + " regenerate_artifacts(ARTIFACT_DIR)\n", " else:\n", " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", " raise FileNotFoundError(\n", From 1bdf59317fbd9fd3655875f4220912e9eaa5f917 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Fri, 6 Mar 2026 08:41:14 +0100 Subject: [PATCH 15/37] Remove Drive permissions flow from DCC26 Colab notebooks --- workshops/dcc26/README.md | 15 ++-- .../dcc26/participant/01_train_generate.ipynb | 24 +++--- .../participant/02_evaluate_metrics.ipynb | 78 ++++--------------- .../dcc26/solutions/01_train_generate.ipynb | 38 ++++----- .../dcc26/solutions/02_evaluate_metrics.ipynb | 78 ++++--------------- 5 files changed, 67 insertions(+), 166 deletions(-) diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index f8050dc..ea64a04 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -17,7 +17,7 @@ It is split into two tracks: - `participant/01_train_generate.ipynb` and `solutions/01_train_generate.ipynb` (30 min) - Lightweight training using `engiopt.cgan_2d.Generator` - Deterministic seeds - - Fallback path with nearest-neighbor generation + - Artifact export for downstream evaluation (runtime/W&B optional transport) - `participant/02_evaluate_metrics.ipynb` and `solutions/02_evaluate_metrics.ipynb` (20 min) - Constraint validation @@ -44,7 +44,7 @@ All notebooks now include a conditional dependency bootstrap cell: - On Colab: installs required packages automatically. - On local envs: skips install by default (`FORCE_INSTALL = False`). -- Note: `engiopt` is not installed from PyPI in these notebooks; workshop execution relies on `engibench` + standard ML libraries. +- Note: `engiopt` is installed from the EngiOpt GitHub branch in Notebook 01 bootstrap. ## Open in Colab @@ -64,13 +64,13 @@ Pre-merge (current branch) links: By default, solution notebooks write generated artifacts to: - Local/Jupyter: `workshops/dcc26/artifacts/` -- Google Colab runtime: `/content/dcc26_artifacts/` (no auth required) +- Google Colab runtime: `/content/dcc26_artifacts/` (no Google Drive permission needed) Optional: - You can enable W&B artifact upload/download in Notebook 01/02 by setting `USE_WANDB_ARTIFACTS = True`. - W&B is disabled by default so participants can run without account setup. -- Notebook 02 auto-regenerates Notebook 01-style artifacts if they are missing in a fresh Colab runtime. +- Notebook 02 does not regenerate artifacts; it expects Notebook 01 artifacts (or W&B download when enabled). These include: @@ -85,9 +85,10 @@ These include: If runtime is constrained: -1. Skip long training in `01_train_generate.ipynb` by enabling fallback mode. -2. Continue directly to `02_evaluate_metrics.ipynb` with fallback-generated designs. -3. Keep `03_add_new_problem_scaffold.ipynb` as the capstone for extensibility. +1. Reuse a previously saved checkpoint/artifact set from W&B or local runtime files. +2. Set `TRAIN_FROM_SCRATCH = False` in `01_train_generate.ipynb` to load the checkpoint. +3. Continue to `02_evaluate_metrics.ipynb` with the exported artifacts. +4. Keep `03_add_new_problem_scaffold.ipynb` as the capstone for extensibility. ## Suggested pre-workshop checks diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index db0510d..71aa537 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -59,6 +59,14 @@ " ) from exc\n", "\n", "\n", + "# Optional W&B artifact flow (disabled by default)\n", + "USE_WANDB_ARTIFACTS = False\n", + "WANDB_PROJECT = 'dcc26-workshop'\n", + "WANDB_ENTITY = None\n", + "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", + "WANDB_ARTIFACT_ALIAS = 'latest'\n", + "\n", + "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", " in_colab = 'google.colab' in sys.modules\n", " if in_colab:\n", @@ -71,13 +79,6 @@ " return path\n", "\n", "\n", - "# Optional W&B artifact flow (disabled by default)\n", - "USE_WANDB_ARTIFACTS = False\n", - "WANDB_PROJECT = 'dcc26-workshop'\n", - "WANDB_ENTITY = None\n", - "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", - "WANDB_ARTIFACT_ALIAS = 'latest'\n", - "\n", "SEED = 7\n", "random.seed(SEED)\n", "np.random.seed(SEED)\n", @@ -167,7 +168,7 @@ " model.eval()\n", " print('loaded checkpoint from', CKPT_PATH)\n", "else:\n", - " print('No checkpoint found. Use fallback generation cell below.')\n" + " raise FileNotFoundError(f'No checkpoint found at {CKPT_PATH}. Set TRAIN_FROM_SCRATCH=True or provide a checkpoint.')\n" ] }, { @@ -178,13 +179,10 @@ "source": [ "# TODO 3: implement generation path with EngiOpt generator\n", "# - sample N_SAMPLES test conditions and baseline designs\n", - "# - if USE_NEAREST_NEIGHBOR_FALLBACK: nearest-neighbor designs from training subset\n", - "# - else: run model(sample_noise(...), condition_tensor)\n", - "# and map tanh outputs to design space with ((x + 1)/2).clamp(0, 1)\n", + "# - run model(sample_noise(...), condition_tensor)\n", + "# - map tanh outputs to design space with ((x + 1)/2).clamp(0, 1)\n", "# - set gen_designs, baseline_designs, test_conds\n", "\n", - "USE_NEAREST_NEIGHBOR_FALLBACK = False\n", - "\n", "raise NotImplementedError('Complete TODO 3 generation path')\n" ] }, diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index 4dd2fdf..9daee2b 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -50,57 +50,20 @@ "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", - "# No-auth fallback: regenerate artifacts locally if missing\n", - "AUTO_REGENERATE_ARTIFACTS_IF_MISSING = True\n", "\n", + "def resolve_artifact_dir(create: bool = False) -> Path:\n", + " in_colab = 'google.colab' in sys.modules\n", + " if in_colab:\n", + " path = Path('/content/dcc26_artifacts')\n", + " else:\n", + " path = Path('workshops/dcc26/artifacts')\n", "\n", - "def preferred_artifact_dir() -> Path:\n", - " if 'google.colab' in sys.modules:\n", - " return Path('/content/dcc26_artifacts')\n", - " return Path('workshops/dcc26/artifacts')\n", - "\n", - "\n", - "def regenerate_artifacts(artifact_dir: Path, seed: int = 7, n_train: int = 512, n_samples: int = 24) -> None:\n", - " print('Regenerating Notebook 01 artifacts (nearest-neighbor fallback)...')\n", - " rng = np.random.default_rng(seed)\n", - " problem = Beams2D(seed=seed)\n", - " train_ds = problem.dataset['train']\n", - " test_ds = problem.dataset['test']\n", - " condition_keys = problem.conditions_keys\n", - "\n", - " subset_idx = rng.choice(len(train_ds), size=n_train, replace=False)\n", - " conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", - " designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", - "\n", - " selected = rng.choice(len(test_ds), size=n_samples, replace=False)\n", - " test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", - " baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", - "\n", - " generated = []\n", - " for c in test_conds:\n", - " dists = np.linalg.norm(conds_np - c[None, :], axis=1)\n", - " idx = int(np.argmin(dists))\n", - " generated.append(designs_np[idx])\n", - " gen_designs = np.array(generated, dtype=np.float32)\n", - "\n", - " conditions_records = []\n", - " for i in range(n_samples):\n", - " rec = {}\n", - " for j, k in enumerate(condition_keys):\n", - " v = test_conds[i, j]\n", - " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", - " conditions_records.append(rec)\n", - "\n", - " artifact_dir.mkdir(parents=True, exist_ok=True)\n", - " np.save(artifact_dir / 'generated_designs.npy', gen_designs)\n", - " np.save(artifact_dir / 'baseline_designs.npy', baseline_designs)\n", - " with open(artifact_dir / 'conditions.json', 'w', encoding='utf-8') as f:\n", - " json.dump(conditions_records, f, indent=2)\n", - "\n", - " print('Regenerated artifacts at', artifact_dir)\n", + " if create:\n", + " path.mkdir(parents=True, exist_ok=True)\n", + " return path\n", "\n", "\n", - "ARTIFACT_DIR = preferred_artifact_dir()\n", + "ARTIFACT_DIR = resolve_artifact_dir(create=True)\n", "required = [\n", " ARTIFACT_DIR / 'generated_designs.npy',\n", " ARTIFACT_DIR / 'baseline_designs.npy',\n", @@ -112,7 +75,6 @@ " try:\n", " import wandb\n", "\n", - " ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='artifact-download', reinit=True)\n", " if WANDB_ENTITY:\n", " artifact_ref = f\"{WANDB_ENTITY}/{WANDB_PROJECT}/{WANDB_ARTIFACT_NAME}:{WANDB_ARTIFACT_ALIAS}\"\n", @@ -123,22 +85,16 @@ " run.finish()\n", " print('Downloaded artifacts from W&B to', ARTIFACT_DIR)\n", " except Exception as exc:\n", - " if AUTO_REGENERATE_ARTIFACTS_IF_MISSING:\n", - " print('W&B download failed, switching to local regeneration:', exc)\n", - " regenerate_artifacts(ARTIFACT_DIR)\n", - " else:\n", - " raise FileNotFoundError(\n", - " 'Artifacts missing locally and W&B download failed. '\n", - " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", - " f'Details: {exc}'\n", - " ) from exc\n", - " elif AUTO_REGENERATE_ARTIFACTS_IF_MISSING:\n", - " regenerate_artifacts(ARTIFACT_DIR)\n", + " raise FileNotFoundError(\n", + " 'Artifacts missing locally and W&B download failed. '\n", + " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", + " f'Details: {exc}'\n", + " ) from exc\n", " else:\n", " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", " raise FileNotFoundError(\n", - " 'Notebook 01 artifacts not found. Run Notebook 01 first (including export cell). '\n", - " 'If you want remote restore, enable USE_WANDB_ARTIFACTS. Missing files:\\n' + missing\n", + " 'Notebook 01 artifacts not found. Run Notebook 01 first (including export cell), '\n", + " 'or enable USE_WANDB_ARTIFACTS to fetch from W&B. Missing files:\\n' + missing\n", " )\n", "\n", "print('using artifact dir:', ARTIFACT_DIR)\n", diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 4604e61..9efa9b6 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -59,6 +59,14 @@ " ) from exc\n", "\n", "\n", + "# Optional W&B artifact flow (disabled by default)\n", + "USE_WANDB_ARTIFACTS = False\n", + "WANDB_PROJECT = 'dcc26-workshop'\n", + "WANDB_ENTITY = None\n", + "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", + "WANDB_ARTIFACT_ALIAS = 'latest'\n", + "\n", + "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", " in_colab = 'google.colab' in sys.modules\n", " if in_colab:\n", @@ -71,13 +79,6 @@ " return path\n", "\n", "\n", - "# Optional W&B artifact flow (disabled by default)\n", - "USE_WANDB_ARTIFACTS = False\n", - "WANDB_PROJECT = 'dcc26-workshop'\n", - "WANDB_ENTITY = None\n", - "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", - "WANDB_ARTIFACT_ALIAS = 'latest'\n", - "\n", "SEED = 7\n", "random.seed(SEED)\n", "np.random.seed(SEED)\n", @@ -190,7 +191,7 @@ " model.eval()\n", " print('loaded checkpoint from', CKPT_PATH)\n", "else:\n", - " print('No checkpoint found. Use fallback generation cell below.')\n" + " raise FileNotFoundError(f'No checkpoint found at {CKPT_PATH}. Set TRAIN_FROM_SCRATCH=True or provide a checkpoint.')\n" ] }, { @@ -199,9 +200,6 @@ "metadata": {}, "outputs": [], "source": [ - "# Optional fallback: nearest-neighbor by condition vector\n", - "USE_NEAREST_NEIGHBOR_FALLBACK = False\n", - "\n", "rng = np.random.default_rng(SEED)\n", "N_SAMPLES = 24\n", "selected = rng.choice(len(test_ds), size=N_SAMPLES, replace=False)\n", @@ -209,19 +207,11 @@ "test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", "baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", "\n", - "if USE_NEAREST_NEIGHBOR_FALLBACK:\n", - " generated = []\n", - " for c in test_conds:\n", - " dists = np.linalg.norm(conds_np - c[None, :], axis=1)\n", - " idx = int(np.argmin(dists))\n", - " generated.append(designs_np[idx])\n", - " gen_designs = np.array(generated, dtype=np.float32)\n", - "else:\n", - " model.eval()\n", - " with th.no_grad():\n", - " tanh_out = model(sample_noise(N_SAMPLES), th.tensor(test_conds, device=DEVICE))\n", - " gen_designs_t = ((tanh_out.clamp(-1.0, 1.0) + 1.0) / 2.0).clamp(0.0, 1.0)\n", - " gen_designs = gen_designs_t.detach().cpu().numpy().astype(np.float32)\n", + "model.eval()\n", + "with th.no_grad():\n", + " tanh_out = model(sample_noise(N_SAMPLES), th.tensor(test_conds, device=DEVICE))\n", + " gen_designs_t = ((tanh_out.clamp(-1.0, 1.0) + 1.0) / 2.0).clamp(0.0, 1.0)\n", + "gen_designs = gen_designs_t.detach().cpu().numpy().astype(np.float32)\n", "\n", "print('generated shape:', gen_designs.shape)\n", "print('baseline shape:', baseline_designs.shape)\n" diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index a662f2a..31b3777 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -50,57 +50,20 @@ "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", - "# No-auth fallback: regenerate artifacts locally if missing\n", - "AUTO_REGENERATE_ARTIFACTS_IF_MISSING = True\n", "\n", + "def resolve_artifact_dir(create: bool = False) -> Path:\n", + " in_colab = 'google.colab' in sys.modules\n", + " if in_colab:\n", + " path = Path('/content/dcc26_artifacts')\n", + " else:\n", + " path = Path('workshops/dcc26/artifacts')\n", "\n", - "def preferred_artifact_dir() -> Path:\n", - " if 'google.colab' in sys.modules:\n", - " return Path('/content/dcc26_artifacts')\n", - " return Path('workshops/dcc26/artifacts')\n", - "\n", - "\n", - "def regenerate_artifacts(artifact_dir: Path, seed: int = 7, n_train: int = 512, n_samples: int = 24) -> None:\n", - " print('Regenerating Notebook 01 artifacts (nearest-neighbor fallback)...')\n", - " rng = np.random.default_rng(seed)\n", - " problem = Beams2D(seed=seed)\n", - " train_ds = problem.dataset['train']\n", - " test_ds = problem.dataset['test']\n", - " condition_keys = problem.conditions_keys\n", - "\n", - " subset_idx = rng.choice(len(train_ds), size=n_train, replace=False)\n", - " conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", - " designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", - "\n", - " selected = rng.choice(len(test_ds), size=n_samples, replace=False)\n", - " test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", - " baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", - "\n", - " generated = []\n", - " for c in test_conds:\n", - " dists = np.linalg.norm(conds_np - c[None, :], axis=1)\n", - " idx = int(np.argmin(dists))\n", - " generated.append(designs_np[idx])\n", - " gen_designs = np.array(generated, dtype=np.float32)\n", - "\n", - " conditions_records = []\n", - " for i in range(n_samples):\n", - " rec = {}\n", - " for j, k in enumerate(condition_keys):\n", - " v = test_conds[i, j]\n", - " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", - " conditions_records.append(rec)\n", - "\n", - " artifact_dir.mkdir(parents=True, exist_ok=True)\n", - " np.save(artifact_dir / 'generated_designs.npy', gen_designs)\n", - " np.save(artifact_dir / 'baseline_designs.npy', baseline_designs)\n", - " with open(artifact_dir / 'conditions.json', 'w', encoding='utf-8') as f:\n", - " json.dump(conditions_records, f, indent=2)\n", - "\n", - " print('Regenerated artifacts at', artifact_dir)\n", + " if create:\n", + " path.mkdir(parents=True, exist_ok=True)\n", + " return path\n", "\n", "\n", - "ARTIFACT_DIR = preferred_artifact_dir()\n", + "ARTIFACT_DIR = resolve_artifact_dir(create=True)\n", "required = [\n", " ARTIFACT_DIR / 'generated_designs.npy',\n", " ARTIFACT_DIR / 'baseline_designs.npy',\n", @@ -112,7 +75,6 @@ " try:\n", " import wandb\n", "\n", - " ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='artifact-download', reinit=True)\n", " if WANDB_ENTITY:\n", " artifact_ref = f\"{WANDB_ENTITY}/{WANDB_PROJECT}/{WANDB_ARTIFACT_NAME}:{WANDB_ARTIFACT_ALIAS}\"\n", @@ -123,22 +85,16 @@ " run.finish()\n", " print('Downloaded artifacts from W&B to', ARTIFACT_DIR)\n", " except Exception as exc:\n", - " if AUTO_REGENERATE_ARTIFACTS_IF_MISSING:\n", - " print('W&B download failed, switching to local regeneration:', exc)\n", - " regenerate_artifacts(ARTIFACT_DIR)\n", - " else:\n", - " raise FileNotFoundError(\n", - " 'Artifacts missing locally and W&B download failed. '\n", - " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", - " f'Details: {exc}'\n", - " ) from exc\n", - " elif AUTO_REGENERATE_ARTIFACTS_IF_MISSING:\n", - " regenerate_artifacts(ARTIFACT_DIR)\n", + " raise FileNotFoundError(\n", + " 'Artifacts missing locally and W&B download failed. '\n", + " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", + " f'Details: {exc}'\n", + " ) from exc\n", " else:\n", " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", " raise FileNotFoundError(\n", - " 'Notebook 01 artifacts not found. Run Notebook 01 first (including export cell). '\n", - " 'If you want remote restore, enable USE_WANDB_ARTIFACTS. Missing files:\\n' + missing\n", + " 'Notebook 01 artifacts not found. Run Notebook 01 first (including export cell), '\n", + " 'or enable USE_WANDB_ARTIFACTS to fetch from W&B. Missing files:\\n' + missing\n", " )\n", "\n", "print('using artifact dir:', ARTIFACT_DIR)\n", From d7dbfccd875bbdd80487e1cb3f3c7e59dda8869e Mon Sep 17 00:00:00 2001 From: Soheyl Date: Fri, 6 Mar 2026 09:17:14 +0100 Subject: [PATCH 16/37] Auto-build Notebook 2 artifacts with EngiOpt when missing --- workshops/dcc26/README.md | 9 +- .../participant/02_evaluate_metrics.ipynb | 136 +++++++++++++++++- .../dcc26/solutions/02_evaluate_metrics.ipynb | 136 +++++++++++++++++- 3 files changed, 262 insertions(+), 19 deletions(-) diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index ea64a04..fc08f30 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -70,7 +70,7 @@ Optional: - You can enable W&B artifact upload/download in Notebook 01/02 by setting `USE_WANDB_ARTIFACTS = True`. - W&B is disabled by default so participants can run without account setup. -- Notebook 02 does not regenerate artifacts; it expects Notebook 01 artifacts (or W&B download when enabled). +- Notebook 02 auto-builds Notebook 01-style artifacts locally with EngiOpt if they are missing (`AUTO_BUILD_ARTIFACTS_IF_MISSING = True`). These include: @@ -85,10 +85,9 @@ These include: If runtime is constrained: -1. Reuse a previously saved checkpoint/artifact set from W&B or local runtime files. -2. Set `TRAIN_FROM_SCRATCH = False` in `01_train_generate.ipynb` to load the checkpoint. -3. Continue to `02_evaluate_metrics.ipynb` with the exported artifacts. -4. Keep `03_add_new_problem_scaffold.ipynb` as the capstone for extensibility. +1. Skip Notebook 01 and run `02_evaluate_metrics.ipynb`; it can build required artifacts automatically. +2. Or reuse a previously saved checkpoint/artifact set from W&B or local runtime files. +3. Keep `03_add_new_problem_scaffold.ipynb` as the capstone for extensibility. ## Suggested pre-workshop checks diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index 9daee2b..c647d77 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -17,11 +17,14 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'pandas', 'matplotlib', 'wandb']\n", + "PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print('Installing base dependencies...')\n", " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", + " print('Installing EngiOpt from GitHub branch...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', ENGIOPT_GIT])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" @@ -34,15 +37,26 @@ "outputs": [], "source": [ "import json\n", + "import random\n", "import sys\n", "from pathlib import Path\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", + "import torch as th\n", + "import torch.nn as nn\n", + "from torch.utils.data import DataLoader, TensorDataset\n", "\n", "from engibench.problems.beams2d.v0 import Beams2D\n", "\n", + "try:\n", + " from engiopt.cgan_2d.cgan_2d import Generator as EngiOptCGAN2DGenerator\n", + "except ModuleNotFoundError as exc:\n", + " raise ModuleNotFoundError(\n", + " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", + " ) from exc\n", + "\n", "# Optional W&B artifact flow (disabled by default)\n", "USE_WANDB_ARTIFACTS = False\n", "WANDB_PROJECT = 'dcc26-workshop'\n", @@ -50,6 +64,9 @@ "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", + "# Local self-heal path (enabled by default)\n", + "AUTO_BUILD_ARTIFACTS_IF_MISSING = True\n", + "\n", "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", " in_colab = 'google.colab' in sys.modules\n", @@ -63,6 +80,105 @@ " return path\n", "\n", "\n", + "def build_artifacts_locally(\n", + " artifact_dir: Path,\n", + " seed: int = 7,\n", + " n_train: int = 512,\n", + " n_samples: int = 24,\n", + " epochs: int = 8,\n", + " batch_size: int = 64,\n", + " latent_dim: int = 32,\n", + ") -> None:\n", + " print('Building Notebook 01-style artifacts locally with EngiOpt...')\n", + "\n", + " random.seed(seed)\n", + " np.random.seed(seed)\n", + " th.manual_seed(seed)\n", + " if th.cuda.is_available():\n", + " th.cuda.manual_seed_all(seed)\n", + "\n", + " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", + " problem = Beams2D(seed=seed)\n", + " train_ds = problem.dataset['train']\n", + " test_ds = problem.dataset['test']\n", + " condition_keys = problem.conditions_keys\n", + "\n", + " rng = np.random.default_rng(seed)\n", + " subset_size = min(n_train, len(train_ds))\n", + " subset_idx = rng.choice(len(train_ds), size=subset_size, replace=False)\n", + "\n", + " conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", + " designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", + " targets_np = designs_np * 2.0 - 1.0\n", + "\n", + " model = EngiOptCGAN2DGenerator(\n", + " latent_dim=latent_dim,\n", + " n_conds=conds_np.shape[1],\n", + " design_shape=problem.design_space.shape,\n", + " ).to(device)\n", + " optimizer = th.optim.Adam(model.parameters(), lr=1e-3)\n", + " criterion = nn.MSELoss()\n", + "\n", + " def sample_noise(batch: int) -> th.Tensor:\n", + " return th.randn((batch, latent_dim), device=device, dtype=th.float32)\n", + "\n", + " ds = TensorDataset(th.tensor(conds_np), th.tensor(targets_np))\n", + " dl = DataLoader(ds, batch_size=batch_size, shuffle=True)\n", + "\n", + " for epoch in range(epochs):\n", + " model.train()\n", + " epoch_loss = 0.0\n", + " for cond_batch, target_batch in dl:\n", + " cond_batch = cond_batch.to(device)\n", + " target_batch = target_batch.to(device)\n", + "\n", + " pred = model(sample_noise(cond_batch.shape[0]), cond_batch)\n", + " loss = criterion(pred, target_batch)\n", + "\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + " epoch_loss += float(loss.item())\n", + " print(f'bootstrap epoch {epoch + 1:02d}/{epochs} - loss: {epoch_loss / len(dl):.4f}')\n", + "\n", + " sample_count = min(n_samples, len(test_ds))\n", + " selected = rng.choice(len(test_ds), size=sample_count, replace=False)\n", + " test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", + " baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", + "\n", + " model.eval()\n", + " with th.no_grad():\n", + " tanh_out = model(sample_noise(sample_count), th.tensor(test_conds, device=device))\n", + " gen_designs_t = ((tanh_out.clamp(-1.0, 1.0) + 1.0) / 2.0).clamp(0.0, 1.0)\n", + " gen_designs = gen_designs_t.detach().cpu().numpy().astype(np.float32)\n", + "\n", + " conditions_records = []\n", + " for i in range(sample_count):\n", + " rec = {}\n", + " for j, k in enumerate(condition_keys):\n", + " v = test_conds[i, j]\n", + " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", + " conditions_records.append(rec)\n", + "\n", + " artifact_dir.mkdir(parents=True, exist_ok=True)\n", + " np.save(artifact_dir / 'generated_designs.npy', gen_designs)\n", + " np.save(artifact_dir / 'baseline_designs.npy', baseline_designs)\n", + " with open(artifact_dir / 'conditions.json', 'w', encoding='utf-8') as f:\n", + " json.dump(conditions_records, f, indent=2)\n", + "\n", + " th.save(\n", + " {\n", + " 'model': model.state_dict(),\n", + " 'condition_keys': condition_keys,\n", + " 'latent_dim': latent_dim,\n", + " 'model_family': 'engiopt.cgan_2d.Generator',\n", + " },\n", + " artifact_dir / 'engiopt_cgan2d_generator_supervised.pt',\n", + " )\n", + "\n", + " print('Built artifacts at', artifact_dir)\n", + "\n", + "\n", "ARTIFACT_DIR = resolve_artifact_dir(create=True)\n", "required = [\n", " ARTIFACT_DIR / 'generated_designs.npy',\n", @@ -85,11 +201,17 @@ " run.finish()\n", " print('Downloaded artifacts from W&B to', ARTIFACT_DIR)\n", " except Exception as exc:\n", - " raise FileNotFoundError(\n", - " 'Artifacts missing locally and W&B download failed. '\n", - " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", - " f'Details: {exc}'\n", - " ) from exc\n", + " if AUTO_BUILD_ARTIFACTS_IF_MISSING:\n", + " print('W&B download failed; switching to local artifact build:', exc)\n", + " build_artifacts_locally(ARTIFACT_DIR)\n", + " else:\n", + " raise FileNotFoundError(\n", + " 'Artifacts missing locally and W&B download failed. '\n", + " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", + " f'Details: {exc}'\n", + " ) from exc\n", + " elif AUTO_BUILD_ARTIFACTS_IF_MISSING:\n", + " build_artifacts_locally(ARTIFACT_DIR)\n", " else:\n", " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", " raise FileNotFoundError(\n", diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 31b3777..12ae07c 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -17,11 +17,14 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'pandas', 'matplotlib', 'wandb']\n", + "PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print('Installing base dependencies...')\n", " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", + " print('Installing EngiOpt from GitHub branch...')\n", + " subprocess.check_call([sys.executable, '-m', 'pip', 'install', ENGIOPT_GIT])\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment).')\n" @@ -34,15 +37,26 @@ "outputs": [], "source": [ "import json\n", + "import random\n", "import sys\n", "from pathlib import Path\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", + "import torch as th\n", + "import torch.nn as nn\n", + "from torch.utils.data import DataLoader, TensorDataset\n", "\n", "from engibench.problems.beams2d.v0 import Beams2D\n", "\n", + "try:\n", + " from engiopt.cgan_2d.cgan_2d import Generator as EngiOptCGAN2DGenerator\n", + "except ModuleNotFoundError as exc:\n", + " raise ModuleNotFoundError(\n", + " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", + " ) from exc\n", + "\n", "# Optional W&B artifact flow (disabled by default)\n", "USE_WANDB_ARTIFACTS = False\n", "WANDB_PROJECT = 'dcc26-workshop'\n", @@ -50,6 +64,9 @@ "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", + "# Local self-heal path (enabled by default)\n", + "AUTO_BUILD_ARTIFACTS_IF_MISSING = True\n", + "\n", "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", " in_colab = 'google.colab' in sys.modules\n", @@ -63,6 +80,105 @@ " return path\n", "\n", "\n", + "def build_artifacts_locally(\n", + " artifact_dir: Path,\n", + " seed: int = 7,\n", + " n_train: int = 512,\n", + " n_samples: int = 24,\n", + " epochs: int = 8,\n", + " batch_size: int = 64,\n", + " latent_dim: int = 32,\n", + ") -> None:\n", + " print('Building Notebook 01-style artifacts locally with EngiOpt...')\n", + "\n", + " random.seed(seed)\n", + " np.random.seed(seed)\n", + " th.manual_seed(seed)\n", + " if th.cuda.is_available():\n", + " th.cuda.manual_seed_all(seed)\n", + "\n", + " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", + " problem = Beams2D(seed=seed)\n", + " train_ds = problem.dataset['train']\n", + " test_ds = problem.dataset['test']\n", + " condition_keys = problem.conditions_keys\n", + "\n", + " rng = np.random.default_rng(seed)\n", + " subset_size = min(n_train, len(train_ds))\n", + " subset_idx = rng.choice(len(train_ds), size=subset_size, replace=False)\n", + "\n", + " conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", + " designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", + " targets_np = designs_np * 2.0 - 1.0\n", + "\n", + " model = EngiOptCGAN2DGenerator(\n", + " latent_dim=latent_dim,\n", + " n_conds=conds_np.shape[1],\n", + " design_shape=problem.design_space.shape,\n", + " ).to(device)\n", + " optimizer = th.optim.Adam(model.parameters(), lr=1e-3)\n", + " criterion = nn.MSELoss()\n", + "\n", + " def sample_noise(batch: int) -> th.Tensor:\n", + " return th.randn((batch, latent_dim), device=device, dtype=th.float32)\n", + "\n", + " ds = TensorDataset(th.tensor(conds_np), th.tensor(targets_np))\n", + " dl = DataLoader(ds, batch_size=batch_size, shuffle=True)\n", + "\n", + " for epoch in range(epochs):\n", + " model.train()\n", + " epoch_loss = 0.0\n", + " for cond_batch, target_batch in dl:\n", + " cond_batch = cond_batch.to(device)\n", + " target_batch = target_batch.to(device)\n", + "\n", + " pred = model(sample_noise(cond_batch.shape[0]), cond_batch)\n", + " loss = criterion(pred, target_batch)\n", + "\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + " epoch_loss += float(loss.item())\n", + " print(f'bootstrap epoch {epoch + 1:02d}/{epochs} - loss: {epoch_loss / len(dl):.4f}')\n", + "\n", + " sample_count = min(n_samples, len(test_ds))\n", + " selected = rng.choice(len(test_ds), size=sample_count, replace=False)\n", + " test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", + " baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", + "\n", + " model.eval()\n", + " with th.no_grad():\n", + " tanh_out = model(sample_noise(sample_count), th.tensor(test_conds, device=device))\n", + " gen_designs_t = ((tanh_out.clamp(-1.0, 1.0) + 1.0) / 2.0).clamp(0.0, 1.0)\n", + " gen_designs = gen_designs_t.detach().cpu().numpy().astype(np.float32)\n", + "\n", + " conditions_records = []\n", + " for i in range(sample_count):\n", + " rec = {}\n", + " for j, k in enumerate(condition_keys):\n", + " v = test_conds[i, j]\n", + " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", + " conditions_records.append(rec)\n", + "\n", + " artifact_dir.mkdir(parents=True, exist_ok=True)\n", + " np.save(artifact_dir / 'generated_designs.npy', gen_designs)\n", + " np.save(artifact_dir / 'baseline_designs.npy', baseline_designs)\n", + " with open(artifact_dir / 'conditions.json', 'w', encoding='utf-8') as f:\n", + " json.dump(conditions_records, f, indent=2)\n", + "\n", + " th.save(\n", + " {\n", + " 'model': model.state_dict(),\n", + " 'condition_keys': condition_keys,\n", + " 'latent_dim': latent_dim,\n", + " 'model_family': 'engiopt.cgan_2d.Generator',\n", + " },\n", + " artifact_dir / 'engiopt_cgan2d_generator_supervised.pt',\n", + " )\n", + "\n", + " print('Built artifacts at', artifact_dir)\n", + "\n", + "\n", "ARTIFACT_DIR = resolve_artifact_dir(create=True)\n", "required = [\n", " ARTIFACT_DIR / 'generated_designs.npy',\n", @@ -85,11 +201,17 @@ " run.finish()\n", " print('Downloaded artifacts from W&B to', ARTIFACT_DIR)\n", " except Exception as exc:\n", - " raise FileNotFoundError(\n", - " 'Artifacts missing locally and W&B download failed. '\n", - " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", - " f'Details: {exc}'\n", - " ) from exc\n", + " if AUTO_BUILD_ARTIFACTS_IF_MISSING:\n", + " print('W&B download failed; switching to local artifact build:', exc)\n", + " build_artifacts_locally(ARTIFACT_DIR)\n", + " else:\n", + " raise FileNotFoundError(\n", + " 'Artifacts missing locally and W&B download failed. '\n", + " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", + " f'Details: {exc}'\n", + " ) from exc\n", + " elif AUTO_BUILD_ARTIFACTS_IF_MISSING:\n", + " build_artifacts_locally(ARTIFACT_DIR)\n", " else:\n", " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", " raise FileNotFoundError(\n", From c569f281abf3e941a675b77f334b9e75ff4c1fee Mon Sep 17 00:00:00 2001 From: Soheyl Date: Fri, 6 Mar 2026 11:12:33 +0100 Subject: [PATCH 17/37] Deepen DCC26 notebooks and add copy-safe Colab flow --- workshops/dcc26/README.md | 26 +- .../participant/00_setup_api_warmup.ipynb | 8 + .../dcc26/participant/01_train_generate.ipynb | 152 +++++++----- .../participant/02_evaluate_metrics.ipynb | 183 ++++++++++++-- .../03_add_new_problem_scaffold.ipynb | 8 + .../dcc26/solutions/00_setup_api_warmup.ipynb | 8 + .../dcc26/solutions/01_train_generate.ipynb | 198 ++++++++++++--- .../dcc26/solutions/02_evaluate_metrics.ipynb | 234 ++++++++++++++++-- .../03_add_new_problem_scaffold.ipynb | 8 + 9 files changed, 677 insertions(+), 148 deletions(-) diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index fc08f30..73a161c 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -48,16 +48,16 @@ All notebooks now include a conditional dependency bootstrap cell: ## Open in Colab -Pre-merge (current branch) links: +Use these `#copy=true` links for workshop sharing so attendees are prompted to create their own Drive copy first. -- Participant 00: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/00_setup_api_warmup.ipynb -- Participant 01: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/01_train_generate.ipynb -- Participant 02: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/02_evaluate_metrics.ipynb -- Participant 03: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb -- Solution 00: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/00_setup_api_warmup.ipynb -- Solution 01: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/01_train_generate.ipynb -- Solution 02: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/02_evaluate_metrics.ipynb -- Solution 03: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +- Participant 00: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/00_setup_api_warmup.ipynb#copy=true +- Participant 01: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/01_train_generate.ipynb#copy=true +- Participant 02: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/02_evaluate_metrics.ipynb#copy=true +- Participant 03: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb#copy=true +- Solution 00: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/00_setup_api_warmup.ipynb#copy=true +- Solution 01: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/01_train_generate.ipynb#copy=true +- Solution 02: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/02_evaluate_metrics.ipynb#copy=true +- Solution 03: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb#copy=true ## Output artifacts @@ -69,7 +69,9 @@ By default, solution notebooks write generated artifacts to: Optional: - You can enable W&B artifact upload/download in Notebook 01/02 by setting `USE_WANDB_ARTIFACTS = True`. -- W&B is disabled by default so participants can run without account setup. +- Notebook 01 logs training dynamics (`train/loss`) and can upload checkpoint/history/plots as artifact payload. +- Notebook 02 can log evaluation metrics, tables, and figures to W&B. +- W&B is disabled by default so participants can run without account setup or API keys. - Notebook 02 auto-builds Notebook 01-style artifacts locally with EngiOpt if they are missing (`AUTO_BUILD_ARTIFACTS_IF_MISSING = True`). These include: @@ -77,8 +79,12 @@ These include: - `generated_designs.npy` - `baseline_designs.npy` - `conditions.json` +- `engiopt_cgan2d_generator_supervised.pt` +- `training_history.csv` +- `training_curve.png` - `metrics_summary.csv` - `objective_histogram.png` +- `objective_scatter.png` - `design_grid.png` ## Facilitator fallback policy diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index f975f94..57f343c 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -5,6 +5,14 @@ "metadata": {}, "source": "# Notebook 00 (Participant): Setup + API Warmup\n\nComplete the `TODO` sections.\n\nGoal: use `Beams2D` API to inspect problem metadata, sample data, rendering, and constraints.\n" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", + "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 71aa537..8df6772 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -4,13 +4,23 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Notebook 01 (Participant): Train + Generate with EngiOpt CGAN-2D\n" + "# Notebook 01 (Participant): Train + Generate with EngiOpt CGAN-2D\n", + "\n", + "Goal: implement the core pipeline to produce reproducible artifacts for Notebook 02.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", + "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", @@ -32,10 +42,20 @@ " print('Skipping install (using current environment).')\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part A: Setup\n", + "\n", + "You can complete this notebook without W&B.\n", + "If you want remote artifacts, set `USE_WANDB_ARTIFACTS=True` and log in with `wandb.login()`.\n" + ] + }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "import json\n", @@ -45,6 +65,7 @@ "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", + "import pandas as pd\n", "import torch as th\n", "import torch.nn as nn\n", "from torch.utils.data import DataLoader, TensorDataset\n", @@ -58,22 +79,17 @@ " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", " ) from exc\n", "\n", - "\n", - "# Optional W&B artifact flow (disabled by default)\n", "USE_WANDB_ARTIFACTS = False\n", "WANDB_PROJECT = 'dcc26-workshop'\n", "WANDB_ENTITY = None\n", "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", "WANDB_ARTIFACT_ALIAS = 'latest'\n", + "WANDB_LOG_TRAINING = True\n", "\n", "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", " in_colab = 'google.colab' in sys.modules\n", - " if in_colab:\n", - " path = Path('/content/dcc26_artifacts')\n", - " else:\n", - " path = Path('workshops/dcc26/artifacts')\n", - "\n", + " path = Path('/content/dcc26_artifacts') if in_colab else Path('workshops/dcc26/artifacts')\n", " if create:\n", " path.mkdir(parents=True, exist_ok=True)\n", " return path\n", @@ -93,13 +109,15 @@ "print('artifact dir:', ARTIFACT_DIR)\n", "\n", "CKPT_PATH = ARTIFACT_DIR / 'engiopt_cgan2d_generator_supervised.pt'\n", + "HISTORY_PATH = ARTIFACT_DIR / 'training_history.csv'\n", + "TRAIN_CURVE_PATH = ARTIFACT_DIR / 'training_curve.png'\n", "LATENT_DIM = 32\n" ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "problem = Beams2D(seed=SEED)\n", @@ -109,14 +127,11 @@ "condition_keys = problem.conditions_keys\n", "print('condition keys:', condition_keys)\n", "\n", - "# Build compact train subset to keep runtime stable in workshop\n", "N_TRAIN = 512\n", "subset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n", "\n", "conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", "designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", - "\n", - "# EngiOpt CGAN generator emits tanh-scaled outputs in [-1, 1]\n", "targets_np = (designs_np * 2.0) - 1.0\n", "\n", "print('conditions shape:', conds_np.shape)\n", @@ -126,95 +141,108 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ - "# TODO 1: instantiate EngiOpt CGAN-2D generator + optimizer/loss\n", - "# Required class is already imported as EngiOptCGAN2DGenerator\n", - "#\n", - "# Suggested:\n", - "# model = EngiOptCGAN2DGenerator(latent_dim=LATENT_DIM, n_conds=conds_np.shape[1], design_shape=problem.design_space.shape).to(DEVICE)\n", - "# optimizer = th.optim.Adam(model.parameters(), lr=1e-3)\n", - "# criterion = nn.MSELoss()\n", - "#\n", - "# def sample_noise(batch_size: int) -> th.Tensor:\n", - "# return th.randn((batch_size, LATENT_DIM), device=DEVICE, dtype=th.float32)\n", - "\n", - "raise NotImplementedError('Complete TODO 1 with the EngiOpt model setup')\n" + "# TODO 1: Instantiate model, optimizer, loss, and noise sampler.\n", + "# Required objects:\n", + "# - model (EngiOptCGAN2DGenerator)\n", + "# - optimizer (Adam)\n", + "# - criterion (MSELoss)\n", + "# - sample_noise(batch_size)\n", + "\n", + "raise NotImplementedError('Complete TODO 1 model setup')\n" ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "TRAIN_FROM_SCRATCH = True\n", "EPOCHS = 8\n", "BATCH_SIZE = 64\n", "\n", - "if TRAIN_FROM_SCRATCH:\n", - " # TODO 2: implement lightweight supervised training loop for EngiOpt generator\n", - " # - dataset tensors: conds_np -> input conditions, targets_np -> tanh-scaled targets in [-1,1]\n", - " # - sample latent noise each batch with sample_noise(...)\n", - " # - loss = criterion(pred, target_batch)\n", - " # - optimizer step\n", - " # - save checkpoint dict with keys: model, condition_keys, latent_dim, model_family\n", - " raise NotImplementedError('Complete TODO 2 training loop')\n", - "elif CKPT_PATH.exists():\n", - " ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n", - " model.load_state_dict(ckpt['model'])\n", - " model.eval()\n", - " print('loaded checkpoint from', CKPT_PATH)\n", - "else:\n", - " raise FileNotFoundError(f'No checkpoint found at {CKPT_PATH}. Set TRAIN_FROM_SCRATCH=True or provide a checkpoint.')\n" + "# TODO 2: Train or load checkpoint.\n", + "# Requirements:\n", + "# - if TRAIN_FROM_SCRATCH:\n", + "# 1) create DataLoader from (conds_np, targets_np)\n", + "# 2) run epoch loop and optimize reconstruction loss\n", + "# 3) collect train_losses list\n", + "# 4) save checkpoint to CKPT_PATH\n", + "# 5) save training history CSV to HISTORY_PATH\n", + "# 6) save training curve figure to TRAIN_CURVE_PATH\n", + "# - elif CKPT_PATH exists: load it\n", + "# - else: raise FileNotFoundError\n", + "\n", + "raise NotImplementedError('Complete TODO 2 training/loading')\n" ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ - "# TODO 3: implement generation path with EngiOpt generator\n", - "# - sample N_SAMPLES test conditions and baseline designs\n", + "# TODO 3: Generate designs and prepare condition records.\n", + "# Requirements:\n", + "# - sample N_SAMPLES from test dataset\n", "# - run model(sample_noise(...), condition_tensor)\n", - "# - map tanh outputs to design space with ((x + 1)/2).clamp(0, 1)\n", - "# - set gen_designs, baseline_designs, test_conds\n", + "# - map tanh output back to [0, 1]\n", + "# - create:\n", + "# gen_designs, baseline_designs, test_conds, conditions_records\n", "\n", - "raise NotImplementedError('Complete TODO 3 generation path')\n" + "raise NotImplementedError('Complete TODO 3 generation')\n" ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ - "# TODO 5: serialize artifacts for Notebook 02\n", + "# TODO 4: Save Notebook 02 artifacts and (optionally) W&B artifact.\n", "# Required files:\n", - "# - ARTIFACT_DIR / 'generated_designs.npy'\n", - "# - ARTIFACT_DIR / 'baseline_designs.npy'\n", - "# - ARTIFACT_DIR / 'conditions.json'\n", - "#\n", - "# Optional (advanced): if USE_WANDB_ARTIFACTS is True,\n", - "# upload these files as a W&B artifact named WANDB_ARTIFACT_NAME.\n", - "\n", - "raise NotImplementedError('Complete TODO 5 artifact export')\n" + "# - generated_designs.npy\n", + "# - baseline_designs.npy\n", + "# - conditions.json\n", + "# Recommended extras:\n", + "# - checkpoint (.pt), training_history.csv, training_curve.png\n", + "\n", + "raise NotImplementedError('Complete TODO 4 artifact export')\n" ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], - "source": "# Quick visual check of generated designs\nfig, axes = plt.subplots(2, 4, figsize=(12, 6))\nfor i, ax in enumerate(axes.ravel()):\n ax.imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n ax.axis('off')\n ax.set_title(f'gen {i}')\nplt.tight_layout()\nplt.show()" + "source": [ + "# Quick visual side-by-side snapshot\n", + "fig, axes = plt.subplots(2, 6, figsize=(14, 5))\n", + "for i in range(6):\n", + " axes[0, i].imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n", + " axes[0, i].set_title(f'gen {i}')\n", + " axes[0, i].axis('off')\n", + "\n", + " axes[1, i].imshow(baseline_designs[i], cmap='gray', vmin=0, vmax=1)\n", + " axes[1, i].set_title(f'base {i}')\n", + " axes[1, i].axis('off')\n", + "\n", + "fig.tight_layout()\n", + "plt.show()\n" + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Next\n\nContinue with **Notebook 02** to validate and evaluate generated designs against baselines.\n" + "source": [ + "## Next\n", + "\n", + "Continue with **Notebook 02** to run physics evaluation and benchmark metrics.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index c647d77..c6697fe 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -3,12 +3,24 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Notebook 02 (Participant): Evaluation + Metrics\n\nComplete the `TODO` sections to evaluate generated designs and export metrics.\n" + "source": [ + "# Notebook 02 (Participant): Evaluation + Metrics\n", + "\n", + "Goal: implement evaluation logic and interpret benchmark outcomes beyond objective value.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", + "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", @@ -30,10 +42,21 @@ " print('Skipping install (using current environment).')\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Artifact loading\n", + "\n", + "This notebook can run standalone:\n", + "- if Notebook 01 artifacts exist, it loads them,\n", + "- otherwise it can auto-build artifacts locally using EngiOpt.\n" + ] + }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "import json\n", @@ -57,24 +80,19 @@ " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", " ) from exc\n", "\n", - "# Optional W&B artifact flow (disabled by default)\n", "USE_WANDB_ARTIFACTS = False\n", "WANDB_PROJECT = 'dcc26-workshop'\n", "WANDB_ENTITY = None\n", "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", - "# Local self-heal path (enabled by default)\n", + "# Self-heal path for workshop robustness\n", "AUTO_BUILD_ARTIFACTS_IF_MISSING = True\n", "\n", "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", " in_colab = 'google.colab' in sys.modules\n", - " if in_colab:\n", - " path = Path('/content/dcc26_artifacts')\n", - " else:\n", - " path = Path('workshops/dcc26/artifacts')\n", - "\n", + " path = Path('/content/dcc26_artifacts') if in_colab else Path('workshops/dcc26/artifacts')\n", " if create:\n", " path.mkdir(parents=True, exist_ok=True)\n", " return path\n", @@ -125,6 +143,7 @@ " ds = TensorDataset(th.tensor(conds_np), th.tensor(targets_np))\n", " dl = DataLoader(ds, batch_size=batch_size, shuffle=True)\n", "\n", + " train_losses = []\n", " for epoch in range(epochs):\n", " model.train()\n", " epoch_loss = 0.0\n", @@ -139,7 +158,10 @@ " loss.backward()\n", " optimizer.step()\n", " epoch_loss += float(loss.item())\n", - " print(f'bootstrap epoch {epoch + 1:02d}/{epochs} - loss: {epoch_loss / len(dl):.4f}')\n", + "\n", + " epoch_avg = epoch_loss / len(dl)\n", + " train_losses.append(epoch_avg)\n", + " print(f'bootstrap epoch {epoch + 1:02d}/{epochs} - loss: {epoch_avg:.4f}')\n", "\n", " sample_count = min(n_samples, len(test_ds))\n", " selected = rng.choice(len(test_ds), size=sample_count, replace=False)\n", @@ -166,6 +188,10 @@ " with open(artifact_dir / 'conditions.json', 'w', encoding='utf-8') as f:\n", " json.dump(conditions_records, f, indent=2)\n", "\n", + " pd.DataFrame({'epoch': np.arange(1, len(train_losses) + 1), 'train_loss': train_losses}).to_csv(\n", + " artifact_dir / 'training_history.csv', index=False\n", + " )\n", + "\n", " th.save(\n", " {\n", " 'model': model.state_dict(),\n", @@ -216,7 +242,7 @@ " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", " raise FileNotFoundError(\n", " 'Notebook 01 artifacts not found. Run Notebook 01 first (including export cell), '\n", - " 'or enable USE_WANDB_ARTIFACTS to fetch from W&B. Missing files:\\n' + missing\n", + " 'or enable USE_WANDB_ARTIFACTS to fetch from W&B. Missing files:\\\\n' + missing\n", " )\n", "\n", "print('using artifact dir:', ARTIFACT_DIR)\n", @@ -233,36 +259,151 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], - "source": "problem = Beams2D(seed=7)\n\n# TODO 1: implement per-sample evaluation loop\n# For each (generated, baseline, config):\n# - check constraints for generated and baseline\n# - run problem.simulate for each\n# - append dict rows to `rows` with:\n# sample, gen_obj, base_obj, gen_minus_base, gen_violations, base_violations\n\nrows = []\nraise NotImplementedError('Complete TODO 1 evaluation loop')\n\nresults = pd.DataFrame(rows)\nresults.head()" + "source": [ + "problem = Beams2D(seed=7)\n", + "\n", + "# TODO 1: Implement the per-sample evaluation loop.\n", + "# For each (generated, baseline, config):\n", + "# - g_viol = problem.check_constraints(design=g, config=cfg)\n", + "# - b_viol = problem.check_constraints(design=b, config=cfg)\n", + "# - reset simulator before each objective call\n", + "# - append dict with: sample, gen_obj, base_obj, gen_minus_base, gen_violations, base_violations\n", + "\n", + "rows = []\n", + "raise NotImplementedError('Complete TODO 1 evaluation loop')\n", + "\n", + "results = pd.DataFrame(rows)\n", + "results.head()\n" + ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], - "source": "# TODO 2: implement summary metrics\n# - n_samples\n# - gen_obj_mean\n# - base_obj_mean\n# - improvement_rate\n# - gen_violation_ratio\n# - base_violation_ratio\n# - gen_diversity_l2 (use helper below)\n\ndef mean_pairwise_l2(designs: np.ndarray) -> float:\n flat = designs.reshape(designs.shape[0], -1)\n n = flat.shape[0]\n if n < 2:\n return 0.0\n dists = []\n for i in range(n):\n for j in range(i + 1, n):\n dists.append(float(np.linalg.norm(flat[i] - flat[j])))\n return float(np.mean(dists))\n\nraise NotImplementedError('Complete TODO 2 summary metrics')" + "source": [ + "# TODO 2: Implement summary metrics.\n", + "# Suggested metrics:\n", + "# - n_samples\n", + "# - gen_obj_mean / base_obj_mean\n", + "# - objective_gap_mean\n", + "# - improvement_rate\n", + "# - gen_violation_ratio / base_violation_ratio\n", + "# - gen_feasible_rate\n", + "# - gen_diversity_l2\n", + "# - gen_novelty_to_train_l2\n", + "\n", + "def mean_pairwise_l2(designs: np.ndarray) -> float:\n", + " flat = designs.reshape(designs.shape[0], -1)\n", + " n = flat.shape[0]\n", + " if n < 2:\n", + " return 0.0\n", + " dists = []\n", + " for i in range(n):\n", + " for j in range(i + 1, n):\n", + " dists.append(float(np.linalg.norm(flat[i] - flat[j])))\n", + " return float(np.mean(dists))\n", + "\n", + "\n", + "def mean_nn_distance_to_reference(designs: np.ndarray, reference_designs: np.ndarray) -> float:\n", + " q = designs.reshape(designs.shape[0], -1)\n", + " r = reference_designs.reshape(reference_designs.shape[0], -1)\n", + " nn_dists = []\n", + " for i in range(q.shape[0]):\n", + " d = np.linalg.norm(r - q[i][None, :], axis=1)\n", + " nn_dists.append(float(np.min(d)))\n", + " return float(np.mean(nn_dists))\n", + "\n", + "raise NotImplementedError('Complete TODO 2 summary metrics')\n" + ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], - "source": "results.to_csv(ARTIFACT_DIR / 'per_sample_metrics.csv', index=False)\nsummary_df.to_csv(ARTIFACT_DIR / 'metrics_summary.csv', index=False)\n\nfig, ax = plt.subplots(figsize=(7, 4))\nax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\nax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\nax.set_xlabel('Compliance objective (lower is better)')\nax.set_ylabel('Count')\nax.set_title('Generated vs baseline objective distribution')\nax.legend()\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'objective_histogram.png', dpi=150)\nplt.show()\n\nprint('Saved:')\nprint('-', ARTIFACT_DIR / 'per_sample_metrics.csv')\nprint('-', ARTIFACT_DIR / 'metrics_summary.csv')\nprint('-', ARTIFACT_DIR / 'objective_histogram.png')" + "source": [ + "# Export metrics and figures\n", + "results_path = ARTIFACT_DIR / 'per_sample_metrics.csv'\n", + "summary_path = ARTIFACT_DIR / 'metrics_summary.csv'\n", + "hist_path = ARTIFACT_DIR / 'objective_histogram.png'\n", + "grid_path = ARTIFACT_DIR / 'design_grid.png'\n", + "scatter_path = ARTIFACT_DIR / 'objective_scatter.png'\n", + "\n", + "results.to_csv(results_path, index=False)\n", + "summary_df.to_csv(summary_path, index=False)\n", + "\n", + "fig, ax = plt.subplots(figsize=(7, 4))\n", + "ax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\n", + "ax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\n", + "ax.set_xlabel('Compliance objective (lower is better)')\n", + "ax.set_ylabel('Count')\n", + "ax.set_title('Generated vs baseline objective distribution')\n", + "ax.legend()\n", + "fig.tight_layout()\n", + "fig.savefig(hist_path, dpi=150)\n", + "plt.show()\n", + "\n", + "fig2, ax2 = plt.subplots(figsize=(5, 5))\n", + "ax2.scatter(results['base_obj'], results['gen_obj'], alpha=0.8)\n", + "min_v = min(results['base_obj'].min(), results['gen_obj'].min())\n", + "max_v = max(results['base_obj'].max(), results['gen_obj'].max())\n", + "ax2.plot([min_v, max_v], [min_v, max_v], '--', color='black', linewidth=1)\n", + "ax2.set_xlabel('Baseline objective')\n", + "ax2.set_ylabel('Generated objective')\n", + "ax2.set_title('Per-sample objective comparison')\n", + "fig2.tight_layout()\n", + "fig2.savefig(scatter_path, dpi=150)\n", + "plt.show()\n", + "\n", + "print('Saved:')\n", + "print('-', results_path)\n", + "print('-', summary_path)\n", + "print('-', hist_path)\n", + "print('-', scatter_path)\n", + "\n", + "# Optional advanced extension:\n", + "# if USE_WANDB_ARTIFACTS:\n", + "# log summary metrics, tables, and images to W&B.\n" + ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], - "source": "# Visual side-by-side sample grid\nfig, axes = plt.subplots(3, 4, figsize=(12, 8))\nfor i, ax in enumerate(axes.ravel()):\n if i >= 12:\n break\n pair_idx = i // 2\n if i % 2 == 0:\n ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'gen {pair_idx}')\n else:\n ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'base {pair_idx}')\n ax.axis('off')\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'design_grid.png', dpi=150)\nplt.show()" + "source": [ + "# Visual side-by-side sample grid\n", + "fig, axes = plt.subplots(3, 4, figsize=(12, 8))\n", + "for i, ax in enumerate(axes.ravel()):\n", + " if i >= 12:\n", + " break\n", + " pair_idx = i // 2\n", + " if i % 2 == 0:\n", + " ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n", + " ax.set_title(f'gen {pair_idx}')\n", + " else:\n", + " ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n", + " ax.set_title(f'base {pair_idx}')\n", + " ax.axis('off')\n", + "fig.tight_layout()\n", + "fig.savefig(grid_path, dpi=150)\n", + "plt.show()\n" + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Interpretation hints\n\n- `improvement_rate` shows how often generated designs beat baselines on objective value.\n- `gen_violation_ratio` tracks practical feasibility pressure from constraints.\n- `gen_diversity_l2` is a simple diversity proxy across generated designs.\n" + "source": [ + "## Interpretation hints\n", + "\n", + "- Strong objective performance with poor feasibility is not deployment-ready.\n", + "- Diversity + novelty are useful for discussing benchmark completeness.\n", + "- Compare your summary metrics with peers during the breakout discussion.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 94a2544..518fc41 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -5,6 +5,14 @@ "metadata": {}, "source": "# Notebook 03 (Participant): Add a New Problem Scaffold\n\nComplete the `TODO` sections to build a minimal EngiBench-compatible toy problem.\n" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", + "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index 17c1681..4e50298 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -5,6 +5,14 @@ "metadata": {}, "source": "# Notebook 00: Setup + API Warmup (DCC26)\n\nThis notebook covers the first workshop segment (10-15 minutes):\n\n1. Environment checks\n2. EngiBench problem instantiation (`Beams2D`)\n3. Design space / objectives / conditions inspection\n4. Dataset sample inspection\n5. Rendering and one explicit constraint check\n\nExpected outcome: participants can navigate the full EngiBench problem API without W&B setup.\n" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", + "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 9efa9b6..9721042 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -4,13 +4,27 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Notebook 01: Train + Generate with EngiOpt CGAN-2D (DCC26)\n" + "# Notebook 01: Train + Generate with EngiOpt CGAN-2D (DCC26)\n", + "\n", + "This notebook maps to the **00:30-01:00** workshop block:\n", + "1. Load an EngiBench problem/dataset (`Beams2D`)\n", + "2. Train an EngiOpt generator on a compact subset\n", + "3. Generate condition-driven designs\n", + "4. Export reproducible artifacts for Notebook 02\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", + "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", @@ -32,10 +46,20 @@ " print('Skipping install (using current environment).')\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part A: Configuration and Runtime Controls\n", + "\n", + "- `USE_WANDB_ARTIFACTS=False` keeps the default path account-free.\n", + "- Set it to `True` if you want to test artifact upload and training logs.\n" + ] + }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "import json\n", @@ -45,6 +69,7 @@ "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", + "import pandas as pd\n", "import torch as th\n", "import torch.nn as nn\n", "from torch.utils.data import DataLoader, TensorDataset\n", @@ -58,22 +83,18 @@ " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", " ) from exc\n", "\n", - "\n", - "# Optional W&B artifact flow (disabled by default)\n", + "# Optional W&B integration\n", "USE_WANDB_ARTIFACTS = False\n", "WANDB_PROJECT = 'dcc26-workshop'\n", "WANDB_ENTITY = None\n", "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", "WANDB_ARTIFACT_ALIAS = 'latest'\n", + "WANDB_LOG_TRAINING = True\n", "\n", "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", " in_colab = 'google.colab' in sys.modules\n", - " if in_colab:\n", - " path = Path('/content/dcc26_artifacts')\n", - " else:\n", - " path = Path('workshops/dcc26/artifacts')\n", - "\n", + " path = Path('/content/dcc26_artifacts') if in_colab else Path('workshops/dcc26/artifacts')\n", " if create:\n", " path.mkdir(parents=True, exist_ok=True)\n", " return path\n", @@ -93,13 +114,24 @@ "print('artifact dir:', ARTIFACT_DIR)\n", "\n", "CKPT_PATH = ARTIFACT_DIR / 'engiopt_cgan2d_generator_supervised.pt'\n", + "HISTORY_PATH = ARTIFACT_DIR / 'training_history.csv'\n", + "TRAIN_CURVE_PATH = ARTIFACT_DIR / 'training_curve.png'\n", "LATENT_DIM = 32\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part B: Load Beams2D data and build a stable workshop subset\n", + "\n", + "We intentionally use a compact subset (`N_TRAIN=512`) so participants can finish on free Colab runtimes.\n" + ] + }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "problem = Beams2D(seed=SEED)\n", @@ -109,14 +141,13 @@ "condition_keys = problem.conditions_keys\n", "print('condition keys:', condition_keys)\n", "\n", - "# Build compact train subset to keep runtime stable in workshop\n", "N_TRAIN = 512\n", "subset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n", "\n", "conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", "designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", "\n", - "# EngiOpt CGAN generator emits tanh-scaled outputs in [-1, 1]\n", + "# EngiOpt CGAN generator emits tanh-scaled outputs in [-1, 1].\n", "targets_np = (designs_np * 2.0) - 1.0\n", "\n", "print('conditions shape:', conds_np.shape)\n", @@ -126,8 +157,8 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "model = EngiOptCGAN2DGenerator(\n", @@ -146,21 +177,43 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "TRAIN_FROM_SCRATCH = True\n", "EPOCHS = 8\n", "BATCH_SIZE = 64\n", "\n", + "train_losses = []\n", + "wandb_train_run = None\n", + "\n", "if TRAIN_FROM_SCRATCH:\n", " ds = TensorDataset(th.tensor(conds_np), th.tensor(targets_np))\n", " dl = DataLoader(ds, batch_size=BATCH_SIZE, shuffle=True)\n", "\n", + " if USE_WANDB_ARTIFACTS and WANDB_LOG_TRAINING:\n", + " import wandb\n", + "\n", + " wandb_train_run = wandb.init(\n", + " project=WANDB_PROJECT,\n", + " entity=WANDB_ENTITY,\n", + " job_type='train',\n", + " config={\n", + " 'seed': SEED,\n", + " 'epochs': EPOCHS,\n", + " 'batch_size': BATCH_SIZE,\n", + " 'n_train': int(N_TRAIN),\n", + " 'latent_dim': LATENT_DIM,\n", + " 'model_family': 'engiopt.cgan_2d.Generator',\n", + " },\n", + " reinit=True,\n", + " )\n", + "\n", " for epoch in range(EPOCHS):\n", " model.train()\n", " epoch_loss = 0.0\n", + "\n", " for cond_batch, target_batch in dl:\n", " cond_batch = cond_batch.to(DEVICE)\n", " target_batch = target_batch.to(DEVICE)\n", @@ -173,7 +226,12 @@ " optimizer.step()\n", " epoch_loss += float(loss.item())\n", "\n", - " print(f'epoch {epoch + 1:02d}/{EPOCHS} - loss: {epoch_loss / len(dl):.4f}')\n", + " epoch_avg = epoch_loss / len(dl)\n", + " train_losses.append(epoch_avg)\n", + " print(f'epoch {epoch + 1:02d}/{EPOCHS} - loss: {epoch_avg:.4f}')\n", + "\n", + " if wandb_train_run is not None:\n", + " wandb_train_run.log({'train/loss': epoch_avg, 'epoch': epoch + 1})\n", "\n", " th.save(\n", " {\n", @@ -185,19 +243,52 @@ " CKPT_PATH,\n", " )\n", " print('saved checkpoint to', CKPT_PATH)\n", + "\n", + " history_df = pd.DataFrame({'epoch': np.arange(1, len(train_losses) + 1), 'train_loss': train_losses})\n", + " history_df.to_csv(HISTORY_PATH, index=False)\n", + "\n", + " fig, ax = plt.subplots(figsize=(6, 3.5))\n", + " ax.plot(history_df['epoch'], history_df['train_loss'], marker='o')\n", + " ax.set_xlabel('Epoch')\n", + " ax.set_ylabel('MSE loss')\n", + " ax.set_title('Notebook 01 training curve')\n", + " ax.grid(alpha=0.3)\n", + " fig.tight_layout()\n", + " fig.savefig(TRAIN_CURVE_PATH, dpi=150)\n", + " plt.show()\n", + "\n", + " if wandb_train_run is not None:\n", + " import wandb\n", + "\n", + " wandb_train_run.log({'train/loss_curve': wandb.Image(str(TRAIN_CURVE_PATH))})\n", + " wandb_train_run.finish()\n", "elif CKPT_PATH.exists():\n", " ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n", " model.load_state_dict(ckpt['model'])\n", " model.eval()\n", " print('loaded checkpoint from', CKPT_PATH)\n", + "\n", + " if HISTORY_PATH.exists():\n", + " history_df = pd.read_csv(HISTORY_PATH)\n", + " else:\n", + " history_df = pd.DataFrame(columns=['epoch', 'train_loss'])\n", "else:\n", " raise FileNotFoundError(f'No checkpoint found at {CKPT_PATH}. Set TRAIN_FROM_SCRATCH=True or provide a checkpoint.')\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part C: Condition-driven generation and quick sanity checks\n", + "\n", + "We generate `N_SAMPLES` designs from the test condition set and report quick feasibility/objective diagnostics.\n" + ] + }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "rng = np.random.default_rng(SEED)\n", @@ -213,16 +304,6 @@ " gen_designs_t = ((tanh_out.clamp(-1.0, 1.0) + 1.0) / 2.0).clamp(0.0, 1.0)\n", "gen_designs = gen_designs_t.detach().cpu().numpy().astype(np.float32)\n", "\n", - "print('generated shape:', gen_designs.shape)\n", - "print('baseline shape:', baseline_designs.shape)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ "conditions_records = []\n", "for i in range(N_SAMPLES):\n", " rec = {}\n", @@ -231,6 +312,22 @@ " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", " conditions_records.append(rec)\n", "\n", + "# Quick checks before full Notebook 02 evaluation\n", + "viol_ratio = np.mean([\n", + " len(problem.check_constraints(design=d, config=cfg)) > 0\n", + " for d, cfg in zip(gen_designs, conditions_records, strict=True)\n", + "])\n", + "print('generated shape:', gen_designs.shape)\n", + "print('baseline shape:', baseline_designs.shape)\n", + "print('generated violation ratio (quick check):', float(viol_ratio))\n" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": [ "generated_path = ARTIFACT_DIR / 'generated_designs.npy'\n", "baseline_path = ARTIFACT_DIR / 'baseline_designs.npy'\n", "conditions_path = ARTIFACT_DIR / 'conditions.json'\n", @@ -241,16 +338,31 @@ " json.dump(conditions_records, f, indent=2)\n", "\n", "print('Saved artifacts to', ARTIFACT_DIR)\n", + "print('-', generated_path)\n", + "print('-', baseline_path)\n", + "print('-', conditions_path)\n", + "print('-', CKPT_PATH)\n", + "print('-', HISTORY_PATH)\n", + "print('-', TRAIN_CURVE_PATH)\n", "\n", "if USE_WANDB_ARTIFACTS:\n", " try:\n", " import wandb\n", "\n", " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='artifact-upload', reinit=True)\n", - " artifact = wandb.Artifact(WANDB_ARTIFACT_NAME, type='dataset', description='DCC26 Notebook 01 generated artifacts')\n", - " artifact.add_file(str(generated_path))\n", - " artifact.add_file(str(baseline_path))\n", - " artifact.add_file(str(conditions_path))\n", + " run.log({\n", + " 'artifact/generated_mean_density': float(gen_designs.mean()),\n", + " 'artifact/generated_std_density': float(gen_designs.std()),\n", + " })\n", + "\n", + " artifact = wandb.Artifact(\n", + " WANDB_ARTIFACT_NAME,\n", + " type='dataset',\n", + " description='DCC26 Notebook 01 artifacts: arrays, conditions, checkpoint, and training diagnostics.',\n", + " )\n", + " for path in [generated_path, baseline_path, conditions_path, CKPT_PATH, HISTORY_PATH, TRAIN_CURVE_PATH]:\n", + " if path.exists():\n", + " artifact.add_file(str(path))\n", " run.log_artifact(artifact, aliases=[WANDB_ARTIFACT_ALIAS])\n", " run.finish()\n", " print('Uploaded artifacts to W&B:', WANDB_ARTIFACT_NAME)\n", @@ -260,15 +372,33 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], - "source": "# Quick visual check of generated designs\nfig, axes = plt.subplots(2, 4, figsize=(12, 6))\nfor i, ax in enumerate(axes.ravel()):\n ax.imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n ax.axis('off')\n ax.set_title(f'gen {i}')\nplt.tight_layout()\nplt.show()" + "source": [ + "# Visual side-by-side snapshot (generated vs baseline)\n", + "fig, axes = plt.subplots(2, 6, figsize=(14, 5))\n", + "for i in range(6):\n", + " axes[0, i].imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n", + " axes[0, i].set_title(f'gen {i}')\n", + " axes[0, i].axis('off')\n", + "\n", + " axes[1, i].imshow(baseline_designs[i], cmap='gray', vmin=0, vmax=1)\n", + " axes[1, i].set_title(f'base {i}')\n", + " axes[1, i].axis('off')\n", + "\n", + "fig.tight_layout()\n", + "plt.show()\n" + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Next\n\nContinue with **Notebook 02** to validate and evaluate generated designs against baselines.\n" + "source": [ + "## Next\n", + "\n", + "Proceed to **Notebook 02** for full physics-based evaluation and metric reporting.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 12ae07c..3a2a0c6 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -3,12 +3,28 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Notebook 02: Evaluation + Metrics (DCC26)\n\nThis notebook covers the evaluation segment (20 minutes):\n\n1. Constraint checks for generated designs\n2. Physics-based simulation via `problem.simulate`\n3. Baseline comparison\n4. Export metrics and plots\n" + "source": [ + "# Notebook 02: Evaluation + Metrics (DCC26)\n", + "\n", + "This notebook maps to the **01:10-01:30** workshop block:\n", + "1. Load artifacts from Notebook 01 (or recover automatically)\n", + "2. Validate constraints and run physics simulation\n", + "3. Compute objective/feasibility/diversity/novelty metrics\n", + "4. Export CSV + figures for reporting\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", + "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", @@ -30,10 +46,22 @@ " print('Skipping install (using current environment).')\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Artifact Loading Strategy\n", + "\n", + "Priority order:\n", + "1. Local runtime artifacts (`/content/dcc26_artifacts`)\n", + "2. Optional W&B artifact download\n", + "3. Automatic local rebuild with EngiOpt (enabled by default)\n" + ] + }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], "source": [ "import json\n", @@ -57,24 +85,19 @@ " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", " ) from exc\n", "\n", - "# Optional W&B artifact flow (disabled by default)\n", "USE_WANDB_ARTIFACTS = False\n", "WANDB_PROJECT = 'dcc26-workshop'\n", "WANDB_ENTITY = None\n", "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", "WANDB_ARTIFACT_ALIAS = 'latest'\n", "\n", - "# Local self-heal path (enabled by default)\n", + "# Self-heal path for workshop robustness\n", "AUTO_BUILD_ARTIFACTS_IF_MISSING = True\n", "\n", "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", " in_colab = 'google.colab' in sys.modules\n", - " if in_colab:\n", - " path = Path('/content/dcc26_artifacts')\n", - " else:\n", - " path = Path('workshops/dcc26/artifacts')\n", - "\n", + " path = Path('/content/dcc26_artifacts') if in_colab else Path('workshops/dcc26/artifacts')\n", " if create:\n", " path.mkdir(parents=True, exist_ok=True)\n", " return path\n", @@ -125,6 +148,7 @@ " ds = TensorDataset(th.tensor(conds_np), th.tensor(targets_np))\n", " dl = DataLoader(ds, batch_size=batch_size, shuffle=True)\n", "\n", + " train_losses = []\n", " for epoch in range(epochs):\n", " model.train()\n", " epoch_loss = 0.0\n", @@ -139,7 +163,10 @@ " loss.backward()\n", " optimizer.step()\n", " epoch_loss += float(loss.item())\n", - " print(f'bootstrap epoch {epoch + 1:02d}/{epochs} - loss: {epoch_loss / len(dl):.4f}')\n", + "\n", + " epoch_avg = epoch_loss / len(dl)\n", + " train_losses.append(epoch_avg)\n", + " print(f'bootstrap epoch {epoch + 1:02d}/{epochs} - loss: {epoch_avg:.4f}')\n", "\n", " sample_count = min(n_samples, len(test_ds))\n", " selected = rng.choice(len(test_ds), size=sample_count, replace=False)\n", @@ -166,6 +193,10 @@ " with open(artifact_dir / 'conditions.json', 'w', encoding='utf-8') as f:\n", " json.dump(conditions_records, f, indent=2)\n", "\n", + " pd.DataFrame({'epoch': np.arange(1, len(train_losses) + 1), 'train_loss': train_losses}).to_csv(\n", + " artifact_dir / 'training_history.csv', index=False\n", + " )\n", + "\n", " th.save(\n", " {\n", " 'model': model.state_dict(),\n", @@ -216,7 +247,7 @@ " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", " raise FileNotFoundError(\n", " 'Notebook 01 artifacts not found. Run Notebook 01 first (including export cell), '\n", - " 'or enable USE_WANDB_ARTIFACTS to fetch from W&B. Missing files:\\n' + missing\n", + " 'or enable USE_WANDB_ARTIFACTS to fetch from W&B. Missing files:\\\\n' + missing\n", " )\n", "\n", "print('using artifact dir:', ARTIFACT_DIR)\n", @@ -231,38 +262,199 @@ "print('conditions:', len(conditions))\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Per-sample evaluation loop\n", + "\n", + "For each condition:\n", + "- run `check_constraints` for generated and baseline designs,\n", + "- run simulator objective for both,\n", + "- store a row for later aggregate analysis.\n" + ] + }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], - "source": "problem = Beams2D(seed=7)\n\nrows = []\nfor i, (g, b, cfg) in enumerate(zip(gen_designs, baseline_designs, conditions, strict=True)):\n g_viol = problem.check_constraints(design=g, config=cfg)\n b_viol = problem.check_constraints(design=b, config=cfg)\n\n # Reset before simulator calls for reproducibility\n problem.reset(seed=7)\n g_obj = float(problem.simulate(g, config=cfg)[0])\n problem.reset(seed=7)\n b_obj = float(problem.simulate(b, config=cfg)[0])\n\n rows.append({\n 'sample': i,\n 'gen_obj': g_obj,\n 'base_obj': b_obj,\n 'gen_minus_base': g_obj - b_obj,\n 'gen_violations': len(g_viol),\n 'base_violations': len(b_viol),\n })\n\nresults = pd.DataFrame(rows)\nresults.head()" + "source": [ + "problem = Beams2D(seed=7)\n", + "\n", + "rows = []\n", + "for i, (g, b, cfg) in enumerate(zip(gen_designs, baseline_designs, conditions, strict=True)):\n", + " g_viol = problem.check_constraints(design=g, config=cfg)\n", + " b_viol = problem.check_constraints(design=b, config=cfg)\n", + "\n", + " # Reset before simulator calls for reproducible comparisons.\n", + " problem.reset(seed=7)\n", + " g_obj = float(problem.simulate(g, config=cfg)[0])\n", + " problem.reset(seed=7)\n", + " b_obj = float(problem.simulate(b, config=cfg)[0])\n", + "\n", + " rows.append({\n", + " 'sample': i,\n", + " 'gen_obj': g_obj,\n", + " 'base_obj': b_obj,\n", + " 'gen_minus_base': g_obj - b_obj,\n", + " 'gen_violations': len(g_viol),\n", + " 'base_violations': len(b_viol),\n", + " })\n", + "\n", + "results = pd.DataFrame(rows)\n", + "results.head()\n" + ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], - "source": "def mean_pairwise_l2(designs: np.ndarray) -> float:\n flat = designs.reshape(designs.shape[0], -1)\n n = flat.shape[0]\n if n < 2:\n return 0.0\n dists = []\n for i in range(n):\n for j in range(i + 1, n):\n dists.append(float(np.linalg.norm(flat[i] - flat[j])))\n return float(np.mean(dists))\n\nsummary = {\n 'n_samples': int(len(results)),\n 'gen_obj_mean': float(results['gen_obj'].mean()),\n 'base_obj_mean': float(results['base_obj'].mean()),\n 'improvement_rate': float((results['gen_obj'] < results['base_obj']).mean()),\n 'gen_violation_ratio': float((results['gen_violations'] > 0).mean()),\n 'base_violation_ratio': float((results['base_violations'] > 0).mean()),\n 'gen_diversity_l2': mean_pairwise_l2(gen_designs),\n}\n\nsummary_df = pd.DataFrame([summary])\nsummary_df" + "source": [ + "def mean_pairwise_l2(designs: np.ndarray) -> float:\n", + " flat = designs.reshape(designs.shape[0], -1)\n", + " n = flat.shape[0]\n", + " if n < 2:\n", + " return 0.0\n", + " dists = []\n", + " for i in range(n):\n", + " for j in range(i + 1, n):\n", + " dists.append(float(np.linalg.norm(flat[i] - flat[j])))\n", + " return float(np.mean(dists))\n", + "\n", + "\n", + "def mean_nn_distance_to_reference(designs: np.ndarray, reference_designs: np.ndarray) -> float:\n", + " q = designs.reshape(designs.shape[0], -1)\n", + " r = reference_designs.reshape(reference_designs.shape[0], -1)\n", + " nn_dists = []\n", + " for i in range(q.shape[0]):\n", + " d = np.linalg.norm(r - q[i][None, :], axis=1)\n", + " nn_dists.append(float(np.min(d)))\n", + " return float(np.mean(nn_dists))\n", + "\n", + "train_designs_full = np.array(problem.dataset['train']['optimal_design']).astype(np.float32)\n", + "ref_idx = np.random.default_rng(7).choice(len(train_designs_full), size=min(1024, len(train_designs_full)), replace=False)\n", + "train_reference = train_designs_full[ref_idx]\n", + "\n", + "summary = {\n", + " 'n_samples': int(len(results)),\n", + " 'gen_obj_mean': float(results['gen_obj'].mean()),\n", + " 'base_obj_mean': float(results['base_obj'].mean()),\n", + " 'objective_gap_mean': float(results['gen_minus_base'].mean()),\n", + " 'improvement_rate': float((results['gen_obj'] < results['base_obj']).mean()),\n", + " 'gen_violation_ratio': float((results['gen_violations'] > 0).mean()),\n", + " 'base_violation_ratio': float((results['base_violations'] > 0).mean()),\n", + " 'gen_feasible_rate': float((results['gen_violations'] == 0).mean()),\n", + " 'gen_diversity_l2': mean_pairwise_l2(gen_designs),\n", + " 'gen_novelty_to_train_l2': mean_nn_distance_to_reference(gen_designs, train_reference),\n", + "}\n", + "\n", + "summary_df = pd.DataFrame([summary])\n", + "summary_df\n" + ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], - "source": "results.to_csv(ARTIFACT_DIR / 'per_sample_metrics.csv', index=False)\nsummary_df.to_csv(ARTIFACT_DIR / 'metrics_summary.csv', index=False)\n\nfig, ax = plt.subplots(figsize=(7, 4))\nax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\nax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\nax.set_xlabel('Compliance objective (lower is better)')\nax.set_ylabel('Count')\nax.set_title('Generated vs baseline objective distribution')\nax.legend()\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'objective_histogram.png', dpi=150)\nplt.show()\n\nprint('Saved:')\nprint('-', ARTIFACT_DIR / 'per_sample_metrics.csv')\nprint('-', ARTIFACT_DIR / 'metrics_summary.csv')\nprint('-', ARTIFACT_DIR / 'objective_histogram.png')" + "source": [ + "results_path = ARTIFACT_DIR / 'per_sample_metrics.csv'\n", + "summary_path = ARTIFACT_DIR / 'metrics_summary.csv'\n", + "hist_path = ARTIFACT_DIR / 'objective_histogram.png'\n", + "grid_path = ARTIFACT_DIR / 'design_grid.png'\n", + "scatter_path = ARTIFACT_DIR / 'objective_scatter.png'\n", + "\n", + "results.to_csv(results_path, index=False)\n", + "summary_df.to_csv(summary_path, index=False)\n", + "\n", + "fig, ax = plt.subplots(figsize=(7, 4))\n", + "ax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\n", + "ax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\n", + "ax.set_xlabel('Compliance objective (lower is better)')\n", + "ax.set_ylabel('Count')\n", + "ax.set_title('Generated vs baseline objective distribution')\n", + "ax.legend()\n", + "fig.tight_layout()\n", + "fig.savefig(hist_path, dpi=150)\n", + "plt.show()\n", + "\n", + "fig2, ax2 = plt.subplots(figsize=(5, 5))\n", + "ax2.scatter(results['base_obj'], results['gen_obj'], alpha=0.8)\n", + "min_v = min(results['base_obj'].min(), results['gen_obj'].min())\n", + "max_v = max(results['base_obj'].max(), results['gen_obj'].max())\n", + "ax2.plot([min_v, max_v], [min_v, max_v], '--', color='black', linewidth=1)\n", + "ax2.set_xlabel('Baseline objective')\n", + "ax2.set_ylabel('Generated objective')\n", + "ax2.set_title('Per-sample objective comparison')\n", + "fig2.tight_layout()\n", + "fig2.savefig(scatter_path, dpi=150)\n", + "plt.show()\n", + "\n", + "print('Saved:')\n", + "print('-', results_path)\n", + "print('-', summary_path)\n", + "print('-', hist_path)\n", + "print('-', scatter_path)\n", + "\n", + "if USE_WANDB_ARTIFACTS:\n", + " try:\n", + " import wandb\n", + "\n", + " eval_run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='evaluation', reinit=True)\n", + " eval_run.log({f'eval/{k}': v for k, v in summary.items()})\n", + " eval_run.log({\n", + " 'eval/objective_histogram': wandb.Image(str(hist_path)),\n", + " 'eval/objective_scatter': wandb.Image(str(scatter_path)),\n", + " 'eval/per_sample_table': wandb.Table(dataframe=results),\n", + " })\n", + "\n", + " eval_artifact = wandb.Artifact('dcc26_beams2d_eval_report', type='evaluation')\n", + " for p in [results_path, summary_path, hist_path, scatter_path]:\n", + " eval_artifact.add_file(str(p))\n", + " eval_run.log_artifact(eval_artifact, aliases=[WANDB_ARTIFACT_ALIAS])\n", + " eval_run.finish()\n", + " print('Logged evaluation outputs to W&B.')\n", + " except Exception as exc:\n", + " print('W&B evaluation logging failed (continuing locally):', exc)\n" + ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "execution_count": null, "outputs": [], - "source": "# Visual side-by-side sample grid\nfig, axes = plt.subplots(3, 4, figsize=(12, 8))\nfor i, ax in enumerate(axes.ravel()):\n if i >= 12:\n break\n pair_idx = i // 2\n if i % 2 == 0:\n ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'gen {pair_idx}')\n else:\n ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n ax.set_title(f'base {pair_idx}')\n ax.axis('off')\nfig.tight_layout()\nfig.savefig(ARTIFACT_DIR / 'design_grid.png', dpi=150)\nplt.show()" + "source": [ + "# Visual side-by-side sample grid\n", + "fig, axes = plt.subplots(3, 4, figsize=(12, 8))\n", + "for i, ax in enumerate(axes.ravel()):\n", + " if i >= 12:\n", + " break\n", + " pair_idx = i // 2\n", + " if i % 2 == 0:\n", + " ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n", + " ax.set_title(f'gen {pair_idx}')\n", + " else:\n", + " ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n", + " ax.set_title(f'base {pair_idx}')\n", + " ax.axis('off')\n", + "fig.tight_layout()\n", + "fig.savefig(grid_path, dpi=150)\n", + "plt.show()\n" + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Interpretation hints\n\n- `improvement_rate` shows how often generated designs beat baselines on objective value.\n- `gen_violation_ratio` tracks practical feasibility pressure from constraints.\n- `gen_diversity_l2` is a simple diversity proxy across generated designs.\n" + "source": [ + "## Interpretation hints and discussion prompts\n", + "\n", + "- `objective_gap_mean < 0` means generated designs outperform baselines on average.\n", + "- `gen_feasible_rate` captures practical design validity pressure.\n", + "- `gen_diversity_l2` and `gen_novelty_to_train_l2` help discuss exploration vs imitation.\n", + "- Use these metrics to seed breakout discussion on benchmark quality (objective-only vs broader criteria).\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 99a0dac..f8ec9ee 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -5,6 +5,14 @@ "metadata": {}, "source": "# Notebook 03: Add a New Problem Scaffold (DCC26)\n\nThis notebook is a contribution-oriented walkthrough showing a **minimal EngiBench-compatible problem**.\n\nIt uses a toy simulator so participants can understand the required interface before implementing real physics backends.\n" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", + "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + ] + }, { "cell_type": "code", "execution_count": null, From 29dfc17542f96b5796ea726a8d0110cd7bfb38f3 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Fri, 6 Mar 2026 11:22:00 +0100 Subject: [PATCH 18/37] Strengthen workshop pedagogy and scientific framing in notebooks --- .../participant/00_setup_api_warmup.ipynb | 24 ++++++++++++ .../dcc26/participant/01_train_generate.ipynb | 11 ++++++ .../participant/02_evaluate_metrics.ipynb | 23 +++++++++++ .../03_add_new_problem_scaffold.ipynb | 27 +++++++++++++ .../dcc26/solutions/00_setup_api_warmup.ipynb | 24 ++++++++++++ .../dcc26/solutions/01_train_generate.ipynb | 39 +++++++++++++++++++ .../dcc26/solutions/02_evaluate_metrics.ipynb | 25 ++++++++++++ .../03_add_new_problem_scaffold.ipynb | 27 +++++++++++++ 8 files changed, 200 insertions(+) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 57f343c..93ebff8 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -13,6 +13,19 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Why this warmup matters\n", + "\n", + "- **EngiBench** provides the benchmark contract: problem definition, conditions, objectives, constraints, simulator, and dataset.\n", + "- **EngiOpt** provides optimization/generative methods that operate against that contract.\n", + "- In this workshop, we separate these concerns so model innovation and evaluation stay reproducible.\n", + "\n", + "By the end of this notebook, you should be able to inspect what is fixed by the benchmark and what is variable in your method design.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -79,6 +92,17 @@ "cell_type": "markdown", "metadata": {}, "source": "## Next\n\nContinue with **Notebook 01** to train a lightweight conditional generator and create designs for evaluation.\n" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reflection prompts\n", + "\n", + "1. Which fields in `problem` are benchmark-defining vs method-defining?\n", + "2. What can go wrong if a paper changes simulator settings without reporting them?\n", + "3. Which API objects would you log in a reproducibility appendix?\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 8df6772..d2d1967 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -52,6 +52,17 @@ "If you want remote artifacts, set `USE_WANDB_ARTIFACTS=True` and log in with `wandb.login()`.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### EngiBench vs EngiOpt roles in this notebook\n", + "\n", + "- `Beams2D` (EngiBench): defines dataset fields, conditions, constraints, and simulator-based objective.\n", + "- `Generator` (EngiOpt): maps condition vectors + latent noise to candidate designs.\n", + "- The **key research question** here is not just reconstruction quality, but whether generated designs remain feasible and competitive under benchmark simulation.\n" + ] + }, { "cell_type": "code", "metadata": {}, diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index c6697fe..f5b9626 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -53,6 +53,17 @@ "- otherwise it can auto-build artifacts locally using EngiOpt.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Why these metrics matter for benchmarking\n", + "\n", + "Objective value alone is not enough for engineering design benchmarks.\n", + "We also track feasibility, diversity, and novelty proxies to reason about method behavior,\n", + "trade-offs, and potential benchmark blind spots.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -404,6 +415,18 @@ "- Diversity + novelty are useful for discussing benchmark completeness.\n", "- Compare your summary metrics with peers during the breakout discussion.\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Discussion bridge to workshop breakout\n", + "\n", + "Use your results to discuss:\n", + "1. Which metric changed your interpretation most (objective vs feasibility vs diversity)?\n", + "2. Which additional engineering criterion is still missing from this benchmark?\n", + "3. How should we report uncertainty/runtime when comparing generative methods?\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 518fc41..d1ce795 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -13,6 +13,21 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## What makes a new problem benchmark-ready\n", + "\n", + "A useful benchmark problem should make the following explicit:\n", + "- Design representation and constraints (what is feasible)\n", + "- Condition space (what is being requested)\n", + "- Objective(s) and simulator semantics (what is optimized)\n", + "- Dataset protocol (train/test split, provenance, leakage risks)\n", + "\n", + "This notebook uses a toy scaffold so you can map these requirements to your own domain later.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -60,6 +75,18 @@ "cell_type": "markdown", "metadata": {}, "source": "## Mapping to real EngiBench contributions\n\nThis toy scaffold demonstrates the interface shape only. For real contributions:\n\n1. Create `engibench/problems//v0.py`\n2. Implement real `simulate` and (optionally) `optimize`\n3. Provide `conditions`, `design_space`, and `dataset_id`\n4. Add docs and tests\n\nReference docs: [Adding a new problem](https://github.com/IDEALLab/EngiBench/blob/main/docs/tutorials/new_problem.md)\n" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Contribution checklist\n", + "\n", + "- Define clear simulator I/O and units.\n", + "- Add deterministic seeds for repeatability.\n", + "- Document constraint semantics and failure modes.\n", + "- Provide at least one baseline and expected metric outputs.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index 4e50298..f3a0f68 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -13,6 +13,19 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Why this warmup matters\n", + "\n", + "- **EngiBench** provides the benchmark contract: problem definition, conditions, objectives, constraints, simulator, and dataset.\n", + "- **EngiOpt** provides optimization/generative methods that operate against that contract.\n", + "- In this workshop, we separate these concerns so model innovation and evaluation stay reproducible.\n", + "\n", + "By the end of this notebook, you should be able to inspect what is fixed by the benchmark and what is variable in your method design.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -79,6 +92,17 @@ "cell_type": "markdown", "metadata": {}, "source": "## Next\n\nContinue with **Notebook 01** to train a lightweight conditional generator and create designs for evaluation.\n" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reflection prompts\n", + "\n", + "1. Which fields in `problem` are benchmark-defining vs method-defining?\n", + "2. What can go wrong if a paper changes simulator settings without reporting them?\n", + "3. Which API objects would you log in a reproducibility appendix?\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 9721042..66878e9 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -56,6 +56,17 @@ "- Set it to `True` if you want to test artifact upload and training logs.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### EngiBench vs EngiOpt roles in this notebook\n", + "\n", + "- `Beams2D` (EngiBench): defines dataset fields, conditions, constraints, and simulator-based objective.\n", + "- `Generator` (EngiOpt): maps condition vectors + latent noise to candidate designs.\n", + "- The **key research question** here is not just reconstruction quality, but whether generated designs remain feasible and competitive under benchmark simulation.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -128,6 +139,19 @@ "We intentionally use a compact subset (`N_TRAIN=512`) so participants can finish on free Colab runtimes.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Training diagnostics to monitor\n", + "\n", + "- Loss trend stability across epochs (convergence vs collapse)\n", + "- Sensitivity to seed, subset size, and latent dimension\n", + "- Whether lower training loss transfers to better simulated objective\n", + "\n", + "These diagnostics are useful for comparing methods across papers, not only for this workshop.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -182,6 +206,7 @@ "outputs": [], "source": [ "TRAIN_FROM_SCRATCH = True\n", + "# Train on compact subset for workshop-time reliability (not full benchmark SOTA mode).\n", "EPOCHS = 8\n", "BATCH_SIZE = 64\n", "\n", @@ -211,6 +236,7 @@ " )\n", "\n", " for epoch in range(EPOCHS):\n", + " # Epoch-average loss is what we compare across quick workshop runs.\n", " model.train()\n", " epoch_loss = 0.0\n", "\n", @@ -285,6 +311,18 @@ "We generate `N_SAMPLES` designs from the test condition set and report quick feasibility/objective diagnostics.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Scientific checkpoint before Notebook 02\n", + "\n", + "Before moving on, ask:\n", + "1. Are generated designs diverse or mostly memorized variants?\n", + "2. Do quick constraint checks indicate likely feasibility issues?\n", + "3. Which artifacts are necessary for independent re-evaluation?\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -328,6 +366,7 @@ "execution_count": null, "outputs": [], "source": [ + "# Artifact contract consumed by Notebook 02 and optional W&B logging.\n", "generated_path = ARTIFACT_DIR / 'generated_designs.npy'\n", "baseline_path = ARTIFACT_DIR / 'baseline_designs.npy'\n", "conditions_path = ARTIFACT_DIR / 'conditions.json'\n", diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 3a2a0c6..ff94606 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -58,6 +58,17 @@ "3. Automatic local rebuild with EngiOpt (enabled by default)\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Why these metrics matter for benchmarking\n", + "\n", + "Objective value alone is not enough for engineering design benchmarks.\n", + "We also track feasibility, diversity, and novelty proxies to reason about method behavior,\n", + "trade-offs, and potential benchmark blind spots.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -280,6 +291,7 @@ "execution_count": null, "outputs": [], "source": [ + "# Constraint and simulation evaluations are decoupled to diagnose failure modes clearly.\n", "problem = Beams2D(seed=7)\n", "\n", "rows = []\n", @@ -312,6 +324,7 @@ "execution_count": null, "outputs": [], "source": [ + "# Diversity proxy (intra-generated spread) and novelty proxy (distance to train set).\n", "def mean_pairwise_l2(designs: np.ndarray) -> float:\n", " flat = designs.reshape(designs.shape[0], -1)\n", " n = flat.shape[0]\n", @@ -455,6 +468,18 @@ "- `gen_diversity_l2` and `gen_novelty_to_train_l2` help discuss exploration vs imitation.\n", "- Use these metrics to seed breakout discussion on benchmark quality (objective-only vs broader criteria).\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Discussion bridge to workshop breakout\n", + "\n", + "Use your results to discuss:\n", + "1. Which metric changed your interpretation most (objective vs feasibility vs diversity)?\n", + "2. Which additional engineering criterion is still missing from this benchmark?\n", + "3. How should we report uncertainty/runtime when comparing generative methods?\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index f8ec9ee..fbebe60 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -13,6 +13,21 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## What makes a new problem benchmark-ready\n", + "\n", + "A useful benchmark problem should make the following explicit:\n", + "- Design representation and constraints (what is feasible)\n", + "- Condition space (what is being requested)\n", + "- Objective(s) and simulator semantics (what is optimized)\n", + "- Dataset protocol (train/test split, provenance, leakage risks)\n", + "\n", + "This notebook uses a toy scaffold so you can map these requirements to your own domain later.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -60,6 +75,18 @@ "cell_type": "markdown", "metadata": {}, "source": "## Mapping to real EngiBench contributions\n\nThis toy scaffold demonstrates the interface shape only. For real contributions:\n\n1. Create `engibench/problems//v0.py`\n2. Implement real `simulate` and (optionally) `optimize`\n3. Provide `conditions`, `design_space`, and `dataset_id`\n4. Add docs and tests\n\nReference docs: [Adding a new problem](https://github.com/IDEALLab/EngiBench/blob/main/docs/tutorials/new_problem.md)\n" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Contribution checklist\n", + "\n", + "- Define clear simulator I/O and units.\n", + "- Add deterministic seeds for repeatability.\n", + "- Document constraint semantics and failure modes.\n", + "- Provide at least one baseline and expected metric outputs.\n" + ] } ], "metadata": { From d8eda9e87e1cee8fd9510f5a058e109336285fee Mon Sep 17 00:00:00 2001 From: Soheyl Date: Fri, 6 Mar 2026 11:24:36 +0100 Subject: [PATCH 19/37] Make DCC26 notebooks self-explanatory standalone tutorials --- .../participant/00_setup_api_warmup.ipynb | 28 +++++++++++ .../dcc26/participant/01_train_generate.ipynb | 50 +++++++++++++++++-- .../participant/02_evaluate_metrics.ipynb | 37 +++++++++++++- .../03_add_new_problem_scaffold.ipynb | 27 ++++++++++ .../dcc26/solutions/00_setup_api_warmup.ipynb | 28 +++++++++++ .../dcc26/solutions/01_train_generate.ipynb | 34 +++++++++++++ .../dcc26/solutions/02_evaluate_metrics.ipynb | 29 +++++++++++ .../03_add_new_problem_scaffold.ipynb | 27 ++++++++++ 8 files changed, 254 insertions(+), 6 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 93ebff8..17833d9 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -13,6 +13,24 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Standalone guide\n", + "\n", + "**What you will learn**\n", + "- How EngiBench packages an engineering benchmark (`problem`, `dataset`, `constraints`, `simulate`).\n", + "- How to inspect design variables, condition variables, and objective context.\n", + "\n", + "**Expected runtime**\n", + "- 5-15 minutes on Colab CPU.\n", + "\n", + "**If you start here without the workshop talk**\n", + "- Read each printed object shape/key carefully.\n", + "- Focus on what is benchmark-fixed (problem API) vs method-flexible (model choice).\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -88,6 +106,16 @@ "outputs": [], "source": "# TODO 4: run one explicit constraint check with an intentionally mismatched volfrac\n# bad_config = dict(config)\n# bad_config['volfrac'] = 0.2\n# violations = ...\n# print(len(violations)); print(violations) if any\n\nraise NotImplementedError('Complete TODO 4 in this cell')" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Troubleshooting\n", + "\n", + "- If dataset loading hangs, rerun the cell once (first fetch may be slower).\n", + "- If plotting fails in local Jupyter, ensure `matplotlib` backend is available.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index d2d1967..d220185 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -17,6 +17,29 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Standalone guide\n", + "\n", + "**What you will learn**\n", + "- How to train an EngiOpt generator against an EngiBench dataset slice.\n", + "- How to export reproducible artifacts for downstream evaluation.\n", + "\n", + "**Expected runtime**\n", + "- ~10-20 minutes on Colab CPU for default settings.\n", + "\n", + "**Outputs produced**\n", + "- `generated_designs.npy`, `baseline_designs.npy`, `conditions.json`\n", + "- `engiopt_cgan2d_generator_supervised.pt`\n", + "- `training_history.csv`, `training_curve.png`\n", + "\n", + "**If results look poor**\n", + "- Check training loss trend first.\n", + "- Then inspect feasibility ratio in Notebook 02 before changing architecture.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -163,7 +186,9 @@ "# - criterion (MSELoss)\n", "# - sample_noise(batch_size)\n", "\n", - "raise NotImplementedError('Complete TODO 1 model setup')\n" + "raise NotImplementedError('Complete TODO 1 model setup')\n", + "\n", + "# Completion check: calling `sample_noise(4)` should return shape (4, LATENT_DIM).\n" ] }, { @@ -188,7 +213,9 @@ "# - elif CKPT_PATH exists: load it\n", "# - else: raise FileNotFoundError\n", "\n", - "raise NotImplementedError('Complete TODO 2 training/loading')\n" + "raise NotImplementedError('Complete TODO 2 training/loading')\n", + "\n", + "# Completion check: after training, CKPT_PATH and HISTORY_PATH should exist.\n" ] }, { @@ -205,7 +232,9 @@ "# - create:\n", "# gen_designs, baseline_designs, test_conds, conditions_records\n", "\n", - "raise NotImplementedError('Complete TODO 3 generation')\n" + "raise NotImplementedError('Complete TODO 3 generation')\n", + "\n", + "# Completion check: `gen_designs.shape == baseline_designs.shape` and len(conditions_records)==N_SAMPLES.\n" ] }, { @@ -222,7 +251,9 @@ "# Recommended extras:\n", "# - checkpoint (.pt), training_history.csv, training_curve.png\n", "\n", - "raise NotImplementedError('Complete TODO 4 artifact export')\n" + "raise NotImplementedError('Complete TODO 4 artifact export')\n", + "\n", + "# Completion check: printed file paths exist and can be loaded by Notebook 02.\n" ] }, { @@ -246,6 +277,17 @@ "plt.show()\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Troubleshooting\n", + "\n", + "- `ModuleNotFoundError: engiopt...`: rerun bootstrap cell; on Colab, Runtime -> Restart runtime.\n", + "- No artifact files found later: verify export cell completed and files printed from `ARTIFACT_DIR`.\n", + "- Training too slow: reduce `EPOCHS` or `N_TRAIN`, but note this affects quality.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index f5b9626..abb55e6 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -17,6 +17,24 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Standalone guide\n", + "\n", + "**What you will learn**\n", + "- How to evaluate generated designs with constraint checks + simulator objective.\n", + "- How to summarize performance with objective, feasibility, diversity, and novelty proxies.\n", + "\n", + "**Expected runtime**\n", + "- 10-25 minutes depending on whether artifacts must be rebuilt.\n", + "\n", + "**If artifacts are missing**\n", + "- Notebook can auto-build them locally (`AUTO_BUILD_ARTIFACTS_IF_MISSING=True`).\n", + "- Optional: enable W&B artifact download.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -287,7 +305,9 @@ "raise NotImplementedError('Complete TODO 1 evaluation loop')\n", "\n", "results = pd.DataFrame(rows)\n", - "results.head()\n" + "results.head()\n", + "\n", + "# Completion check: `results` should have one row per sample.\n" ] }, { @@ -328,7 +348,9 @@ " nn_dists.append(float(np.min(d)))\n", " return float(np.mean(nn_dists))\n", "\n", - "raise NotImplementedError('Complete TODO 2 summary metrics')\n" + "raise NotImplementedError('Complete TODO 2 summary metrics')\n", + "\n", + "# Completion check: `summary_df` should be one row with all metric columns populated.\n" ] }, { @@ -427,6 +449,17 @@ "2. Which additional engineering criterion is still missing from this benchmark?\n", "3. How should we report uncertainty/runtime when comparing generative methods?\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Troubleshooting\n", + "\n", + "- Missing artifacts: keep `AUTO_BUILD_ARTIFACTS_IF_MISSING=True` or run Notebook 01 first.\n", + "- W&B pull fails: disable `USE_WANDB_ARTIFACTS` and use local path.\n", + "- Objective comparison seems inconsistent: ensure simulator reset is called before each run.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index d1ce795..c3adcc2 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -13,6 +13,23 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Standalone guide\n", + "\n", + "**What you will learn**\n", + "- Which `Problem` components are required for a reusable benchmark scaffold.\n", + "- How to think about benchmark quality beyond “code runs”.\n", + "\n", + "**Expected runtime**\n", + "- 10-20 minutes.\n", + "\n", + "**Outcome**\n", + "- A toy scaffold template you can map to a real domain and contribution-ready checklist.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -87,6 +104,16 @@ "- Document constraint semantics and failure modes.\n", "- Provide at least one baseline and expected metric outputs.\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Troubleshooting\n", + "\n", + "- If class TODOs fail, implement methods incrementally and run one check cell at a time.\n", + "- Keep seed handling deterministic so your scaffold behavior is reproducible.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index f3a0f68..bd051df 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -13,6 +13,24 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Standalone guide\n", + "\n", + "**What you will learn**\n", + "- How EngiBench packages an engineering benchmark (`problem`, `dataset`, `constraints`, `simulate`).\n", + "- How to inspect design variables, condition variables, and objective context.\n", + "\n", + "**Expected runtime**\n", + "- 5-15 minutes on Colab CPU.\n", + "\n", + "**If you start here without the workshop talk**\n", + "- Read each printed object shape/key carefully.\n", + "- Focus on what is benchmark-fixed (problem API) vs method-flexible (model choice).\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -88,6 +106,16 @@ "outputs": [], "source": "# One explicit constraint check with intentionally mismatched volume fraction\nbad_config = dict(config)\nbad_config['volfrac'] = 0.2\nviolations = problem.check_constraints(design=design, config=bad_config)\n\nprint('Violation count:', len(violations))\nif violations:\n print(violations)\nelse:\n print('No violations found')" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Troubleshooting\n", + "\n", + "- If dataset loading hangs, rerun the cell once (first fetch may be slower).\n", + "- If plotting fails in local Jupyter, ensure `matplotlib` backend is available.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 66878e9..ec4d76e 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -21,6 +21,29 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Standalone guide\n", + "\n", + "**What you will learn**\n", + "- How to train an EngiOpt generator against an EngiBench dataset slice.\n", + "- How to export reproducible artifacts for downstream evaluation.\n", + "\n", + "**Expected runtime**\n", + "- ~10-20 minutes on Colab CPU for default settings.\n", + "\n", + "**Outputs produced**\n", + "- `generated_designs.npy`, `baseline_designs.npy`, `conditions.json`\n", + "- `engiopt_cgan2d_generator_supervised.pt`\n", + "- `training_history.csv`, `training_curve.png`\n", + "\n", + "**If results look poor**\n", + "- Check training loss trend first.\n", + "- Then inspect feasibility ratio in Notebook 02 before changing architecture.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -430,6 +453,17 @@ "plt.show()\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Troubleshooting\n", + "\n", + "- `ModuleNotFoundError: engiopt...`: rerun bootstrap cell; on Colab, Runtime -> Restart runtime.\n", + "- No artifact files found later: verify export cell completed and files printed from `ARTIFACT_DIR`.\n", + "- Training too slow: reduce `EPOCHS` or `N_TRAIN`, but note this affects quality.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index ff94606..712bf8e 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -21,6 +21,24 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Standalone guide\n", + "\n", + "**What you will learn**\n", + "- How to evaluate generated designs with constraint checks + simulator objective.\n", + "- How to summarize performance with objective, feasibility, diversity, and novelty proxies.\n", + "\n", + "**Expected runtime**\n", + "- 10-25 minutes depending on whether artifacts must be rebuilt.\n", + "\n", + "**If artifacts are missing**\n", + "- Notebook can auto-build them locally (`AUTO_BUILD_ARTIFACTS_IF_MISSING=True`).\n", + "- Optional: enable W&B artifact download.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -480,6 +498,17 @@ "2. Which additional engineering criterion is still missing from this benchmark?\n", "3. How should we report uncertainty/runtime when comparing generative methods?\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Troubleshooting\n", + "\n", + "- Missing artifacts: keep `AUTO_BUILD_ARTIFACTS_IF_MISSING=True` or run Notebook 01 first.\n", + "- W&B pull fails: disable `USE_WANDB_ARTIFACTS` and use local path.\n", + "- Objective comparison seems inconsistent: ensure simulator reset is called before each run.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index fbebe60..1c6b141 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -13,6 +13,23 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Standalone guide\n", + "\n", + "**What you will learn**\n", + "- Which `Problem` components are required for a reusable benchmark scaffold.\n", + "- How to think about benchmark quality beyond “code runs”.\n", + "\n", + "**Expected runtime**\n", + "- 10-20 minutes.\n", + "\n", + "**Outcome**\n", + "- A toy scaffold template you can map to a real domain and contribution-ready checklist.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -87,6 +104,16 @@ "- Document constraint semantics and failure modes.\n", "- Provide at least one baseline and expected metric outputs.\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Troubleshooting\n", + "\n", + "- If class TODOs fail, implement methods incrementally and run one check cell at a time.\n", + "- Keep seed handling deterministic so your scaffold behavior is reproducible.\n" + ] } ], "metadata": { From b855b8629b78571f7776cfd28b958d60175e7fb6 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Fri, 6 Mar 2026 11:32:15 +0100 Subject: [PATCH 20/37] Author notebooks from pedagogy blueprint with stepwise teaching cards --- .../dcc26/NOTEBOOK_PEDAGOGY_BLUEPRINT.md | 131 ++++++++++++++++++ .../participant/00_setup_api_warmup.ipynb | 63 +++++++++ .../dcc26/participant/01_train_generate.ipynb | 81 +++++++++++ .../participant/02_evaluate_metrics.ipynb | 63 +++++++++ .../03_add_new_problem_scaffold.ipynb | 45 ++++++ .../dcc26/solutions/00_setup_api_warmup.ipynb | 63 +++++++++ .../dcc26/solutions/01_train_generate.ipynb | 96 +++++++++++++ .../dcc26/solutions/02_evaluate_metrics.ipynb | 67 +++++++++ .../03_add_new_problem_scaffold.ipynb | 45 ++++++ 9 files changed, 654 insertions(+) create mode 100644 workshops/dcc26/NOTEBOOK_PEDAGOGY_BLUEPRINT.md diff --git a/workshops/dcc26/NOTEBOOK_PEDAGOGY_BLUEPRINT.md b/workshops/dcc26/NOTEBOOK_PEDAGOGY_BLUEPRINT.md new file mode 100644 index 0000000..19d6299 --- /dev/null +++ b/workshops/dcc26/NOTEBOOK_PEDAGOGY_BLUEPRINT.md @@ -0,0 +1,131 @@ +# DCC26 Notebook Pedagogy Blueprint (Pre-write Source) + +This document is the canonical pre-write for workshop notebooks. Notebooks should be generated from this structure, not authored directly as raw `.ipynb` first. + +## Teaching Design Principles + +1. Every technical step is paired with a markdown teaching cell. +2. Every code section has local context: why, inputs, outputs, checks, failure modes. +3. Benchmark science is explicit: objective, feasibility, diversity, novelty, reproducibility. +4. Discussion prompts are embedded and mapped to workshop breakout questions. +5. Participant and solution tracks share the same pedagogical arc; only implementation detail differs. + +## Common Cell Pattern + +For each section: + +- Purpose: why this step matters for benchmark credibility +- Inputs: what artifacts/variables are required +- Action: code operation performed +- Success check: what output indicates correctness +- Failure modes: common pitfalls and fixes +- Discussion bridge: one reflection question + +--- + +## Notebook 00: Setup + API Warmup + +### Learning objective +Understand EngiBench benchmark contract components and reproducibility controls. + +### Section plan +1. Read-me-first + copy mode + runtime expectation +2. Concept cell: EngiBench vs model libraries +3. Environment bootstrap +4. Reproducibility cell (seed, versions) +5. Problem instantiation (`Beams2D`) + inspection +6. Dataset inspection and shape sanity +7. Render one sample and explain representation +8. Explicit constraint violation check with interpretation +9. Reflection prompts tied to comparability across papers + +### Discussion trigger +Which benchmark settings must be fixed for fair method comparison? + +--- + +## Notebook 01: Train + Generate + +### Learning objective +Implement an EngiOpt model against EngiBench data while preserving evaluation-ready artifacts. + +### Section plan +1. Read-me-first + copy mode + expected runtime +2. Concept cell: inverse design framing, conditional generation assumptions +3. Bootstrap deps and imports +4. Configuration and artifact contract +5. Data subset construction and rationale (runtime vs fidelity) +6. Model definition and optimizer +7. Training loop with diagnostics + expected loss behavior +8. Generation from test conditions +9. Quick feasibility precheck (not final evaluation) +10. Artifact export contract (npy/json/checkpoint/history/curve) +11. Optional W&B logging: train curve, scalar logs, artifact bundle +12. Visual sanity grid +13. Discussion prompt: training loss vs engineering validity mismatch + +### Discussion trigger +Can lower train reconstruction loss worsen simulator objective or feasibility? + +--- + +## Notebook 02: Evaluate + Metrics + +### Learning objective +Run robust benchmark evaluation and interpret trade-offs beyond objective score. + +### Section plan +1. Read-me-first + copy mode + expected runtime +2. Concept cell: why objective-only reporting is incomplete +3. Bootstrap deps and imports +4. Artifact loading strategy (local -> optional W&B -> local auto-build) +5. Per-sample evaluation loop (constraint + simulate) +6. Metric layer: + - objective means and gap + - improvement rate + - feasibility/violation rates + - diversity proxy + - novelty-to-train proxy +7. Export layer: CSV + histogram + scatter + grid +8. Optional W&B evaluation logging (table + images + summary) +9. Interpretation rubric with examples +10. Breakout prompts mapped to workshop proposal + +### Discussion trigger +Which missing metric would change conclusions for your domain? + +--- + +## Notebook 03: Add New Problem Scaffold + +### Learning objective +Understand minimal interface required for a reusable EngiBench-style benchmark problem. + +### Section plan +1. Read-me-first + copy mode +2. Concept cell: benchmark-ready problem checklist +3. Scaffold imports and abstract contract explanation +4. Minimal `Problem` implementation skeleton +5. Toy simulator and constraints +6. Registration/discovery and deterministic behavior +7. Contribution checklist for real domains +8. Reflection prompts on leakage, units, and reproducibility metadata + +### Discussion trigger +What metadata is minimally required so another lab can reproduce your new benchmark? + +--- + +## Participant vs Solution Policy + +- Participant notebooks: keep code TODOs, but each TODO has explicit completion checks and expected outputs. +- Solution notebooks: complete implementations plus concise inline comments for non-obvious logic only. +- Both tracks: keep identical markdown structure for pedagogical alignment. + +## Quality Gate Before Publishing + +1. All code cells compile. +2. Solution Notebook 01+02 execute end-to-end in workshop env. +3. Artifact contract is consistent between Notebook 01 and 02. +4. Copy-safe links use `#copy=true`. +5. Standalone readability check: each notebook understandable without live lecture. diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 17833d9..94bb46e 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -13,6 +13,15 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebook map\n", + "\n", + "Use this notebook as a standalone guide to understanding the EngiBench API contract before modeling.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -71,6 +80,15 @@ " print('Skipping install (using current environment).')\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1 - Initialize Reproducible Session\n", + "\n", + "Run seed/version setup first.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -78,6 +96,15 @@ "outputs": [], "source": "import random\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nimport engibench\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nprint('engibench version:', engibench.__version__)\nprint('seed:', SEED)" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2 - Instantiate Benchmark Problem (TODO)\n", + "\n", + "Inspect API objects after completion.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -85,6 +112,15 @@ "outputs": [], "source": "# TODO 1: instantiate the problem with the global SEED\n# problem = ...\n\n# TODO 2: print these fields\n# - type(problem).__name__\n# - problem.design_space\n# - problem.objectives\n# - problem.conditions\n# - problem.conditions_keys\n# - problem.dataset_id\n\nraise NotImplementedError('Complete TODO 1/2 in this cell')" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3 - Inspect Dataset Structure (TODO)\n", + "\n", + "Confirm sample keys and shapes.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -92,6 +128,15 @@ "outputs": [], "source": "# TODO 3: load dataset and extract one sample\n# dataset = problem.dataset\n# sample_idx = 0\n# design = ...\n# config = ... # dict over problem.conditions_keys\n\n# print dataset summary and sample shapes/values\n\nraise NotImplementedError('Complete TODO 3 in this cell')" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4 - Visualize One Benchmark Design\n", + "\n", + "Check if rendering matches expectation.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -99,6 +144,15 @@ "outputs": [], "source": "# Render the sampled design (run after TODO 3)\nfig, ax = problem.render(design)\nax.set_title('Participant: sampled Beams2D design')\nplt.show()" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 5 - Test Constraint Semantics (TODO)\n", + "\n", + "Explain why violation output makes sense.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -131,6 +185,15 @@ "2. What can go wrong if a paper changes simulator settings without reporting them?\n", "3. Which API objects would you log in a reproducibility appendix?\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Takeaways\n", + "\n", + "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index d220185..13f168a 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -17,6 +17,15 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebook map\n", + "\n", + "This notebook teaches the full train-generate artifact workflow with TODO checkpoints for independent learners.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -86,6 +95,15 @@ "- The **key research question** here is not just reconstruction quality, but whether generated designs remain feasible and competitive under benchmark simulation.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1 - Configure Reproducible Environment\n", + "\n", + "Follow the printed checks to confirm your setup before implementing TODOs.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -148,6 +166,15 @@ "LATENT_DIM = 32\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2 - Build Training Slice from EngiBench Dataset\n", + "\n", + "Use this as fixed benchmark input; your method logic starts in TODO cells.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -173,6 +200,15 @@ "print('target range:', float(targets_np.min()), 'to', float(targets_np.max()))\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3 - Implement Model Setup (TODO)\n", + "\n", + "Fill TODO 1, then run the completion check in the cell comments.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -191,6 +227,15 @@ "# Completion check: calling `sample_noise(4)` should return shape (4, LATENT_DIM).\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4 - Implement Train/Load Logic (TODO)\n", + "\n", + "Fill TODO 2 and verify checkpoint/history artifacts are created.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -218,6 +263,15 @@ "# Completion check: after training, CKPT_PATH and HISTORY_PATH should exist.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 5 - Implement Generation Logic (TODO)\n", + "\n", + "Ensure generated/baseline shapes match and condition records are complete.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -237,6 +291,15 @@ "# Completion check: `gen_designs.shape == baseline_designs.shape` and len(conditions_records)==N_SAMPLES.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 6 - Implement Artifact Export (TODO)\n", + "\n", + "Notebook 02 depends on these exact files; treat them as API outputs.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -256,6 +319,15 @@ "# Completion check: printed file paths exist and can be loaded by Notebook 02.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 7 - Quick Visual QA\n", + "\n", + "Use this to debug generation issues before full evaluation.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -296,6 +368,15 @@ "\n", "Continue with **Notebook 02** to run physics evaluation and benchmark metrics.\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Takeaways\n", + "\n", + "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index abb55e6..2f5c5d6 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -17,6 +17,15 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebook map\n", + "\n", + "This notebook guides full benchmark evaluation with TODOs and interpretation prompts.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -82,6 +91,15 @@ "trade-offs, and potential benchmark blind spots.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1 - Resolve Artifact Source and Recovery Path\n", + "\n", + "If Notebook 01 artifacts are missing, keep auto-build enabled for a self-contained run.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -286,6 +304,15 @@ "print('conditions:', len(conditions))\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2 - Implement Per-Sample Evaluation (TODO)\n", + "\n", + "Fill TODO 1 and confirm one `results` row per sample.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -310,6 +337,15 @@ "# Completion check: `results` should have one row per sample.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3 - Implement Summary Metrics (TODO)\n", + "\n", + "Include objective, feasibility, diversity, and novelty summaries.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -353,6 +389,15 @@ "# Completion check: `summary_df` should be one row with all metric columns populated.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4 - Export Evidence Artifacts\n", + "\n", + "Confirm CSV and figure files are saved to `ARTIFACT_DIR`.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -403,6 +448,15 @@ "# log summary metrics, tables, and images to W&B.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 5 - Visual Comparison for Interpretation\n", + "\n", + "Use this to interpret disagreements between scalar metrics and visual quality.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -460,6 +514,15 @@ "- W&B pull fails: disable `USE_WANDB_ARTIFACTS` and use local path.\n", "- Objective comparison seems inconsistent: ensure simulator reset is called before each run.\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Takeaways\n", + "\n", + "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index c3adcc2..4ea4726 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -13,6 +13,15 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebook map\n", + "\n", + "Use this as a standalone template for implementing a new benchmark problem contract.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -67,6 +76,15 @@ " print('Skipping install (using current environment).')\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1 - Import Scaffold Dependencies\n", + "\n", + "Use these imports to complete the TODO skeleton.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -74,6 +92,15 @@ "outputs": [], "source": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport numpy as np\nfrom gymnasium import spaces\n\nfrom engibench.constraint import bounded\nfrom engibench.core import ObjectiveDirection\nfrom engibench.core import OptiStep\nfrom engibench.core import Problem\n" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2 - Implement Minimal Problem Contract (TODO)\n", + "\n", + "Complete required methods incrementally.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -81,6 +108,15 @@ "outputs": [], "source": "class ToyDensityProblem(Problem[np.ndarray]):\n \"\"\"Minimal toy problem scaffold for workshop teaching.\"\"\"\n\n version = 0\n objectives = ((\"toy_cost\", ObjectiveDirection.MINIMIZE),)\n\n @dataclass\n class Conditions:\n target_density: Annotated[float, bounded(lower=0.0, upper=1.0)] = 0.5\n\n @dataclass\n class Config(Conditions):\n resolution: Annotated[int, bounded(lower=4, upper=128)] = 16\n max_iter: Annotated[int, bounded(lower=1, upper=200)] = 20\n\n dataset_id = \"IDEALLab/beams_2d_50_100_v0\" # placeholder to satisfy scaffold\n container_id = None\n\n def __init__(self, seed: int = 0, **kwargs):\n super().__init__(seed=seed)\n self.config = self.Config(**kwargs)\n self.conditions = self.Conditions(target_density=self.config.target_density)\n self.design_space = spaces.Box(\n low=0.0, high=1.0, shape=(self.config.resolution, self.config.resolution), dtype=np.float32\n )\n\n def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n # TODO 1: implement toy objective\n # Suggested terms:\n # - density mismatch to target_density\n # - smoothness penalty based on np.diff\n raise NotImplementedError('Implement simulate')\n\n def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n # TODO 2: implement simple iterative optimizer\n # - update design toward target density\n # - append OptiStep each iteration\n raise NotImplementedError('Implement optimize')\n\n def render(self, design: np.ndarray, *, open_window: bool = False):\n import matplotlib.pyplot as plt\n\n fig, ax = plt.subplots(figsize=(4, 4))\n ax.imshow(design, cmap=\"viridis\", vmin=0, vmax=1)\n ax.set_title(\"ToyDensityProblem design\")\n ax.axis(\"off\")\n if open_window:\n plt.show()\n return fig, ax\n\n def random_design(self):\n d = self.np_random.random(self.design_space.shape).astype(np.float32)\n return d, -1" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3 - Smoke-Test Your Scaffold\n", + "\n", + "Run checks to validate your implementation.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -114,6 +150,15 @@ "- If class TODOs fail, implement methods incrementally and run one check cell at a time.\n", "- Keep seed handling deterministic so your scaffold behavior is reproducible.\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Takeaways\n", + "\n", + "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index bd051df..743e6b8 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -13,6 +13,15 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebook map\n", + "\n", + "This notebook is a complete reference walkthrough of EngiBench API semantics for Beams2D.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -71,6 +80,15 @@ " print('Skipping install (using current environment).')\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1 - Initialize Reproducible Session\n", + "\n", + "Set seeds and inspect runtime context before touching the benchmark API.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -78,6 +96,15 @@ "outputs": [], "source": "import random\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nimport engibench\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nprint('engibench version:', engibench.__version__)\nprint('seed:', SEED)" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2 - Instantiate Benchmark Problem\n", + "\n", + "Inspect `design_space`, `condition_space`, and objective metadata.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -85,6 +112,15 @@ "outputs": [], "source": "problem = Beams2D(seed=SEED)\n\nprint('Problem class:', type(problem).__name__)\nprint('Design space:', problem.design_space)\nprint('Objectives:', problem.objectives)\nprint('Conditions instance:', problem.conditions)\nprint('Condition keys:', problem.conditions_keys)\nprint('Dataset ID:', problem.dataset_id)" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3 - Inspect Dataset Structure\n", + "\n", + "Validate train/test splits and sample fields used by downstream notebooks.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -92,6 +128,15 @@ "outputs": [], "source": "dataset = problem.dataset\nprint(dataset)\n\nsample_idx = 0\ndesign = np.array(dataset['train']['optimal_design'][sample_idx])\nconfig = {k: dataset['train'][k][sample_idx] for k in problem.conditions_keys}\n\nprint('Sample design shape:', design.shape)\nprint('Sample config:', config)" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4 - Visualize One Benchmark Design\n", + "\n", + "Interpret what the representation encodes physically.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -99,6 +144,15 @@ "outputs": [], "source": "fig, ax = problem.render(design)\nax.set_title('Sample Beams2D design from training split')\nplt.show()" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 5 - Test Constraint Semantics\n", + "\n", + "Use an intentionally mismatched config to understand violation reporting behavior.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -131,6 +185,15 @@ "2. What can go wrong if a paper changes simulator settings without reporting them?\n", "3. Which API objects would you log in a reproducibility appendix?\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Takeaways\n", + "\n", + "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index ec4d76e..d84ed90 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -21,6 +21,15 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebook map\n", + "\n", + "This notebook is a complete reference for reproducible EngiOpt training and artifact creation against EngiBench.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -90,6 +99,18 @@ "- The **key research question** here is not just reconstruction quality, but whether generated designs remain feasible and competitive under benchmark simulation.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1 - Configure Reproducible Environment\n", + "\n", + "**Purpose:** centralize runtime flags, seeds, and artifact locations.\n", + "**Inputs:** notebook config toggles.\n", + "**Success check:** device and artifact directory print correctly.\n", + "**Failure mode:** missing `engiopt` import; rerun bootstrap/restart runtime.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -175,6 +196,18 @@ "These diagnostics are useful for comparing methods across papers, not only for this workshop.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2 - Build Training Slice from EngiBench Dataset\n", + "\n", + "**Purpose:** create a time-bounded workshop subset from the benchmark dataset.\n", + "**Inputs:** `problem.dataset`, `condition_keys`, `N_TRAIN`.\n", + "**Success check:** condition/design arrays have expected shapes.\n", + "**Discussion:** how does subset size affect scientific validity?\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -202,6 +235,17 @@ "print('target range:', float(targets_np.min()), 'to', float(targets_np.max()))\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3 - Define EngiOpt Model and Optimization Objects\n", + "\n", + "**Purpose:** instantiate the generator and optimization components.\n", + "**Inputs:** condition dimensionality and design shape from EngiBench.\n", + "**Success check:** model initializes on CPU/GPU and noise sampler returns expected shape.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -222,6 +266,17 @@ " return th.randn((batch_size, LATENT_DIM), device=DEVICE, dtype=th.float32)\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4 - Train (or Load) with Diagnostics\n", + "\n", + "**Purpose:** fit the generator and record diagnostics for reproducibility.\n", + "**Success check:** checkpoint/history/curve files are created.\n", + "**Failure mode:** unstable loss; reduce learning rate or verify target scaling.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -346,6 +401,17 @@ "3. Which artifacts are necessary for independent re-evaluation?\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 5 - Generate Conditioned Designs\n", + "\n", + "**Purpose:** generate candidate designs for held-out test conditions.\n", + "**Success check:** generated and baseline arrays align in shape.\n", + "**Discussion:** do generated designs look diverse or mode-collapsed?\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -383,6 +449,17 @@ "print('generated violation ratio (quick check):', float(viol_ratio))\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 6 - Export Artifact Contract\n", + "\n", + "**Purpose:** persist a strict handoff contract for Notebook 02 evaluation.\n", + "**Required outputs:** generated/baseline arrays + conditions JSON.\n", + "**Optional outputs:** W&B artifact bundle for remote recovery.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -432,6 +509,16 @@ " print('W&B upload failed (continuing with local artifacts only):', exc)\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 7 - Quick Visual QA\n", + "\n", + "**Purpose:** catch obvious degeneration before simulator evaluation.\n", + "**Success check:** generated structures are not uniformly blank/noisy.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -472,6 +559,15 @@ "\n", "Proceed to **Notebook 02** for full physics-based evaluation and metric reporting.\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Takeaways\n", + "\n", + "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 712bf8e..d39afb0 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -21,6 +21,15 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebook map\n", + "\n", + "This notebook is a complete reference for robust evaluation beyond objective-only reporting.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -87,6 +96,16 @@ "trade-offs, and potential benchmark blind spots.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1 - Resolve Artifact Source and Recovery Path\n", + "\n", + "**Purpose:** load Notebook 01 outputs robustly (local/W&B/auto-build).\n", + "**Success check:** artifact directory and sample counts print correctly.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -303,6 +322,16 @@ "- store a row for later aggregate analysis.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2 - Run Per-Sample Physics Evaluation\n", + "\n", + "**Purpose:** evaluate generated vs baseline fairly under identical simulator conditions.\n", + "**Success check:** one results row per sample.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -336,6 +365,16 @@ "results.head()\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3 - Compute Benchmark Metrics\n", + "\n", + "**Purpose:** quantify objective, feasibility, diversity, and novelty.\n", + "**Discussion:** which metric changes your conclusion most?\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -385,6 +424,16 @@ "summary_df\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4 - Export Evidence Artifacts\n", + "\n", + "**Purpose:** save tables and figures for reporting and reproducibility.\n", + "**Optional:** push evaluation package to W&B.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -451,6 +500,15 @@ " print('W&B evaluation logging failed (continuing locally):', exc)\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 5 - Visual Comparison for Interpretation\n", + "\n", + "Pairwise generated-vs-baseline visuals help interpret metric outliers.\n" + ] + }, { "cell_type": "code", "metadata": {}, @@ -509,6 +567,15 @@ "- W&B pull fails: disable `USE_WANDB_ARTIFACTS` and use local path.\n", "- Objective comparison seems inconsistent: ensure simulator reset is called before each run.\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Takeaways\n", + "\n", + "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 1c6b141..69fc668 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -13,6 +13,15 @@ "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebook map\n", + "\n", + "This notebook provides a full minimal scaffold and validation flow for new benchmark problems.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -67,6 +76,15 @@ " print('Skipping install (using current environment).')\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1 - Import Scaffold Dependencies\n", + "\n", + "These define the minimal benchmark interface contract.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -74,6 +92,15 @@ "outputs": [], "source": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport numpy as np\nfrom gymnasium import spaces\n\nfrom engibench.constraint import bounded\nfrom engibench.core import ObjectiveDirection\nfrom engibench.core import OptiStep\nfrom engibench.core import Problem\n" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2 - Implement Minimal Problem Contract\n", + "\n", + "Focus on deterministic behavior and explicit objective semantics.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -81,6 +108,15 @@ "outputs": [], "source": "class ToyDensityProblem(Problem[np.ndarray]):\n \"\"\"Minimal toy problem scaffold for workshop teaching.\"\"\"\n\n version = 0\n objectives = ((\"toy_cost\", ObjectiveDirection.MINIMIZE),)\n\n @dataclass\n class Conditions:\n target_density: Annotated[float, bounded(lower=0.0, upper=1.0)] = 0.5\n\n @dataclass\n class Config(Conditions):\n resolution: Annotated[int, bounded(lower=4, upper=128)] = 16\n max_iter: Annotated[int, bounded(lower=1, upper=200)] = 20\n\n dataset_id = \"IDEALLab/beams_2d_50_100_v0\" # placeholder to satisfy scaffold\n container_id = None\n\n def __init__(self, seed: int = 0, **kwargs):\n super().__init__(seed=seed)\n self.config = self.Config(**kwargs)\n self.conditions = self.Conditions(target_density=self.config.target_density)\n self.design_space = spaces.Box(\n low=0.0, high=1.0, shape=(self.config.resolution, self.config.resolution), dtype=np.float32\n )\n\n def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n cfg = {\"target_density\": self.config.target_density, **(config or {})}\n # Toy objective: mismatch to target density + smoothness penalty\n density_term = abs(float(design.mean()) - float(cfg[\"target_density\"]))\n smoothness = float(np.mean(np.abs(np.diff(design, axis=0))))\n return np.array([density_term + 0.1 * smoothness], dtype=np.float32)\n\n def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n cfg = {\"target_density\": self.config.target_density, **(config or {})}\n x = starting_point.copy().astype(np.float32)\n hist = []\n for step in range(self.config.max_iter):\n # Toy update toward target density (not a real optimizer)\n x = np.clip(x + 0.2 * (cfg[\"target_density\"] - x), 0.0, 1.0)\n hist.append(OptiStep(obj_values=self.simulate(x, cfg), step=step))\n return x, hist\n\n def render(self, design: np.ndarray, *, open_window: bool = False):\n import matplotlib.pyplot as plt\n\n fig, ax = plt.subplots(figsize=(4, 4))\n ax.imshow(design, cmap=\"viridis\", vmin=0, vmax=1)\n ax.set_title(\"ToyDensityProblem design\")\n ax.axis(\"off\")\n if open_window:\n plt.show()\n return fig, ax\n\n def random_design(self):\n d = self.np_random.random(self.design_space.shape).astype(np.float32)\n return d, -1\n" }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3 - Smoke-Test the Scaffold\n", + "\n", + "Validate simulator loop and constraint checks end-to-end.\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -114,6 +150,15 @@ "- If class TODOs fail, implement methods incrementally and run one check cell at a time.\n", "- Keep seed handling deterministic so your scaffold behavior is reproducible.\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Takeaways\n", + "\n", + "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + ] } ], "metadata": { From cee13cd8e7211f397393f7beab9d79032e92a139 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Tue, 10 Mar 2026 11:09:26 +0100 Subject: [PATCH 21/37] Polish notebook narrative for professional workshop delivery --- .../participant/00_setup_api_warmup.ipynb | 89 ++++++++------ .../dcc26/participant/01_train_generate.ipynb | 83 ++++++------- .../participant/02_evaluate_metrics.ipynb | 74 +++++------ .../03_add_new_problem_scaffold.ipynb | 66 +++++----- .../dcc26/solutions/00_setup_api_warmup.ipynb | 81 ++++++------ .../dcc26/solutions/01_train_generate.ipynb | 115 +++++++----------- .../dcc26/solutions/02_evaluate_metrics.ipynb | 90 ++++++-------- .../03_add_new_problem_scaffold.ipynb | 66 +++++----- 8 files changed, 308 insertions(+), 356 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 94bb46e..12b801a 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -3,14 +3,18 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Notebook 00 (Participant): Setup + API Warmup\n\nComplete the `TODO` sections.\n\nGoal: use `Beams2D` API to inspect problem metadata, sample data, rendering, and constraints.\n" + "source": [ + "# Notebook 00 (Participant): Setup + API Warmup\n", + "\n", + "This chapter introduces the benchmark interface itself.\n", + "Your target is to read a problem definition as a reproducible scientific object, not just an API object.\n" + ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", - "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" ] }, { @@ -19,7 +23,12 @@ "source": [ "## Notebook map\n", "\n", - "Use this notebook as a standalone guide to understanding the EngiBench API contract before modeling.\n" + "This notebook is written as a standalone lab chapter:\n", + "- context first,\n", + "- implementation second,\n", + "- interpretation third.\n", + "\n", + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" ] }, { @@ -28,16 +37,10 @@ "source": [ "## Standalone guide\n", "\n", - "**What you will learn**\n", - "- How EngiBench packages an engineering benchmark (`problem`, `dataset`, `constraints`, `simulate`).\n", - "- How to inspect design variables, condition variables, and objective context.\n", - "\n", - "**Expected runtime**\n", - "- 5-15 minutes on Colab CPU.\n", - "\n", - "**If you start here without the workshop talk**\n", - "- Read each printed object shape/key carefully.\n", - "- Focus on what is benchmark-fixed (problem API) vs method-flexible (model choice).\n" + "Learning goals:\n", + "- identify what EngiBench fixes (problem contract),\n", + "- identify what researchers can vary (methods),\n", + "- inspect conditions, objectives, and constraints with reproducibility in mind.\n" ] }, { @@ -46,17 +49,19 @@ "source": [ "## Why this warmup matters\n", "\n", - "- **EngiBench** provides the benchmark contract: problem definition, conditions, objectives, constraints, simulator, and dataset.\n", - "- **EngiOpt** provides optimization/generative methods that operate against that contract.\n", - "- In this workshop, we separate these concerns so model innovation and evaluation stay reproducible.\n", - "\n", - "By the end of this notebook, you should be able to inspect what is fixed by the benchmark and what is variable in your method design.\n" + "In engineering-design ML, many apparent gains come from hidden evaluation differences.\n", + "This warmup is about controlling that risk: understanding exactly what is held constant by the benchmark.\n" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Optional install cell (fresh Colab)\n\nUncomment if your runtime does not already contain required packages.\n" + "source": [ + "## Optional install cell (fresh Colab)\n", + "\n", + "Run this cell only on a fresh Colab runtime or if imports fail.\n", + "Local environments can usually skip it.\n" + ] }, { "cell_type": "code", @@ -84,9 +89,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 1 - Initialize Reproducible Session\n", + "### Step 1 - Initialize reproducible session\n", "\n", - "Run seed/version setup first.\n" + "Set seed and print versions.\n", + "If this step is inconsistent across machines, downstream comparisons are not interpretable.\n" ] }, { @@ -100,9 +106,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Instantiate Benchmark Problem (TODO)\n", + "### Step 2 - Instantiate the benchmark problem (TODO)\n", "\n", - "Inspect API objects after completion.\n" + "Create `Beams2D` and inspect its key fields.\n", + "Checkpoint: you should be able to explain which attributes define the benchmark contract.\n" ] }, { @@ -116,9 +123,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 3 - Inspect Dataset Structure (TODO)\n", + "### Step 3 - Inspect dataset structure (TODO)\n", "\n", - "Confirm sample keys and shapes.\n" + "Load one train/test sample and inspect keys and shapes.\n", + "Checkpoint: confirm condition variables and design representation are explicit.\n" ] }, { @@ -132,9 +140,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 4 - Visualize One Benchmark Design\n", + "### Step 4 - Visualize one benchmark design\n", "\n", - "Check if rendering matches expectation.\n" + "Render a sample and interpret what visual features correspond to feasible structure.\n" ] }, { @@ -148,9 +156,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 5 - Test Constraint Semantics (TODO)\n", + "### Step 5 - Test constraint semantics (TODO)\n", "\n", - "Explain why violation output makes sense.\n" + "Run a deliberate mismatch case.\n", + "Checkpoint: verify the violation output is understandable and actionable.\n" ] }, { @@ -166,14 +175,18 @@ "source": [ "## Troubleshooting\n", "\n", - "- If dataset loading hangs, rerun the cell once (first fetch may be slower).\n", - "- If plotting fails in local Jupyter, ensure `matplotlib` backend is available.\n" + "If a section fails, do not continue downstream. Fix locally first, then rerun the section and its immediate checks.\n", + "This notebook is intentionally staged so failures are localized.\n" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Next\n\nContinue with **Notebook 01** to train a lightweight conditional generator and create designs for evaluation.\n" + "source": [ + "## Next\n", + "\n", + "Proceed to Notebook 01 to connect this benchmark interface to a concrete generative model pipeline.\n" + ] }, { "cell_type": "markdown", @@ -181,9 +194,8 @@ "source": [ "## Reflection prompts\n", "\n", - "1. Which fields in `problem` are benchmark-defining vs method-defining?\n", - "2. What can go wrong if a paper changes simulator settings without reporting them?\n", - "3. Which API objects would you log in a reproducibility appendix?\n" + "- Which benchmark fields must be reported in every paper for fair comparison?\n", + "- Which hidden defaults are most likely to create accidental unfairness?\n" ] }, { @@ -192,7 +204,10 @@ "source": [ "## Takeaways\n", "\n", - "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + "Before closing, record three points:\n", + "1. What conclusion is directly supported by your metrics?\n", + "2. What remains uncertain (and why)?\n", + "3. What extra experiment would you run next to reduce that uncertainty?\n" ] } ], diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 13f168a..1c2c979 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -6,15 +6,14 @@ "source": [ "# Notebook 01 (Participant): Train + Generate with EngiOpt CGAN-2D\n", "\n", - "Goal: implement the core pipeline to produce reproducible artifacts for Notebook 02.\n" + "You will implement the full train-and-generate path and produce artifacts consumed by Notebook 02.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", - "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" ] }, { @@ -23,7 +22,12 @@ "source": [ "## Notebook map\n", "\n", - "This notebook teaches the full train-generate artifact workflow with TODO checkpoints for independent learners.\n" + "This notebook is written as a standalone lab chapter:\n", + "- context first,\n", + "- implementation second,\n", + "- interpretation third.\n", + "\n", + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" ] }, { @@ -32,21 +36,8 @@ "source": [ "## Standalone guide\n", "\n", - "**What you will learn**\n", - "- How to train an EngiOpt generator against an EngiBench dataset slice.\n", - "- How to export reproducible artifacts for downstream evaluation.\n", - "\n", - "**Expected runtime**\n", - "- ~10-20 minutes on Colab CPU for default settings.\n", - "\n", - "**Outputs produced**\n", - "- `generated_designs.npy`, `baseline_designs.npy`, `conditions.json`\n", - "- `engiopt_cgan2d_generator_supervised.pt`\n", - "- `training_history.csv`, `training_curve.png`\n", - "\n", - "**If results look poor**\n", - "- Check training loss trend first.\n", - "- Then inspect feasibility ratio in Notebook 02 before changing architecture.\n" + "This chapter is about **method integration under benchmark constraints**.\n", + "Success means reproducible artifacts and interpretable diagnostics, not only low training loss.\n" ] }, { @@ -80,8 +71,7 @@ "source": [ "## Part A: Setup\n", "\n", - "You can complete this notebook without W&B.\n", - "If you want remote artifacts, set `USE_WANDB_ARTIFACTS=True` and log in with `wandb.login()`.\n" + "Lock down runtime, seeds, and artifact paths before writing model logic.\n" ] }, { @@ -90,18 +80,19 @@ "source": [ "### EngiBench vs EngiOpt roles in this notebook\n", "\n", - "- `Beams2D` (EngiBench): defines dataset fields, conditions, constraints, and simulator-based objective.\n", - "- `Generator` (EngiOpt): maps condition vectors + latent noise to candidate designs.\n", - "- The **key research question** here is not just reconstruction quality, but whether generated designs remain feasible and competitive under benchmark simulation.\n" + "- EngiBench: defines data semantics, constraints, and simulator objective.\n", + "- EngiOpt: defines the generative model family and training dynamics.\n", + "\n", + "Keep this separation explicit in your reasoning and reporting.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 1 - Configure Reproducible Environment\n", + "### Step 1 - Configure reproducible environment\n", "\n", - "Follow the printed checks to confirm your setup before implementing TODOs.\n" + "Set all global controls once; downstream cells should rely on these values only.\n" ] }, { @@ -170,9 +161,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Build Training Slice from EngiBench Dataset\n", + "### Step 2 - Build training slice from EngiBench dataset\n", "\n", - "Use this as fixed benchmark input; your method logic starts in TODO cells.\n" + "Use a compact subset for workshop runtime; treat this as a pedagogical approximation, not final benchmark protocol.\n" ] }, { @@ -204,9 +195,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 3 - Implement Model Setup (TODO)\n", + "### Step 3 - Implement model setup (TODO)\n", "\n", - "Fill TODO 1, then run the completion check in the cell comments.\n" + "Instantiate generator, optimizer, and loss exactly once.\n", + "Checkpoint: noise and condition tensors must align with model input dimensions.\n" ] }, { @@ -231,9 +223,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 4 - Implement Train/Load Logic (TODO)\n", + "### Step 4 - Implement train/load logic (TODO)\n", "\n", - "Fill TODO 2 and verify checkpoint/history artifacts are created.\n" + "Track loss per epoch and persist checkpoint/history outputs.\n", + "Checkpoint: you can reload and run generation without retraining.\n" ] }, { @@ -267,9 +260,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 5 - Implement Generation Logic (TODO)\n", + "### Step 5 - Implement generation logic (TODO)\n", "\n", - "Ensure generated/baseline shapes match and condition records are complete.\n" + "Generate conditioned designs on held-out test conditions.\n", + "Checkpoint: generated and baseline arrays are shape-compatible.\n" ] }, { @@ -295,9 +289,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 6 - Implement Artifact Export (TODO)\n", + "### Step 6 - Implement artifact export (TODO)\n", "\n", - "Notebook 02 depends on these exact files; treat them as API outputs.\n" + "Notebook 02 expects these files as a strict handoff contract.\n", + "Treat artifact naming/format as part of the benchmark interface.\n" ] }, { @@ -323,9 +318,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 7 - Quick Visual QA\n", + "### Step 7 - Quick visual QA\n", "\n", - "Use this to debug generation issues before full evaluation.\n" + "Use this for fast sanity checks only; final judgment comes from Notebook 02 simulator metrics.\n" ] }, { @@ -355,9 +350,8 @@ "source": [ "## Troubleshooting\n", "\n", - "- `ModuleNotFoundError: engiopt...`: rerun bootstrap cell; on Colab, Runtime -> Restart runtime.\n", - "- No artifact files found later: verify export cell completed and files printed from `ARTIFACT_DIR`.\n", - "- Training too slow: reduce `EPOCHS` or `N_TRAIN`, but note this affects quality.\n" + "If a section fails, do not continue downstream. Fix locally first, then rerun the section and its immediate checks.\n", + "This notebook is intentionally staged so failures are localized.\n" ] }, { @@ -366,7 +360,7 @@ "source": [ "## Next\n", "\n", - "Continue with **Notebook 02** to run physics evaluation and benchmark metrics.\n" + "Proceed to Notebook 02 for physics-based evaluation and benchmark interpretation.\n" ] }, { @@ -375,7 +369,10 @@ "source": [ "## Takeaways\n", "\n", - "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + "Before closing, record three points:\n", + "1. What conclusion is directly supported by your metrics?\n", + "2. What remains uncertain (and why)?\n", + "3. What extra experiment would you run next to reduce that uncertainty?\n" ] } ], diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index 2f5c5d6..38942f8 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -6,15 +6,14 @@ "source": [ "# Notebook 02 (Participant): Evaluation + Metrics\n", "\n", - "Goal: implement evaluation logic and interpret benchmark outcomes beyond objective value.\n" + "You will implement the evaluation layer and produce the evidence used for scientific comparison.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", - "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" ] }, { @@ -23,7 +22,12 @@ "source": [ "## Notebook map\n", "\n", - "This notebook guides full benchmark evaluation with TODOs and interpretation prompts.\n" + "This notebook is written as a standalone lab chapter:\n", + "- context first,\n", + "- implementation second,\n", + "- interpretation third.\n", + "\n", + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" ] }, { @@ -32,16 +36,7 @@ "source": [ "## Standalone guide\n", "\n", - "**What you will learn**\n", - "- How to evaluate generated designs with constraint checks + simulator objective.\n", - "- How to summarize performance with objective, feasibility, diversity, and novelty proxies.\n", - "\n", - "**Expected runtime**\n", - "- 10-25 minutes depending on whether artifacts must be rebuilt.\n", - "\n", - "**If artifacts are missing**\n", - "- Notebook can auto-build them locally (`AUTO_BUILD_ARTIFACTS_IF_MISSING=True`).\n", - "- Optional: enable W&B artifact download.\n" + "This chapter answers: *did the generated designs actually improve engineering outcomes under benchmark simulation?*\n" ] }, { @@ -75,9 +70,8 @@ "source": [ "## Artifact loading\n", "\n", - "This notebook can run standalone:\n", - "- if Notebook 01 artifacts exist, it loads them,\n", - "- otherwise it can auto-build artifacts locally using EngiOpt.\n" + "Load artifacts from Notebook 01, optional W&B, or local auto-build fallback.\n", + "Do not proceed until artifact shapes/configs are confirmed.\n" ] }, { @@ -86,18 +80,17 @@ "source": [ "### Why these metrics matter for benchmarking\n", "\n", - "Objective value alone is not enough for engineering design benchmarks.\n", - "We also track feasibility, diversity, and novelty proxies to reason about method behavior,\n", - "trade-offs, and potential benchmark blind spots.\n" + "Objective alone can overstate progress.\n", + "We include feasibility, diversity, and novelty proxies to reduce that blind spot.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 1 - Resolve Artifact Source and Recovery Path\n", + "### Step 1 - Resolve artifact source and recovery path\n", "\n", - "If Notebook 01 artifacts are missing, keep auto-build enabled for a self-contained run.\n" + "Checkpoint: you can load `generated`, `baseline`, and `conditions` with matching sample counts.\n" ] }, { @@ -308,9 +301,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Implement Per-Sample Evaluation (TODO)\n", + "### Step 2 - Implement per-sample evaluation (TODO)\n", "\n", - "Fill TODO 1 and confirm one `results` row per sample.\n" + "Compute constraints and simulator objective for generated and baseline designs under identical settings.\n" ] }, { @@ -341,9 +334,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 3 - Implement Summary Metrics (TODO)\n", + "### Step 3 - Implement summary metrics (TODO)\n", "\n", - "Include objective, feasibility, diversity, and novelty summaries.\n" + "Report objective, feasibility, diversity, and novelty summaries in one table.\n" ] }, { @@ -393,9 +386,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 4 - Export Evidence Artifacts\n", + "### Step 4 - Export evidence artifacts\n", "\n", - "Confirm CSV and figure files are saved to `ARTIFACT_DIR`.\n" + "Persist outputs as audit-ready artifacts, not ephemeral notebook prints.\n" ] }, { @@ -452,9 +445,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 5 - Visual Comparison for Interpretation\n", + "### Step 5 - Visual comparison for interpretation\n", "\n", - "Use this to interpret disagreements between scalar metrics and visual quality.\n" + "Use visuals to interpret outliers and reconcile metric-level contradictions.\n" ] }, { @@ -487,9 +480,8 @@ "source": [ "## Interpretation hints\n", "\n", - "- Strong objective performance with poor feasibility is not deployment-ready.\n", - "- Diversity + novelty are useful for discussing benchmark completeness.\n", - "- Compare your summary metrics with peers during the breakout discussion.\n" + "Strong results usually balance objective quality **and** feasibility.\n", + "Use diversity/novelty to discuss exploration vs imitation behavior.\n" ] }, { @@ -498,10 +490,8 @@ "source": [ "## Discussion bridge to workshop breakout\n", "\n", - "Use your results to discuss:\n", - "1. Which metric changed your interpretation most (objective vs feasibility vs diversity)?\n", - "2. Which additional engineering criterion is still missing from this benchmark?\n", - "3. How should we report uncertainty/runtime when comparing generative methods?\n" + "Bring one concrete claim and one uncertainty to discussion.\n", + "Example: “Objective improved, but feasibility degraded under stricter conditions.”\n" ] }, { @@ -510,9 +500,8 @@ "source": [ "## Troubleshooting\n", "\n", - "- Missing artifacts: keep `AUTO_BUILD_ARTIFACTS_IF_MISSING=True` or run Notebook 01 first.\n", - "- W&B pull fails: disable `USE_WANDB_ARTIFACTS` and use local path.\n", - "- Objective comparison seems inconsistent: ensure simulator reset is called before each run.\n" + "If a section fails, do not continue downstream. Fix locally first, then rerun the section and its immediate checks.\n", + "This notebook is intentionally staged so failures are localized.\n" ] }, { @@ -521,7 +510,10 @@ "source": [ "## Takeaways\n", "\n", - "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + "Before closing, record three points:\n", + "1. What conclusion is directly supported by your metrics?\n", + "2. What remains uncertain (and why)?\n", + "3. What extra experiment would you run next to reduce that uncertainty?\n" ] } ], diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 4ea4726..c19f70b 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -3,14 +3,17 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Notebook 03 (Participant): Add a New Problem Scaffold\n\nComplete the `TODO` sections to build a minimal EngiBench-compatible toy problem.\n" + "source": [ + "# Notebook 03 (Participant): Add a New Problem Scaffold\n", + "\n", + "You will implement a minimal problem contract and evaluate whether it is benchmark-ready.\n" + ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", - "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" ] }, { @@ -19,7 +22,12 @@ "source": [ "## Notebook map\n", "\n", - "Use this as a standalone template for implementing a new benchmark problem contract.\n" + "This notebook is written as a standalone lab chapter:\n", + "- context first,\n", + "- implementation second,\n", + "- interpretation third.\n", + "\n", + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" ] }, { @@ -28,15 +36,7 @@ "source": [ "## Standalone guide\n", "\n", - "**What you will learn**\n", - "- Which `Problem` components are required for a reusable benchmark scaffold.\n", - "- How to think about benchmark quality beyond “code runs”.\n", - "\n", - "**Expected runtime**\n", - "- 10-20 minutes.\n", - "\n", - "**Outcome**\n", - "- A toy scaffold template you can map to a real domain and contribution-ready checklist.\n" + "This chapter is about benchmark design quality, not model training speed.\n" ] }, { @@ -45,13 +45,7 @@ "source": [ "## What makes a new problem benchmark-ready\n", "\n", - "A useful benchmark problem should make the following explicit:\n", - "- Design representation and constraints (what is feasible)\n", - "- Condition space (what is being requested)\n", - "- Objective(s) and simulator semantics (what is optimized)\n", - "- Dataset protocol (train/test split, provenance, leakage risks)\n", - "\n", - "This notebook uses a toy scaffold so you can map these requirements to your own domain later.\n" + "A publishable benchmark needs explicit representation, constraints, objectives, simulator semantics, and reproducibility metadata.\n" ] }, { @@ -80,9 +74,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 1 - Import Scaffold Dependencies\n", + "### Step 1 - Import scaffold dependencies\n", "\n", - "Use these imports to complete the TODO skeleton.\n" + "Ensure all required interfaces are visible before class implementation.\n" ] }, { @@ -96,9 +90,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Implement Minimal Problem Contract (TODO)\n", + "### Step 2 - Implement minimal problem contract (TODO)\n", "\n", - "Complete required methods incrementally.\n" + "Complete each required method with deterministic behavior and clear failure messages.\n" ] }, { @@ -112,9 +106,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 3 - Smoke-Test Your Scaffold\n", + "### Step 3 - Smoke-test your scaffold\n", "\n", - "Run checks to validate your implementation.\n" + "Run minimal checks to verify interface consistency and simulator behavior.\n" ] }, { @@ -127,7 +121,11 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## Mapping to real EngiBench contributions\n\nThis toy scaffold demonstrates the interface shape only. For real contributions:\n\n1. Create `engibench/problems//v0.py`\n2. Implement real `simulate` and (optionally) `optimize`\n3. Provide `conditions`, `design_space`, and `dataset_id`\n4. Add docs and tests\n\nReference docs: [Adding a new problem](https://github.com/IDEALLab/EngiBench/blob/main/docs/tutorials/new_problem.md)\n" + "source": [ + "## Mapping to real EngiBench contributions\n", + "\n", + "Translate this toy scaffold into domain-specific simulators and datasets with documented assumptions.\n" + ] }, { "cell_type": "markdown", @@ -135,10 +133,7 @@ "source": [ "## Contribution checklist\n", "\n", - "- Define clear simulator I/O and units.\n", - "- Add deterministic seeds for repeatability.\n", - "- Document constraint semantics and failure modes.\n", - "- Provide at least one baseline and expected metric outputs.\n" + "Before proposing a new problem, verify data provenance, split policy, evaluation protocol, and reporting templates.\n" ] }, { @@ -147,8 +142,8 @@ "source": [ "## Troubleshooting\n", "\n", - "- If class TODOs fail, implement methods incrementally and run one check cell at a time.\n", - "- Keep seed handling deterministic so your scaffold behavior is reproducible.\n" + "If a section fails, do not continue downstream. Fix locally first, then rerun the section and its immediate checks.\n", + "This notebook is intentionally staged so failures are localized.\n" ] }, { @@ -157,7 +152,10 @@ "source": [ "## Takeaways\n", "\n", - "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + "Before closing, record three points:\n", + "1. What conclusion is directly supported by your metrics?\n", + "2. What remains uncertain (and why)?\n", + "3. What extra experiment would you run next to reduce that uncertainty?\n" ] } ], diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index 743e6b8..14428f8 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -3,14 +3,17 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Notebook 00: Setup + API Warmup (DCC26)\n\nThis notebook covers the first workshop segment (10-15 minutes):\n\n1. Environment checks\n2. EngiBench problem instantiation (`Beams2D`)\n3. Design space / objectives / conditions inspection\n4. Dataset sample inspection\n5. Rendering and one explicit constraint check\n\nExpected outcome: participants can navigate the full EngiBench problem API without W&B setup.\n" + "source": [ + "# Notebook 00: Setup + API Warmup (DCC26)\n", + "\n", + "Reference walkthrough for inspecting EngiBench as a reproducible benchmark contract.\n" + ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", - "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" ] }, { @@ -19,7 +22,12 @@ "source": [ "## Notebook map\n", "\n", - "This notebook is a complete reference walkthrough of EngiBench API semantics for Beams2D.\n" + "This notebook is written as a standalone lab chapter:\n", + "- context first,\n", + "- implementation second,\n", + "- interpretation third.\n", + "\n", + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" ] }, { @@ -28,16 +36,8 @@ "source": [ "## Standalone guide\n", "\n", - "**What you will learn**\n", - "- How EngiBench packages an engineering benchmark (`problem`, `dataset`, `constraints`, `simulate`).\n", - "- How to inspect design variables, condition variables, and objective context.\n", - "\n", - "**Expected runtime**\n", - "- 5-15 minutes on Colab CPU.\n", - "\n", - "**If you start here without the workshop talk**\n", - "- Read each printed object shape/key carefully.\n", - "- Focus on what is benchmark-fixed (problem API) vs method-flexible (model choice).\n" + "Use this notebook as the baseline interpretation layer before any model training.\n", + "The objective is conceptual correctness, not speed.\n" ] }, { @@ -46,17 +46,18 @@ "source": [ "## Why this warmup matters\n", "\n", - "- **EngiBench** provides the benchmark contract: problem definition, conditions, objectives, constraints, simulator, and dataset.\n", - "- **EngiOpt** provides optimization/generative methods that operate against that contract.\n", - "- In this workshop, we separate these concerns so model innovation and evaluation stay reproducible.\n", - "\n", - "By the end of this notebook, you should be able to inspect what is fixed by the benchmark and what is variable in your method design.\n" + "Most benchmarking disagreements come from interface misunderstandings, not algorithmic novelty.\n", + "This chapter removes that ambiguity up front.\n" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Optional install cell (fresh Colab)\n\nUncomment if your runtime does not already contain required packages.\n" + "source": [ + "## Optional install cell (fresh Colab)\n", + "\n", + "Only required on fresh or reset Colab runtimes.\n" + ] }, { "cell_type": "code", @@ -84,9 +85,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 1 - Initialize Reproducible Session\n", + "### Step 1 - Initialize reproducible session\n", "\n", - "Set seeds and inspect runtime context before touching the benchmark API.\n" + "Seed and version information define your execution context.\n" ] }, { @@ -100,9 +101,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Instantiate Benchmark Problem\n", + "### Step 2 - Instantiate benchmark problem\n", "\n", - "Inspect `design_space`, `condition_space`, and objective metadata.\n" + "Inspect problem metadata and ensure the design/condition/objective contract is explicit.\n" ] }, { @@ -116,9 +117,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 3 - Inspect Dataset Structure\n", + "### Step 3 - Inspect dataset structure\n", "\n", - "Validate train/test splits and sample fields used by downstream notebooks.\n" + "Confirm split semantics and field consistency before using this data in modeling notebooks.\n" ] }, { @@ -132,9 +133,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 4 - Visualize One Benchmark Design\n", + "### Step 4 - Visualize one benchmark design\n", "\n", - "Interpret what the representation encodes physically.\n" + "Use rendering to align numerical representation with engineering intuition.\n" ] }, { @@ -148,9 +149,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 5 - Test Constraint Semantics\n", + "### Step 5 - Test constraint semantics\n", "\n", - "Use an intentionally mismatched config to understand violation reporting behavior.\n" + "A controlled violation case clarifies what `check_constraints` is actually diagnosing.\n" ] }, { @@ -166,14 +167,18 @@ "source": [ "## Troubleshooting\n", "\n", - "- If dataset loading hangs, rerun the cell once (first fetch may be slower).\n", - "- If plotting fails in local Jupyter, ensure `matplotlib` backend is available.\n" + "If a section fails, do not continue downstream. Fix locally first, then rerun the section and its immediate checks.\n", + "This notebook is intentionally staged so failures are localized.\n" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Next\n\nContinue with **Notebook 01** to train a lightweight conditional generator and create designs for evaluation.\n" + "source": [ + "## Next\n", + "\n", + "Continue with Notebook 01 for model integration and artifact generation.\n" + ] }, { "cell_type": "markdown", @@ -181,9 +186,8 @@ "source": [ "## Reflection prompts\n", "\n", - "1. Which fields in `problem` are benchmark-defining vs method-defining?\n", - "2. What can go wrong if a paper changes simulator settings without reporting them?\n", - "3. Which API objects would you log in a reproducibility appendix?\n" + "- What would make two reported results incomparable on this benchmark?\n", + "- Which configuration fields are non-negotiable in method reporting?\n" ] }, { @@ -192,7 +196,10 @@ "source": [ "## Takeaways\n", "\n", - "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + "Before closing, record three points:\n", + "1. What conclusion is directly supported by your metrics?\n", + "2. What remains uncertain (and why)?\n", + "3. What extra experiment would you run next to reduce that uncertainty?\n" ] } ], diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index d84ed90..0ad9aa0 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -6,19 +6,14 @@ "source": [ "# Notebook 01: Train + Generate with EngiOpt CGAN-2D (DCC26)\n", "\n", - "This notebook maps to the **00:30-01:00** workshop block:\n", - "1. Load an EngiBench problem/dataset (`Beams2D`)\n", - "2. Train an EngiOpt generator on a compact subset\n", - "3. Generate condition-driven designs\n", - "4. Export reproducible artifacts for Notebook 02\n" + "Reference implementation for method integration, diagnostics, and artifact contract creation.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", - "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" ] }, { @@ -27,7 +22,12 @@ "source": [ "## Notebook map\n", "\n", - "This notebook is a complete reference for reproducible EngiOpt training and artifact creation against EngiBench.\n" + "This notebook is written as a standalone lab chapter:\n", + "- context first,\n", + "- implementation second,\n", + "- interpretation third.\n", + "\n", + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" ] }, { @@ -36,21 +36,8 @@ "source": [ "## Standalone guide\n", "\n", - "**What you will learn**\n", - "- How to train an EngiOpt generator against an EngiBench dataset slice.\n", - "- How to export reproducible artifacts for downstream evaluation.\n", - "\n", - "**Expected runtime**\n", - "- ~10-20 minutes on Colab CPU for default settings.\n", - "\n", - "**Outputs produced**\n", - "- `generated_designs.npy`, `baseline_designs.npy`, `conditions.json`\n", - "- `engiopt_cgan2d_generator_supervised.pt`\n", - "- `training_history.csv`, `training_curve.png`\n", - "\n", - "**If results look poor**\n", - "- Check training loss trend first.\n", - "- Then inspect feasibility ratio in Notebook 02 before changing architecture.\n" + "This notebook is designed to be publication-grade reproducibility scaffolding:\n", + "clear controls, explicit artifacts, and interpretable training diagnostics.\n" ] }, { @@ -82,10 +69,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Part A: Configuration and Runtime Controls\n", + "## Part A: Configuration and runtime controls\n", "\n", - "- `USE_WANDB_ARTIFACTS=False` keeps the default path account-free.\n", - "- Set it to `True` if you want to test artifact upload and training logs.\n" + "Establish deterministic setup and artifact policy before touching model code.\n" ] }, { @@ -94,21 +80,17 @@ "source": [ "### EngiBench vs EngiOpt roles in this notebook\n", "\n", - "- `Beams2D` (EngiBench): defines dataset fields, conditions, constraints, and simulator-based objective.\n", - "- `Generator` (EngiOpt): maps condition vectors + latent noise to candidate designs.\n", - "- The **key research question** here is not just reconstruction quality, but whether generated designs remain feasible and competitive under benchmark simulation.\n" + "EngiBench provides the benchmark contract; EngiOpt provides the method implementation.\n", + "Conflating these layers is a common source of irreproducible claims.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 1 - Configure Reproducible Environment\n", + "### Step 1 - Configure reproducible environment\n", "\n", - "**Purpose:** centralize runtime flags, seeds, and artifact locations.\n", - "**Inputs:** notebook config toggles.\n", - "**Success check:** device and artifact directory print correctly.\n", - "**Failure mode:** missing `engiopt` import; rerun bootstrap/restart runtime.\n" + "Seed control and path control are first-class experimental settings, not boilerplate.\n" ] }, { @@ -180,7 +162,7 @@ "source": [ "## Part B: Load Beams2D data and build a stable workshop subset\n", "\n", - "We intentionally use a compact subset (`N_TRAIN=512`) so participants can finish on free Colab runtimes.\n" + "We intentionally constrain runtime while preserving the full benchmark interaction pattern.\n" ] }, { @@ -189,23 +171,17 @@ "source": [ "### Training diagnostics to monitor\n", "\n", - "- Loss trend stability across epochs (convergence vs collapse)\n", - "- Sensitivity to seed, subset size, and latent dimension\n", - "- Whether lower training loss transfers to better simulated objective\n", - "\n", - "These diagnostics are useful for comparing methods across papers, not only for this workshop.\n" + "Track trend shape (stability/collapse), not only endpoint value.\n", + "Diagnostics are evidence, not decoration.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Build Training Slice from EngiBench Dataset\n", + "### Step 2 - Build training slice from EngiBench dataset\n", "\n", - "**Purpose:** create a time-bounded workshop subset from the benchmark dataset.\n", - "**Inputs:** `problem.dataset`, `condition_keys`, `N_TRAIN`.\n", - "**Success check:** condition/design arrays have expected shapes.\n", - "**Discussion:** how does subset size affect scientific validity?\n" + "Create aligned condition/design tensors and document scaling assumptions.\n" ] }, { @@ -239,11 +215,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 3 - Define EngiOpt Model and Optimization Objects\n", + "### Step 3 - Define EngiOpt model and optimization objects\n", "\n", - "**Purpose:** instantiate the generator and optimization components.\n", - "**Inputs:** condition dimensionality and design shape from EngiBench.\n", - "**Success check:** model initializes on CPU/GPU and noise sampler returns expected shape.\n" + "Model dimensions should derive from benchmark metadata, not hard-coded guesswork.\n" ] }, { @@ -270,11 +244,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 4 - Train (or Load) with Diagnostics\n", + "### Step 4 - Train (or load) with diagnostics\n", "\n", - "**Purpose:** fit the generator and record diagnostics for reproducibility.\n", - "**Success check:** checkpoint/history/curve files are created.\n", - "**Failure mode:** unstable loss; reduce learning rate or verify target scaling.\n" + "Persist checkpoint and training traces so downstream evaluation can be audited and repeated.\n" ] }, { @@ -386,7 +358,7 @@ "source": [ "## Part C: Condition-driven generation and quick sanity checks\n", "\n", - "We generate `N_SAMPLES` designs from the test condition set and report quick feasibility/objective diagnostics.\n" + "Generate candidates for held-out conditions and run quick feasibility checks before full simulation.\n" ] }, { @@ -395,21 +367,17 @@ "source": [ "### Scientific checkpoint before Notebook 02\n", "\n", - "Before moving on, ask:\n", - "1. Are generated designs diverse or mostly memorized variants?\n", - "2. Do quick constraint checks indicate likely feasibility issues?\n", - "3. Which artifacts are necessary for independent re-evaluation?\n" + "Ask whether outputs are merely plausible-looking or genuinely evaluation-ready.\n", + "Notebook 02 will resolve this quantitatively.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 5 - Generate Conditioned Designs\n", + "### Step 5 - Generate conditioned designs\n", "\n", - "**Purpose:** generate candidate designs for held-out test conditions.\n", - "**Success check:** generated and baseline arrays align in shape.\n", - "**Discussion:** do generated designs look diverse or mode-collapsed?\n" + "Use consistent test sampling so comparisons remain interpretable across runs.\n" ] }, { @@ -453,11 +421,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 6 - Export Artifact Contract\n", + "### Step 6 - Export artifact contract\n", "\n", - "**Purpose:** persist a strict handoff contract for Notebook 02 evaluation.\n", - "**Required outputs:** generated/baseline arrays + conditions JSON.\n", - "**Optional outputs:** W&B artifact bundle for remote recovery.\n" + "These files are the reproducible boundary between modeling and evaluation notebooks.\n" ] }, { @@ -513,10 +479,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 7 - Quick Visual QA\n", + "### Step 7 - Quick visual QA\n", "\n", - "**Purpose:** catch obvious degeneration before simulator evaluation.\n", - "**Success check:** generated structures are not uniformly blank/noisy.\n" + "Visual checks detect obvious failure patterns early (blank/noise/mode collapse).\n" ] }, { @@ -546,9 +511,8 @@ "source": [ "## Troubleshooting\n", "\n", - "- `ModuleNotFoundError: engiopt...`: rerun bootstrap cell; on Colab, Runtime -> Restart runtime.\n", - "- No artifact files found later: verify export cell completed and files printed from `ARTIFACT_DIR`.\n", - "- Training too slow: reduce `EPOCHS` or `N_TRAIN`, but note this affects quality.\n" + "If a section fails, do not continue downstream. Fix locally first, then rerun the section and its immediate checks.\n", + "This notebook is intentionally staged so failures are localized.\n" ] }, { @@ -557,7 +521,7 @@ "source": [ "## Next\n", "\n", - "Proceed to **Notebook 02** for full physics-based evaluation and metric reporting.\n" + "Continue with Notebook 02 for benchmark-grade evaluation and reporting outputs.\n" ] }, { @@ -566,7 +530,10 @@ "source": [ "## Takeaways\n", "\n", - "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + "Before closing, record three points:\n", + "1. What conclusion is directly supported by your metrics?\n", + "2. What remains uncertain (and why)?\n", + "3. What extra experiment would you run next to reduce that uncertainty?\n" ] } ], diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index d39afb0..a98e184 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -6,19 +6,14 @@ "source": [ "# Notebook 02: Evaluation + Metrics (DCC26)\n", "\n", - "This notebook maps to the **01:10-01:30** workshop block:\n", - "1. Load artifacts from Notebook 01 (or recover automatically)\n", - "2. Validate constraints and run physics simulation\n", - "3. Compute objective/feasibility/diversity/novelty metrics\n", - "4. Export CSV + figures for reporting\n" + "Reference implementation for benchmark-grade evaluation and result interpretation.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", - "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" ] }, { @@ -27,7 +22,12 @@ "source": [ "## Notebook map\n", "\n", - "This notebook is a complete reference for robust evaluation beyond objective-only reporting.\n" + "This notebook is written as a standalone lab chapter:\n", + "- context first,\n", + "- implementation second,\n", + "- interpretation third.\n", + "\n", + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" ] }, { @@ -36,16 +36,7 @@ "source": [ "## Standalone guide\n", "\n", - "**What you will learn**\n", - "- How to evaluate generated designs with constraint checks + simulator objective.\n", - "- How to summarize performance with objective, feasibility, diversity, and novelty proxies.\n", - "\n", - "**Expected runtime**\n", - "- 10-25 minutes depending on whether artifacts must be rebuilt.\n", - "\n", - "**If artifacts are missing**\n", - "- Notebook can auto-build them locally (`AUTO_BUILD_ARTIFACTS_IF_MISSING=True`).\n", - "- Optional: enable W&B artifact download.\n" + "This notebook operationalizes rigorous comparison: same conditions, same simulator, explicit metrics, reproducible exports.\n" ] }, { @@ -77,12 +68,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Artifact Loading Strategy\n", + "## Artifact loading strategy\n", "\n", - "Priority order:\n", - "1. Local runtime artifacts (`/content/dcc26_artifacts`)\n", - "2. Optional W&B artifact download\n", - "3. Automatic local rebuild with EngiOpt (enabled by default)\n" + "Resolution order is explicit to avoid hidden state:\n", + "local artifacts -> optional W&B pull -> local auto-build fallback.\n" ] }, { @@ -91,19 +80,17 @@ "source": [ "### Why these metrics matter for benchmarking\n", "\n", - "Objective value alone is not enough for engineering design benchmarks.\n", - "We also track feasibility, diversity, and novelty proxies to reason about method behavior,\n", - "trade-offs, and potential benchmark blind spots.\n" + "Objective-only reporting can hide infeasibility or mode collapse.\n", + "The metric suite is intentionally multi-dimensional.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 1 - Resolve Artifact Source and Recovery Path\n", + "### Step 1 - Resolve artifact source and recovery path\n", "\n", - "**Purpose:** load Notebook 01 outputs robustly (local/W&B/auto-build).\n", - "**Success check:** artifact directory and sample counts print correctly.\n" + "Validate artifact integrity before evaluation; otherwise downstream metrics are misleading.\n" ] }, { @@ -316,20 +303,16 @@ "source": [ "## Per-sample evaluation loop\n", "\n", - "For each condition:\n", - "- run `check_constraints` for generated and baseline designs,\n", - "- run simulator objective for both,\n", - "- store a row for later aggregate analysis.\n" + "Evaluate generated and baseline designs sample-by-sample under identical simulator resets.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Run Per-Sample Physics Evaluation\n", + "### Step 2 - Run per-sample physics evaluation\n", "\n", - "**Purpose:** evaluate generated vs baseline fairly under identical simulator conditions.\n", - "**Success check:** one results row per sample.\n" + "Constraint and objective are computed separately to expose failure modes clearly.\n" ] }, { @@ -369,10 +352,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 3 - Compute Benchmark Metrics\n", + "### Step 3 - Compute benchmark metrics\n", "\n", - "**Purpose:** quantify objective, feasibility, diversity, and novelty.\n", - "**Discussion:** which metric changes your conclusion most?\n" + "Summaries should support both leaderboard-style comparison and scientific diagnosis.\n" ] }, { @@ -428,10 +410,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 4 - Export Evidence Artifacts\n", + "### Step 4 - Export evidence artifacts\n", "\n", - "**Purpose:** save tables and figures for reporting and reproducibility.\n", - "**Optional:** push evaluation package to W&B.\n" + "Persist tables/plots and optional W&B logs so external reviewers can inspect claims.\n" ] }, { @@ -504,9 +485,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 5 - Visual Comparison for Interpretation\n", + "### Step 5 - Visual comparison for interpretation\n", "\n", - "Pairwise generated-vs-baseline visuals help interpret metric outliers.\n" + "Pairwise plots help explain where scalar metrics are insufficient.\n" ] }, { @@ -539,10 +520,8 @@ "source": [ "## Interpretation hints and discussion prompts\n", "\n", - "- `objective_gap_mean < 0` means generated designs outperform baselines on average.\n", - "- `gen_feasible_rate` captures practical design validity pressure.\n", - "- `gen_diversity_l2` and `gen_novelty_to_train_l2` help discuss exploration vs imitation.\n", - "- Use these metrics to seed breakout discussion on benchmark quality (objective-only vs broader criteria).\n" + "Interpretation rule: improvements are compelling only if feasibility remains acceptable.\n", + "Use novelty/diversity to discuss whether the method generalizes or imitates.\n" ] }, { @@ -551,10 +530,7 @@ "source": [ "## Discussion bridge to workshop breakout\n", "\n", - "Use your results to discuss:\n", - "1. Which metric changed your interpretation most (objective vs feasibility vs diversity)?\n", - "2. Which additional engineering criterion is still missing from this benchmark?\n", - "3. How should we report uncertainty/runtime when comparing generative methods?\n" + "Prepare one metric-driven insight and one benchmark-design critique for group discussion.\n" ] }, { @@ -563,9 +539,8 @@ "source": [ "## Troubleshooting\n", "\n", - "- Missing artifacts: keep `AUTO_BUILD_ARTIFACTS_IF_MISSING=True` or run Notebook 01 first.\n", - "- W&B pull fails: disable `USE_WANDB_ARTIFACTS` and use local path.\n", - "- Objective comparison seems inconsistent: ensure simulator reset is called before each run.\n" + "If a section fails, do not continue downstream. Fix locally first, then rerun the section and its immediate checks.\n", + "This notebook is intentionally staged so failures are localized.\n" ] }, { @@ -574,7 +549,10 @@ "source": [ "## Takeaways\n", "\n", - "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + "Before closing, record three points:\n", + "1. What conclusion is directly supported by your metrics?\n", + "2. What remains uncertain (and why)?\n", + "3. What extra experiment would you run next to reduce that uncertainty?\n" ] } ], diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 69fc668..238bef8 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -3,14 +3,17 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Notebook 03: Add a New Problem Scaffold (DCC26)\n\nThis notebook is a contribution-oriented walkthrough showing a **minimal EngiBench-compatible problem**.\n\nIt uses a toy simulator so participants can understand the required interface before implementing real physics backends.\n" + "source": [ + "# Notebook 03: Add a New Problem Scaffold (DCC26)\n", + "\n", + "Reference implementation of a minimal, reproducible benchmark problem scaffold.\n" + ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Before editing:** if you opened from a GitHub URL, click **File -> Save a copy in Drive** first.\n", - "That keeps your edits in your own copy and avoids accidental GitHub sync prompts.\n" + "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" ] }, { @@ -19,7 +22,12 @@ "source": [ "## Notebook map\n", "\n", - "This notebook provides a full minimal scaffold and validation flow for new benchmark problems.\n" + "This notebook is written as a standalone lab chapter:\n", + "- context first,\n", + "- implementation second,\n", + "- interpretation third.\n", + "\n", + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" ] }, { @@ -28,15 +36,7 @@ "source": [ "## Standalone guide\n", "\n", - "**What you will learn**\n", - "- Which `Problem` components are required for a reusable benchmark scaffold.\n", - "- How to think about benchmark quality beyond “code runs”.\n", - "\n", - "**Expected runtime**\n", - "- 10-20 minutes.\n", - "\n", - "**Outcome**\n", - "- A toy scaffold template you can map to a real domain and contribution-ready checklist.\n" + "Use this as a pattern for structuring new benchmark problems with explicit contracts and validation checks.\n" ] }, { @@ -45,13 +45,7 @@ "source": [ "## What makes a new problem benchmark-ready\n", "\n", - "A useful benchmark problem should make the following explicit:\n", - "- Design representation and constraints (what is feasible)\n", - "- Condition space (what is being requested)\n", - "- Objective(s) and simulator semantics (what is optimized)\n", - "- Dataset protocol (train/test split, provenance, leakage risks)\n", - "\n", - "This notebook uses a toy scaffold so you can map these requirements to your own domain later.\n" + "Benchmark value comes from clarity and comparability, not only simulator sophistication.\n" ] }, { @@ -80,9 +74,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 1 - Import Scaffold Dependencies\n", + "### Step 1 - Import scaffold dependencies\n", "\n", - "These define the minimal benchmark interface contract.\n" + "Keep imports minimal and interface-focused.\n" ] }, { @@ -96,9 +90,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Implement Minimal Problem Contract\n", + "### Step 2 - Implement minimal problem contract\n", "\n", - "Focus on deterministic behavior and explicit objective semantics.\n" + "Ensure methods are deterministic and constraints/objectives are semantically explicit.\n" ] }, { @@ -112,9 +106,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 3 - Smoke-Test the Scaffold\n", + "### Step 3 - Smoke-test the scaffold\n", "\n", - "Validate simulator loop and constraint checks end-to-end.\n" + "Validate behavior with simple checks before scaling to real domains.\n" ] }, { @@ -127,7 +121,11 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## Mapping to real EngiBench contributions\n\nThis toy scaffold demonstrates the interface shape only. For real contributions:\n\n1. Create `engibench/problems//v0.py`\n2. Implement real `simulate` and (optionally) `optimize`\n3. Provide `conditions`, `design_space`, and `dataset_id`\n4. Add docs and tests\n\nReference docs: [Adding a new problem](https://github.com/IDEALLab/EngiBench/blob/main/docs/tutorials/new_problem.md)\n" + "source": [ + "## Mapping to real EngiBench contributions\n", + "\n", + "Use this template to onboard new domains while preserving common evaluation semantics.\n" + ] }, { "cell_type": "markdown", @@ -135,10 +133,7 @@ "source": [ "## Contribution checklist\n", "\n", - "- Define clear simulator I/O and units.\n", - "- Add deterministic seeds for repeatability.\n", - "- Document constraint semantics and failure modes.\n", - "- Provide at least one baseline and expected metric outputs.\n" + "Check for leakage risks, undocumented defaults, and missing reproducibility metadata before contribution.\n" ] }, { @@ -147,8 +142,8 @@ "source": [ "## Troubleshooting\n", "\n", - "- If class TODOs fail, implement methods incrementally and run one check cell at a time.\n", - "- Keep seed handling deterministic so your scaffold behavior is reproducible.\n" + "If a section fails, do not continue downstream. Fix locally first, then rerun the section and its immediate checks.\n", + "This notebook is intentionally staged so failures are localized.\n" ] }, { @@ -157,7 +152,10 @@ "source": [ "## Takeaways\n", "\n", - "Summarize what you learned, what remains uncertain, and which metric or API component you would inspect next.\n" + "Before closing, record three points:\n", + "1. What conclusion is directly supported by your metrics?\n", + "2. What remains uncertain (and why)?\n", + "3. What extra experiment would you run next to reduce that uncertainty?\n" ] } ], From cbad68a22d551d50d0b44c968e4f6c11a882eedf Mon Sep 17 00:00:00 2001 From: Soheyl Date: Tue, 10 Mar 2026 14:09:02 +0100 Subject: [PATCH 22/37] Replace Notebook 03 toy scaffold with battery cold-plate problem --- workshops/dcc26/README.md | 4 +- .../03_add_new_problem_scaffold.ipynb | 157 +++++++++++- .../03_add_new_problem_scaffold.ipynb | 234 +++++++++++++++++- 3 files changed, 383 insertions(+), 12 deletions(-) diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index 73a161c..52e0abe 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -26,8 +26,8 @@ It is split into two tracks: - Metric and artifact export - `participant/03_add_new_problem_scaffold.ipynb` and `solutions/03_add_new_problem_scaffold.ipynb` (25 min) - - Minimal `Problem` scaffold - - Toy simulator and optimization loop + - Ambitious `Problem` scaffold (`BatteryColdPlate2DProblem`, not currently in EngiBench) + - Lightweight thermal-flow tradeoff simulator and optimization loop - Mapping to contribution docs ## Runtime assumptions diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index c19f70b..ca15591 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -6,7 +6,7 @@ "source": [ "# Notebook 03 (Participant): Add a New Problem Scaffold\n", "\n", - "You will implement a minimal problem contract and evaluate whether it is benchmark-ready.\n" + "You will implement a battery cold-plate problem contract and evaluate whether it is benchmark-ready.\n" ] }, { @@ -84,13 +84,27 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport numpy as np\nfrom gymnasium import spaces\n\nfrom engibench.constraint import bounded\nfrom engibench.core import ObjectiveDirection\nfrom engibench.core import OptiStep\nfrom engibench.core import Problem\n" + "source": [ + "from __future__ import annotations\n", + "\n", + "from dataclasses import dataclass\n", + "from typing import Annotated\n", + "\n", + "import numpy as np\n", + "from gymnasium import spaces\n", + "\n", + "from engibench.constraint import bounded\n", + "from engibench.constraint import constraint\n", + "from engibench.core import ObjectiveDirection\n", + "from engibench.core import OptiStep\n", + "from engibench.core import Problem\n" + ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Implement minimal problem contract (TODO)\n", + "### Step 2 - Implement battery cold-plate problem contract (TODO)\n", "\n", "Complete each required method with deterministic behavior and clear failure messages.\n" ] @@ -100,7 +114,96 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "class ToyDensityProblem(Problem[np.ndarray]):\n \"\"\"Minimal toy problem scaffold for workshop teaching.\"\"\"\n\n version = 0\n objectives = ((\"toy_cost\", ObjectiveDirection.MINIMIZE),)\n\n @dataclass\n class Conditions:\n target_density: Annotated[float, bounded(lower=0.0, upper=1.0)] = 0.5\n\n @dataclass\n class Config(Conditions):\n resolution: Annotated[int, bounded(lower=4, upper=128)] = 16\n max_iter: Annotated[int, bounded(lower=1, upper=200)] = 20\n\n dataset_id = \"IDEALLab/beams_2d_50_100_v0\" # placeholder to satisfy scaffold\n container_id = None\n\n def __init__(self, seed: int = 0, **kwargs):\n super().__init__(seed=seed)\n self.config = self.Config(**kwargs)\n self.conditions = self.Conditions(target_density=self.config.target_density)\n self.design_space = spaces.Box(\n low=0.0, high=1.0, shape=(self.config.resolution, self.config.resolution), dtype=np.float32\n )\n\n def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n # TODO 1: implement toy objective\n # Suggested terms:\n # - density mismatch to target_density\n # - smoothness penalty based on np.diff\n raise NotImplementedError('Implement simulate')\n\n def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n # TODO 2: implement simple iterative optimizer\n # - update design toward target density\n # - append OptiStep each iteration\n raise NotImplementedError('Implement optimize')\n\n def render(self, design: np.ndarray, *, open_window: bool = False):\n import matplotlib.pyplot as plt\n\n fig, ax = plt.subplots(figsize=(4, 4))\n ax.imshow(design, cmap=\"viridis\", vmin=0, vmax=1)\n ax.set_title(\"ToyDensityProblem design\")\n ax.axis(\"off\")\n if open_window:\n plt.show()\n return fig, ax\n\n def random_design(self):\n d = self.np_random.random(self.design_space.shape).astype(np.float32)\n return d, -1" + "source": [ + "class BatteryColdPlate2DProblem(Problem[np.ndarray]):\n", + " \"\"\"Scaffold for a battery cold-plate topology problem (not currently in EngiBench).\"\"\"\n", + "\n", + " version = 0\n", + " objectives = (\n", + " (\"max_temperature_c\", ObjectiveDirection.MINIMIZE),\n", + " (\"flow_penalty\", ObjectiveDirection.MINIMIZE),\n", + " )\n", + "\n", + " @dataclass\n", + " class Conditions:\n", + " heat_load_left: Annotated[float, bounded(lower=0.1, upper=2.0)] = 1.0\n", + " heat_load_right: Annotated[float, bounded(lower=0.1, upper=2.0)] = 1.0\n", + " inlet_temp_c: Annotated[float, bounded(lower=10.0, upper=40.0)] = 25.0\n", + " flow_budget: Annotated[float, bounded(lower=0.15, upper=0.65)] = 0.35\n", + "\n", + " @dataclass\n", + " class Config(Conditions):\n", + " resolution: Annotated[int, bounded(lower=16, upper=96)] = 32\n", + " max_iter: Annotated[int, bounded(lower=1, upper=200)] = 30\n", + " solver_iters: Annotated[int, bounded(lower=20, upper=500)] = 120\n", + " min_channel_fraction: Annotated[float, bounded(lower=0.05, upper=0.60)] = 0.18\n", + " max_channel_fraction: Annotated[float, bounded(lower=0.10, upper=0.85)] = 0.55\n", + " max_edge_density: Annotated[float, bounded(lower=0.01, upper=1.00)] = 0.28\n", + "\n", + " dataset_id = \"IDEALLab/battery_cold_plate_2d_v0\" # placeholder for future dataset integration\n", + " container_id = None\n", + "\n", + " def __init__(self, seed: int = 0, **kwargs):\n", + " super().__init__(seed=seed)\n", + " self.config = self.Config(**kwargs)\n", + " self.conditions = self.Conditions(\n", + " heat_load_left=self.config.heat_load_left,\n", + " heat_load_right=self.config.heat_load_right,\n", + " inlet_temp_c=self.config.inlet_temp_c,\n", + " flow_budget=self.config.flow_budget,\n", + " )\n", + " self.design_space = spaces.Box(\n", + " low=0.0,\n", + " high=1.0,\n", + " shape=(self.config.resolution, self.config.resolution),\n", + " dtype=np.float32,\n", + " )\n", + "\n", + " # TODO 1: add at least two design constraints using @constraint\n", + " # Suggested:\n", + " # - channel fraction between min_channel_fraction and max_channel_fraction\n", + " # - edge-density/manufacturability bound using max_edge_density\n", + " raise NotImplementedError('Implement design constraints and assign self.design_constraints')\n", + "\n", + " def _heat_map(self, cfg: dict) -> np.ndarray:\n", + " # TODO 2: implement two gaussian heat sources (left/right battery modules)\n", + " raise NotImplementedError('Implement _heat_map')\n", + "\n", + " def _solve_temperature(self, conductivity: np.ndarray, heat: np.ndarray, inlet_temp: float, n_iter: int) -> np.ndarray:\n", + " # TODO 3: implement iterative finite-difference temperature solver\n", + " # Boundary suggestions:\n", + " # - left boundary fixed at inlet_temp\n", + " # - right boundary convective mix to ambient\n", + " # - top/bottom insulated copy\n", + " raise NotImplementedError('Implement _solve_temperature')\n", + "\n", + " def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n", + " # TODO 4: implement two-objective evaluation\n", + " # Objective 1: max temperature [C]\n", + " # Objective 2: flow_penalty (channel-fraction mismatch + roughness + mild temperature uniformity term)\n", + " raise NotImplementedError('Implement simulate')\n", + "\n", + " def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n", + " # TODO 5: implement a simple iterative optimizer and return (design, list[OptiStep])\n", + " # Keep it deterministic and lightweight for workshop runtime.\n", + " raise NotImplementedError('Implement optimize')\n", + "\n", + " def render(self, design: np.ndarray, *, open_window: bool = False):\n", + " import matplotlib.pyplot as plt\n", + "\n", + " fig, ax = plt.subplots(figsize=(4.2, 4.2))\n", + " im = ax.imshow(design, cmap=\"inferno\", vmin=0, vmax=1)\n", + " ax.set_title(\"BatteryColdPlate2D design (1=solid, 0=channel)\")\n", + " ax.axis(\"off\")\n", + " fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)\n", + " if open_window:\n", + " plt.show()\n", + " return fig, ax\n", + "\n", + " def random_design(self):\n", + " # TODO 6: implement smooth random design initialization\n", + " raise NotImplementedError('Implement random_design')\n" + ] }, { "cell_type": "markdown", @@ -116,7 +219,49 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Run this cell after finishing TODOs in ToyDensityProblem\nproblem = ToyDensityProblem(seed=42, resolution=16, target_density=0.4, max_iter=10)\nstart, _ = problem.random_design()\n\nprint('design space:', problem.design_space)\nprint('objectives:', problem.objectives)\nprint('conditions:', problem.conditions)\n\nviol = problem.check_constraints(start, config={'target_density': 0.4, 'resolution': 16, 'max_iter': 10})\nprint('constraint violations:', len(viol))\n\nobj0 = problem.simulate(start, config={'target_density': 0.4})\nopt_design, history = problem.optimize(start, config={'target_density': 0.4})\nobjf = problem.simulate(opt_design, config={'target_density': 0.4})\n\nprint('initial objective:', float(obj0[0]))\nprint('final objective:', float(objf[0]))\nprint('optimization steps:', len(history))\n\nproblem.render(opt_design)" + "source": [ + "# Run this cell after finishing TODOs in BatteryColdPlate2DProblem\n", + "problem = BatteryColdPlate2DProblem(\n", + " seed=42,\n", + " resolution=32,\n", + " max_iter=20,\n", + " heat_load_left=1.4,\n", + " heat_load_right=1.1,\n", + " inlet_temp_c=24.0,\n", + " flow_budget=0.33,\n", + ")\n", + "start, _ = problem.random_design()\n", + "\n", + "cfg = {\n", + " 'heat_load_left': 1.4,\n", + " 'heat_load_right': 1.1,\n", + " 'inlet_temp_c': 24.0,\n", + " 'flow_budget': 0.33,\n", + " 'resolution': 32,\n", + " 'max_iter': 20,\n", + " 'solver_iters': 100,\n", + " 'min_channel_fraction': 0.18,\n", + " 'max_channel_fraction': 0.55,\n", + " 'max_edge_density': 0.35,\n", + "}\n", + "\n", + "print('design space:', problem.design_space)\n", + "print('objectives:', problem.objectives)\n", + "print('conditions:', problem.conditions)\n", + "\n", + "viol = problem.check_constraints(start, config=cfg)\n", + "print('constraint violations:', len(viol))\n", + "\n", + "obj0 = problem.simulate(start, config=cfg)\n", + "opt_design, history = problem.optimize(start, config=cfg)\n", + "objf = problem.simulate(opt_design, config=cfg)\n", + "\n", + "print('initial objectives [max_temp_c, flow_penalty]:', obj0.tolist())\n", + "print('final objectives [max_temp_c, flow_penalty]:', objf.tolist())\n", + "print('optimization steps:', len(history))\n", + "\n", + "problem.render(opt_design)\n" + ] }, { "cell_type": "markdown", @@ -124,7 +269,7 @@ "source": [ "## Mapping to real EngiBench contributions\n", "\n", - "Translate this toy scaffold into domain-specific simulators and datasets with documented assumptions.\n" + "Translate this battery cold-plate scaffold into domain-specific simulators and datasets with documented assumptions.\n" ] }, { diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 238bef8..355475e 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -84,13 +84,27 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport numpy as np\nfrom gymnasium import spaces\n\nfrom engibench.constraint import bounded\nfrom engibench.core import ObjectiveDirection\nfrom engibench.core import OptiStep\nfrom engibench.core import Problem\n" + "source": [ + "from __future__ import annotations\n", + "\n", + "from dataclasses import dataclass\n", + "from typing import Annotated\n", + "\n", + "import numpy as np\n", + "from gymnasium import spaces\n", + "\n", + "from engibench.constraint import bounded\n", + "from engibench.constraint import constraint\n", + "from engibench.core import ObjectiveDirection\n", + "from engibench.core import OptiStep\n", + "from engibench.core import Problem\n" + ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Implement minimal problem contract\n", + "### Step 2 - Implement battery cold-plate problem contract\n", "\n", "Ensure methods are deterministic and constraints/objectives are semantically explicit.\n" ] @@ -100,7 +114,178 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "class ToyDensityProblem(Problem[np.ndarray]):\n \"\"\"Minimal toy problem scaffold for workshop teaching.\"\"\"\n\n version = 0\n objectives = ((\"toy_cost\", ObjectiveDirection.MINIMIZE),)\n\n @dataclass\n class Conditions:\n target_density: Annotated[float, bounded(lower=0.0, upper=1.0)] = 0.5\n\n @dataclass\n class Config(Conditions):\n resolution: Annotated[int, bounded(lower=4, upper=128)] = 16\n max_iter: Annotated[int, bounded(lower=1, upper=200)] = 20\n\n dataset_id = \"IDEALLab/beams_2d_50_100_v0\" # placeholder to satisfy scaffold\n container_id = None\n\n def __init__(self, seed: int = 0, **kwargs):\n super().__init__(seed=seed)\n self.config = self.Config(**kwargs)\n self.conditions = self.Conditions(target_density=self.config.target_density)\n self.design_space = spaces.Box(\n low=0.0, high=1.0, shape=(self.config.resolution, self.config.resolution), dtype=np.float32\n )\n\n def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n cfg = {\"target_density\": self.config.target_density, **(config or {})}\n # Toy objective: mismatch to target density + smoothness penalty\n density_term = abs(float(design.mean()) - float(cfg[\"target_density\"]))\n smoothness = float(np.mean(np.abs(np.diff(design, axis=0))))\n return np.array([density_term + 0.1 * smoothness], dtype=np.float32)\n\n def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n cfg = {\"target_density\": self.config.target_density, **(config or {})}\n x = starting_point.copy().astype(np.float32)\n hist = []\n for step in range(self.config.max_iter):\n # Toy update toward target density (not a real optimizer)\n x = np.clip(x + 0.2 * (cfg[\"target_density\"] - x), 0.0, 1.0)\n hist.append(OptiStep(obj_values=self.simulate(x, cfg), step=step))\n return x, hist\n\n def render(self, design: np.ndarray, *, open_window: bool = False):\n import matplotlib.pyplot as plt\n\n fig, ax = plt.subplots(figsize=(4, 4))\n ax.imshow(design, cmap=\"viridis\", vmin=0, vmax=1)\n ax.set_title(\"ToyDensityProblem design\")\n ax.axis(\"off\")\n if open_window:\n plt.show()\n return fig, ax\n\n def random_design(self):\n d = self.np_random.random(self.design_space.shape).astype(np.float32)\n return d, -1\n" + "source": [ + "class BatteryColdPlate2DProblem(Problem[np.ndarray]):\n", + " \"\"\"Scaffold for a battery cold-plate topology problem (not currently in EngiBench).\"\"\"\n", + "\n", + " version = 0\n", + " objectives = (\n", + " (\"max_temperature_c\", ObjectiveDirection.MINIMIZE),\n", + " (\"flow_penalty\", ObjectiveDirection.MINIMIZE),\n", + " )\n", + "\n", + " @dataclass\n", + " class Conditions:\n", + " heat_load_left: Annotated[float, bounded(lower=0.1, upper=2.0)] = 1.0\n", + " heat_load_right: Annotated[float, bounded(lower=0.1, upper=2.0)] = 1.0\n", + " inlet_temp_c: Annotated[float, bounded(lower=10.0, upper=40.0)] = 25.0\n", + " flow_budget: Annotated[float, bounded(lower=0.15, upper=0.65)] = 0.35\n", + "\n", + " @dataclass\n", + " class Config(Conditions):\n", + " resolution: Annotated[int, bounded(lower=16, upper=96)] = 32\n", + " max_iter: Annotated[int, bounded(lower=1, upper=200)] = 30\n", + " solver_iters: Annotated[int, bounded(lower=20, upper=500)] = 120\n", + " min_channel_fraction: Annotated[float, bounded(lower=0.05, upper=0.60)] = 0.18\n", + " max_channel_fraction: Annotated[float, bounded(lower=0.10, upper=0.85)] = 0.55\n", + " max_edge_density: Annotated[float, bounded(lower=0.01, upper=1.00)] = 0.28\n", + "\n", + " dataset_id = \"IDEALLab/battery_cold_plate_2d_v0\" # placeholder for future dataset integration\n", + " container_id = None\n", + "\n", + " def __init__(self, seed: int = 0, **kwargs):\n", + " super().__init__(seed=seed)\n", + " self.config = self.Config(**kwargs)\n", + " self.conditions = self.Conditions(\n", + " heat_load_left=self.config.heat_load_left,\n", + " heat_load_right=self.config.heat_load_right,\n", + " inlet_temp_c=self.config.inlet_temp_c,\n", + " flow_budget=self.config.flow_budget,\n", + " )\n", + " self.design_space = spaces.Box(\n", + " low=0.0,\n", + " high=1.0,\n", + " shape=(self.config.resolution, self.config.resolution),\n", + " dtype=np.float32,\n", + " )\n", + "\n", + " @constraint\n", + " def channel_fraction(design: np.ndarray, min_channel_fraction: float, max_channel_fraction: float, **_) -> None:\n", + " cf = float(np.mean(1.0 - design))\n", + " assert min_channel_fraction <= cf <= max_channel_fraction, (\n", + " f\"channel_fraction={cf:.3f} outside [{min_channel_fraction:.3f}, {max_channel_fraction:.3f}]\"\n", + " )\n", + "\n", + " @constraint\n", + " def edge_density(design: np.ndarray, max_edge_density: float, **_) -> None:\n", + " tv = float(np.mean(np.abs(np.diff(design, axis=0))) + np.mean(np.abs(np.diff(design, axis=1))))\n", + " assert tv <= max_edge_density, f\"edge_density={tv:.3f} exceeds {max_edge_density:.3f}\"\n", + "\n", + " self.design_constraints = [channel_fraction, edge_density]\n", + "\n", + " def _heat_map(self, cfg: dict) -> np.ndarray:\n", + " h, w = self.design_space.shape\n", + " yy, xx = np.indices((h, w), dtype=np.float32)\n", + " s = 0.08 * min(h, w)\n", + "\n", + " left = np.exp(-(((xx - 0.22 * w) ** 2 + (yy - 0.35 * h) ** 2) / (2.0 * s**2)))\n", + " right = np.exp(-(((xx - 0.78 * w) ** 2 + (yy - 0.65 * h) ** 2) / (2.0 * s**2)))\n", + " heat = cfg[\"heat_load_left\"] * left + cfg[\"heat_load_right\"] * right\n", + " return heat.astype(np.float32)\n", + "\n", + " def _solve_temperature(self, conductivity: np.ndarray, heat: np.ndarray, inlet_temp: float, n_iter: int) -> np.ndarray:\n", + " T = np.full_like(conductivity, float(inlet_temp), dtype=np.float32)\n", + " ambient = float(inlet_temp) + 5.0\n", + "\n", + " for _ in range(int(n_iter)):\n", + " T_old = T.copy()\n", + "\n", + " k_c = conductivity[1:-1, 1:-1]\n", + " k_e = 0.5 * (k_c + conductivity[1:-1, 2:])\n", + " k_w = 0.5 * (k_c + conductivity[1:-1, :-2])\n", + " k_n = 0.5 * (k_c + conductivity[:-2, 1:-1])\n", + " k_s = 0.5 * (k_c + conductivity[2:, 1:-1])\n", + "\n", + " numer = (\n", + " k_e * T_old[1:-1, 2:]\n", + " + k_w * T_old[1:-1, :-2]\n", + " + k_n * T_old[:-2, 1:-1]\n", + " + k_s * T_old[2:, 1:-1]\n", + " + 0.02 * heat[1:-1, 1:-1]\n", + " )\n", + " denom = k_e + k_w + k_n + k_s + 1e-6\n", + " T[1:-1, 1:-1] = numer / denom\n", + "\n", + " T[:, 0] = inlet_temp\n", + " T[:, -1] = 0.7 * T[:, -2] + 0.3 * ambient\n", + " T[0, :] = T[1, :]\n", + " T[-1, :] = T[-2, :]\n", + "\n", + " return T\n", + "\n", + " def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n", + " cfg = {**self.__dict__[\"config\"].__dict__, **(config or {})}\n", + "\n", + " x = np.clip(design.astype(np.float32), 0.0, 1.0)\n", + " channel = 1.0 - x\n", + "\n", + " k_channel = 0.25\n", + " k_solid = 4.5\n", + " conductivity = k_channel + x * (k_solid - k_channel)\n", + "\n", + " heat = self._heat_map(cfg)\n", + " T = self._solve_temperature(conductivity, heat, cfg[\"inlet_temp_c\"], cfg[\"solver_iters\"])\n", + "\n", + " max_temp = float(np.max(T))\n", + " temp_std = float(np.std(T))\n", + "\n", + " channel_fraction = float(np.mean(channel))\n", + " edge_density = float(np.mean(np.abs(np.diff(channel, axis=0))) + np.mean(np.abs(np.diff(channel, axis=1))))\n", + " flow_penalty = abs(channel_fraction - cfg[\"flow_budget\"]) + 0.15 * edge_density + 0.05 * temp_std\n", + "\n", + " return np.array([max_temp, float(flow_penalty)], dtype=np.float32)\n", + "\n", + " def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n", + " cfg = {**self.__dict__[\"config\"].__dict__, **(config or {})}\n", + " x = np.clip(starting_point.astype(np.float32), 0.0, 1.0)\n", + " history = []\n", + "\n", + " h, w = x.shape\n", + " yy, xx = np.indices((h, w), dtype=np.float32)\n", + " thermal_bias = np.exp(-(((xx - 0.50 * w) ** 2 + (yy - 0.50 * h) ** 2) / (2.0 * (0.28 * min(h, w)) ** 2)))\n", + " thermal_bias = (thermal_bias - thermal_bias.min()) / (thermal_bias.max() - thermal_bias.min() + 1e-8)\n", + "\n", + " target_density = np.clip(1.0 - cfg[\"flow_budget\"], 0.2, 0.9)\n", + "\n", + " for step in range(cfg[\"max_iter\"]):\n", + " neighbor = (\n", + " x\n", + " + np.roll(x, 1, axis=0)\n", + " + np.roll(x, -1, axis=0)\n", + " + np.roll(x, 1, axis=1)\n", + " + np.roll(x, -1, axis=1)\n", + " ) / 5.0\n", + " x = 0.70 * x + 0.20 * neighbor + 0.08 * target_density + 0.02 * thermal_bias\n", + " x = np.clip(x, 0.0, 1.0)\n", + "\n", + " history.append(OptiStep(obj_values=self.simulate(x, cfg), step=step))\n", + "\n", + " return x, history\n", + "\n", + " def render(self, design: np.ndarray, *, open_window: bool = False):\n", + " import matplotlib.pyplot as plt\n", + "\n", + " fig, ax = plt.subplots(figsize=(4.2, 4.2))\n", + " im = ax.imshow(design, cmap=\"inferno\", vmin=0, vmax=1)\n", + " ax.set_title(\"BatteryColdPlate2D design (1=solid, 0=channel)\")\n", + " ax.axis(\"off\")\n", + " fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)\n", + " if open_window:\n", + " plt.show()\n", + " return fig, ax\n", + "\n", + " def random_design(self):\n", + " x = self.np_random.random(self.design_space.shape).astype(np.float32)\n", + " for _ in range(3):\n", + " x = (\n", + " x\n", + " + np.roll(x, 1, axis=0)\n", + " + np.roll(x, -1, axis=0)\n", + " + np.roll(x, 1, axis=1)\n", + " + np.roll(x, -1, axis=1)\n", + " ) / 5.0\n", + " return np.clip(x, 0.0, 1.0), -1\n" + ] }, { "cell_type": "markdown", @@ -116,7 +301,48 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "problem = ToyDensityProblem(seed=42, resolution=16, target_density=0.4, max_iter=10)\nstart, _ = problem.random_design()\n\nprint('design space:', problem.design_space)\nprint('objectives:', problem.objectives)\nprint('conditions:', problem.conditions)\n\nviol = problem.check_constraints(start, config={'target_density': 0.4, 'resolution': 16, 'max_iter': 10})\nprint('constraint violations:', len(viol))\n\nobj0 = problem.simulate(start, config={'target_density': 0.4})\nopt_design, history = problem.optimize(start, config={'target_density': 0.4})\nobjf = problem.simulate(opt_design, config={'target_density': 0.4})\n\nprint('initial objective:', float(obj0[0]))\nprint('final objective:', float(objf[0]))\nprint('optimization steps:', len(history))\n\nproblem.render(opt_design)" + "source": [ + "problem = BatteryColdPlate2DProblem(\n", + " seed=42,\n", + " resolution=32,\n", + " max_iter=20,\n", + " heat_load_left=1.4,\n", + " heat_load_right=1.1,\n", + " inlet_temp_c=24.0,\n", + " flow_budget=0.33,\n", + ")\n", + "start, _ = problem.random_design()\n", + "\n", + "cfg = {\n", + " 'heat_load_left': 1.4,\n", + " 'heat_load_right': 1.1,\n", + " 'inlet_temp_c': 24.0,\n", + " 'flow_budget': 0.33,\n", + " 'resolution': 32,\n", + " 'max_iter': 20,\n", + " 'solver_iters': 100,\n", + " 'min_channel_fraction': 0.18,\n", + " 'max_channel_fraction': 0.55,\n", + " 'max_edge_density': 0.35,\n", + "}\n", + "\n", + "print('design space:', problem.design_space)\n", + "print('objectives:', problem.objectives)\n", + "print('conditions:', problem.conditions)\n", + "\n", + "viol = problem.check_constraints(start, config=cfg)\n", + "print('constraint violations:', len(viol))\n", + "\n", + "obj0 = problem.simulate(start, config=cfg)\n", + "opt_design, history = problem.optimize(start, config=cfg)\n", + "objf = problem.simulate(opt_design, config=cfg)\n", + "\n", + "print('initial objectives [max_temp_c, flow_penalty]:', obj0.tolist())\n", + "print('final objectives [max_temp_c, flow_penalty]:', objf.tolist())\n", + "print('optimization steps:', len(history))\n", + "\n", + "problem.render(opt_design)\n" + ] }, { "cell_type": "markdown", From 5840b126aa10a5c5b0a1577d6c370d99e162dec8 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Tue, 10 Mar 2026 14:15:45 +0100 Subject: [PATCH 23/37] Make Notebook 03 plots interpretable with labeled thermal panels --- .../03_add_new_problem_scaffold.ipynb | 63 ++++++++++++++++--- .../03_add_new_problem_scaffold.ipynb | 63 ++++++++++++++++--- 2 files changed, 110 insertions(+), 16 deletions(-) diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index ca15591..c3a58cf 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -191,14 +191,55 @@ " def render(self, design: np.ndarray, *, open_window: bool = False):\n", " import matplotlib.pyplot as plt\n", "\n", - " fig, ax = plt.subplots(figsize=(4.2, 4.2))\n", - " im = ax.imshow(design, cmap=\"inferno\", vmin=0, vmax=1)\n", - " ax.set_title(\"BatteryColdPlate2D design (1=solid, 0=channel)\")\n", - " ax.axis(\"off\")\n", - " fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)\n", + " # Visual semantics:\n", + " # - solid/channel map: 1.0 means conductive solid, 0.0 means coolant channel\n", + " # - conductivity map: effective thermal conductivity used by solver\n", + " # - heat map: imposed module heat loads (problem conditions)\n", + " # - temperature map: solved steady-state thermal field\n", + " x = np.clip(design.astype(np.float32), 0.0, 1.0)\n", + " channel = 1.0 - x\n", + "\n", + " cfg = self.config.__dict__\n", + " k_channel = 0.25\n", + " k_solid = 4.5\n", + " conductivity = k_channel + x * (k_solid - k_channel)\n", + " heat = self._heat_map(cfg)\n", + " temperature = self._solve_temperature(conductivity, heat, cfg[\"inlet_temp_c\"], cfg[\"solver_iters\"])\n", + "\n", + " fig, axes = plt.subplots(1, 4, figsize=(16, 4))\n", + "\n", + " im0 = axes[0].imshow(x, cmap=\"gray\", vmin=0, vmax=1)\n", + " axes[0].set_title(\"Design map\\n(1=solid, 0=channel)\")\n", + " axes[0].axis(\"off\")\n", + " fig.colorbar(im0, ax=axes[0], fraction=0.046, pad=0.04)\n", + "\n", + " im1 = axes[1].imshow(conductivity, cmap=\"cividis\")\n", + " axes[1].set_title(\"Conductivity field\")\n", + " axes[1].axis(\"off\")\n", + " fig.colorbar(im1, ax=axes[1], fraction=0.046, pad=0.04)\n", + "\n", + " im2 = axes[2].imshow(heat, cmap=\"magma\")\n", + " axes[2].set_title(\"Heat-load map\")\n", + " axes[2].axis(\"off\")\n", + " fig.colorbar(im2, ax=axes[2], fraction=0.046, pad=0.04)\n", + "\n", + " im3 = axes[3].imshow(temperature, cmap=\"inferno\")\n", + " axes[3].set_title(\"Temperature field [C]\")\n", + " axes[3].axis(\"off\")\n", + " fig.colorbar(im3, ax=axes[3], fraction=0.046, pad=0.04)\n", + "\n", + " channel_fraction = float(np.mean(channel))\n", + " edge_density = float(np.mean(np.abs(np.diff(channel, axis=0))) + np.mean(np.abs(np.diff(channel, axis=1))))\n", + " fig.suptitle(\n", + " f\"channel_fraction={channel_fraction:.3f}, edge_density={edge_density:.3f}, \"\n", + " f\"max_temp={float(np.max(temperature)):.2f}C\",\n", + " y=1.03,\n", + " )\n", + "\n", + " fig.tight_layout()\n", " if open_window:\n", " plt.show()\n", - " return fig, ax\n", + " return fig, axes\n", "\n", " def random_design(self):\n", " # TODO 6: implement smooth random design initialization\n", @@ -211,7 +252,10 @@ "source": [ "### Step 3 - Smoke-test your scaffold\n", "\n", - "Run minimal checks to verify interface consistency and simulator behavior.\n" + "Run minimal checks to verify interface consistency and simulator behavior.\n", + "\n", + "\n", + "Use the multi-panel render to read **where heat enters**, **how material is distributed**, and **where thermal bottlenecks remain**.\n" ] }, { @@ -260,7 +304,10 @@ "print('final objectives [max_temp_c, flow_penalty]:', objf.tolist())\n", "print('optimization steps:', len(history))\n", "\n", - "problem.render(opt_design)\n" + "problem.render(opt_design)\n", + "\n", + "\n", + "print('How to read plots: design(1=solid,0=channel) | conductivity | heat-load | temperature')\n" ] }, { diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 355475e..db677fb 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -265,14 +265,55 @@ " def render(self, design: np.ndarray, *, open_window: bool = False):\n", " import matplotlib.pyplot as plt\n", "\n", - " fig, ax = plt.subplots(figsize=(4.2, 4.2))\n", - " im = ax.imshow(design, cmap=\"inferno\", vmin=0, vmax=1)\n", - " ax.set_title(\"BatteryColdPlate2D design (1=solid, 0=channel)\")\n", - " ax.axis(\"off\")\n", - " fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)\n", + " # Visual semantics:\n", + " # - solid/channel map: 1.0 means conductive solid, 0.0 means coolant channel\n", + " # - conductivity map: effective thermal conductivity used by solver\n", + " # - heat map: imposed module heat loads (problem conditions)\n", + " # - temperature map: solved steady-state thermal field\n", + " x = np.clip(design.astype(np.float32), 0.0, 1.0)\n", + " channel = 1.0 - x\n", + "\n", + " cfg = self.config.__dict__\n", + " k_channel = 0.25\n", + " k_solid = 4.5\n", + " conductivity = k_channel + x * (k_solid - k_channel)\n", + " heat = self._heat_map(cfg)\n", + " temperature = self._solve_temperature(conductivity, heat, cfg[\"inlet_temp_c\"], cfg[\"solver_iters\"])\n", + "\n", + " fig, axes = plt.subplots(1, 4, figsize=(16, 4))\n", + "\n", + " im0 = axes[0].imshow(x, cmap=\"gray\", vmin=0, vmax=1)\n", + " axes[0].set_title(\"Design map\\n(1=solid, 0=channel)\")\n", + " axes[0].axis(\"off\")\n", + " fig.colorbar(im0, ax=axes[0], fraction=0.046, pad=0.04)\n", + "\n", + " im1 = axes[1].imshow(conductivity, cmap=\"cividis\")\n", + " axes[1].set_title(\"Conductivity field\")\n", + " axes[1].axis(\"off\")\n", + " fig.colorbar(im1, ax=axes[1], fraction=0.046, pad=0.04)\n", + "\n", + " im2 = axes[2].imshow(heat, cmap=\"magma\")\n", + " axes[2].set_title(\"Heat-load map\")\n", + " axes[2].axis(\"off\")\n", + " fig.colorbar(im2, ax=axes[2], fraction=0.046, pad=0.04)\n", + "\n", + " im3 = axes[3].imshow(temperature, cmap=\"inferno\")\n", + " axes[3].set_title(\"Temperature field [C]\")\n", + " axes[3].axis(\"off\")\n", + " fig.colorbar(im3, ax=axes[3], fraction=0.046, pad=0.04)\n", + "\n", + " channel_fraction = float(np.mean(channel))\n", + " edge_density = float(np.mean(np.abs(np.diff(channel, axis=0))) + np.mean(np.abs(np.diff(channel, axis=1))))\n", + " fig.suptitle(\n", + " f\"channel_fraction={channel_fraction:.3f}, edge_density={edge_density:.3f}, \"\n", + " f\"max_temp={float(np.max(temperature)):.2f}C\",\n", + " y=1.03,\n", + " )\n", + "\n", + " fig.tight_layout()\n", " if open_window:\n", " plt.show()\n", - " return fig, ax\n", + " return fig, axes\n", "\n", " def random_design(self):\n", " x = self.np_random.random(self.design_space.shape).astype(np.float32)\n", @@ -293,7 +334,10 @@ "source": [ "### Step 3 - Smoke-test the scaffold\n", "\n", - "Validate behavior with simple checks before scaling to real domains.\n" + "Validate behavior with simple checks before scaling to real domains.\n", + "\n", + "\n", + "Use the multi-panel render to read **where heat enters**, **how material is distributed**, and **where thermal bottlenecks remain**.\n" ] }, { @@ -341,7 +385,10 @@ "print('final objectives [max_temp_c, flow_penalty]:', objf.tolist())\n", "print('optimization steps:', len(history))\n", "\n", - "problem.render(opt_design)\n" + "problem.render(opt_design)\n", + "\n", + "\n", + "print('How to read plots: design(1=solid,0=channel) | conductivity | heat-load | temperature')\n" ] }, { From 149c84b6b16ff2e8e0a7d9739c90191c03b80229 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Tue, 10 Mar 2026 14:36:39 +0100 Subject: [PATCH 24/37] Replace Notebook 03 with PyBullet manipulator co-design scaffold --- .../03_add_new_problem_scaffold.ipynb | 207 ++++----- .../03_add_new_problem_scaffold.ipynb | 428 ++++++++++-------- 2 files changed, 320 insertions(+), 315 deletions(-) diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index c3a58cf..67b2672 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -6,7 +6,7 @@ "source": [ "# Notebook 03 (Participant): Add a New Problem Scaffold\n", "\n", - "You will implement a battery cold-plate problem contract and evaluate whether it is benchmark-ready.\n" + "You will implement a robotics co-design problem contract and evaluate whether it is benchmark-ready.\n" ] }, { @@ -60,7 +60,7 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium']\n", + "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium', 'pybullet']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", @@ -97,14 +97,16 @@ "from engibench.constraint import constraint\n", "from engibench.core import ObjectiveDirection\n", "from engibench.core import OptiStep\n", - "from engibench.core import Problem\n" + "from engibench.core import Problem\n", + "\n", + "import pybullet as p\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Implement battery cold-plate problem contract (TODO)\n", + "### Step 2 - Implement PyBullet manipulator co-design problem contract (TODO)\n", "\n", "Complete each required method with deterministic behavior and clear failure messages.\n" ] @@ -115,134 +117,90 @@ "metadata": {}, "outputs": [], "source": [ - "class BatteryColdPlate2DProblem(Problem[np.ndarray]):\n", - " \"\"\"Scaffold for a battery cold-plate topology problem (not currently in EngiBench).\"\"\"\n", + "class PlanarManipulatorCoDesignProblem(Problem[np.ndarray]):\n", + " \"\"\"Robotics co-design scaffold using a real PyBullet rollout loop.\"\"\"\n", "\n", " version = 0\n", " objectives = (\n", - " (\"max_temperature_c\", ObjectiveDirection.MINIMIZE),\n", - " (\"flow_penalty\", ObjectiveDirection.MINIMIZE),\n", + " (\"final_tracking_error_m\", ObjectiveDirection.MINIMIZE),\n", + " (\"actuation_energy_j\", ObjectiveDirection.MINIMIZE),\n", " )\n", "\n", " @dataclass\n", " class Conditions:\n", - " heat_load_left: Annotated[float, bounded(lower=0.1, upper=2.0)] = 1.0\n", - " heat_load_right: Annotated[float, bounded(lower=0.1, upper=2.0)] = 1.0\n", - " inlet_temp_c: Annotated[float, bounded(lower=10.0, upper=40.0)] = 25.0\n", - " flow_budget: Annotated[float, bounded(lower=0.15, upper=0.65)] = 0.35\n", + " target_x: Annotated[float, bounded(lower=0.20, upper=1.35)] = 0.85\n", + " target_y: Annotated[float, bounded(lower=0.05, upper=1.20)] = 0.45\n", + " payload_kg: Annotated[float, bounded(lower=0.0, upper=2.0)] = 0.8\n", + " disturbance_scale: Annotated[float, bounded(lower=0.0, upper=0.30)] = 0.05\n", "\n", " @dataclass\n", " class Config(Conditions):\n", - " resolution: Annotated[int, bounded(lower=16, upper=96)] = 32\n", - " max_iter: Annotated[int, bounded(lower=1, upper=200)] = 30\n", - " solver_iters: Annotated[int, bounded(lower=20, upper=500)] = 120\n", - " min_channel_fraction: Annotated[float, bounded(lower=0.05, upper=0.60)] = 0.18\n", - " max_channel_fraction: Annotated[float, bounded(lower=0.10, upper=0.85)] = 0.55\n", - " max_edge_density: Annotated[float, bounded(lower=0.01, upper=1.00)] = 0.28\n", - "\n", - " dataset_id = \"IDEALLab/battery_cold_plate_2d_v0\" # placeholder for future dataset integration\n", + " sim_steps: Annotated[int, bounded(lower=60, upper=1200)] = 240\n", + " dt: Annotated[float, bounded(lower=1e-4, upper=0.05)] = 1.0 / 120.0\n", + " torque_limit: Annotated[float, bounded(lower=1.0, upper=50.0)] = 12.0\n", + " max_iter: Annotated[int, bounded(lower=1, upper=300)] = 60\n", + "\n", + " dataset_id = \"IDEALLab/planar_manipulator_codesign_v0\" # placeholder for future dataset integration\n", " container_id = None\n", "\n", " def __init__(self, seed: int = 0, **kwargs):\n", " super().__init__(seed=seed)\n", " self.config = self.Config(**kwargs)\n", " self.conditions = self.Conditions(\n", - " heat_load_left=self.config.heat_load_left,\n", - " heat_load_right=self.config.heat_load_right,\n", - " inlet_temp_c=self.config.inlet_temp_c,\n", - " flow_budget=self.config.flow_budget,\n", + " target_x=self.config.target_x,\n", + " target_y=self.config.target_y,\n", + " payload_kg=self.config.payload_kg,\n", + " disturbance_scale=self.config.disturbance_scale,\n", " )\n", + "\n", + " # Design vector = [link1_m, link2_m, motor_strength, kp, kd, damping]\n", " self.design_space = spaces.Box(\n", - " low=0.0,\n", - " high=1.0,\n", - " shape=(self.config.resolution, self.config.resolution),\n", + " low=np.array([0.25, 0.20, 2.0, 5.0, 0.2, 0.0], dtype=np.float32),\n", + " high=np.array([1.00, 0.95, 30.0, 120.0, 18.0, 1.5], dtype=np.float32),\n", " dtype=np.float32,\n", " )\n", "\n", - " # TODO 1: add at least two design constraints using @constraint\n", - " # Suggested:\n", - " # - channel fraction between min_channel_fraction and max_channel_fraction\n", - " # - edge-density/manufacturability bound using max_edge_density\n", - " raise NotImplementedError('Implement design constraints and assign self.design_constraints')\n", + " # TODO 1: implement design constraints and assign self.design_constraints\n", + " # Suggested constraints:\n", + " # - reachable workspace: link1+link2 must reach target radius\n", + " # - gain consistency: kd must be bounded relative to kp\n", + " raise NotImplementedError('Implement __init__ constraints')\n", "\n", - " def _heat_map(self, cfg: dict) -> np.ndarray:\n", - " # TODO 2: implement two gaussian heat sources (left/right battery modules)\n", - " raise NotImplementedError('Implement _heat_map')\n", + " def _build_robot(self, l1: float, l2: float, payload_kg: float, damping: float) -> tuple[int, int]:\n", + " # TODO 2: build 2-link planar robot in PyBullet DIRECT mode\n", + " raise NotImplementedError('Implement _build_robot')\n", "\n", - " def _solve_temperature(self, conductivity: np.ndarray, heat: np.ndarray, inlet_temp: float, n_iter: int) -> np.ndarray:\n", - " # TODO 3: implement iterative finite-difference temperature solver\n", - " # Boundary suggestions:\n", - " # - left boundary fixed at inlet_temp\n", - " # - right boundary convective mix to ambient\n", - " # - top/bottom insulated copy\n", - " raise NotImplementedError('Implement _solve_temperature')\n", + " def _inverse_kinematics_2link(self, x: float, y: float, l1: float, l2: float) -> tuple[float, float]:\n", + " # TODO 3: implement closed-form IK for 2-link planar arm\n", + " raise NotImplementedError('Implement _inverse_kinematics_2link')\n", + "\n", + " def _forward_kinematics_2link(self, q1: float, q2: float, l1: float, l2: float) -> tuple[float, float]:\n", + " # TODO 4: implement FK for end-effector position\n", + " raise NotImplementedError('Implement _forward_kinematics_2link')\n", + "\n", + " def _rollout(self, design: np.ndarray, cfg: dict, return_trace: bool = False):\n", + " # TODO 5: run PyBullet rollout and compute objectives\n", + " # Required outputs:\n", + " # - final tracking error [m]\n", + " # - actuation energy [J]\n", + " # Optional trace for rendering: ee path, error curve, torque trace\n", + " raise NotImplementedError('Implement _rollout')\n", "\n", " def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n", - " # TODO 4: implement two-objective evaluation\n", - " # Objective 1: max temperature [C]\n", - " # Objective 2: flow_penalty (channel-fraction mismatch + roughness + mild temperature uniformity term)\n", + " # TODO 6: call rollout with cfg merge and clipping to design bounds\n", " raise NotImplementedError('Implement simulate')\n", "\n", " def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n", - " # TODO 5: implement a simple iterative optimizer and return (design, list[OptiStep])\n", - " # Keep it deterministic and lightweight for workshop runtime.\n", + " # TODO 7: implement deterministic local search + OptiStep history\n", " raise NotImplementedError('Implement optimize')\n", "\n", " def render(self, design: np.ndarray, *, open_window: bool = False):\n", - " import matplotlib.pyplot as plt\n", - "\n", - " # Visual semantics:\n", - " # - solid/channel map: 1.0 means conductive solid, 0.0 means coolant channel\n", - " # - conductivity map: effective thermal conductivity used by solver\n", - " # - heat map: imposed module heat loads (problem conditions)\n", - " # - temperature map: solved steady-state thermal field\n", - " x = np.clip(design.astype(np.float32), 0.0, 1.0)\n", - " channel = 1.0 - x\n", - "\n", - " cfg = self.config.__dict__\n", - " k_channel = 0.25\n", - " k_solid = 4.5\n", - " conductivity = k_channel + x * (k_solid - k_channel)\n", - " heat = self._heat_map(cfg)\n", - " temperature = self._solve_temperature(conductivity, heat, cfg[\"inlet_temp_c\"], cfg[\"solver_iters\"])\n", - "\n", - " fig, axes = plt.subplots(1, 4, figsize=(16, 4))\n", - "\n", - " im0 = axes[0].imshow(x, cmap=\"gray\", vmin=0, vmax=1)\n", - " axes[0].set_title(\"Design map\\n(1=solid, 0=channel)\")\n", - " axes[0].axis(\"off\")\n", - " fig.colorbar(im0, ax=axes[0], fraction=0.046, pad=0.04)\n", - "\n", - " im1 = axes[1].imshow(conductivity, cmap=\"cividis\")\n", - " axes[1].set_title(\"Conductivity field\")\n", - " axes[1].axis(\"off\")\n", - " fig.colorbar(im1, ax=axes[1], fraction=0.046, pad=0.04)\n", - "\n", - " im2 = axes[2].imshow(heat, cmap=\"magma\")\n", - " axes[2].set_title(\"Heat-load map\")\n", - " axes[2].axis(\"off\")\n", - " fig.colorbar(im2, ax=axes[2], fraction=0.046, pad=0.04)\n", - "\n", - " im3 = axes[3].imshow(temperature, cmap=\"inferno\")\n", - " axes[3].set_title(\"Temperature field [C]\")\n", - " axes[3].axis(\"off\")\n", - " fig.colorbar(im3, ax=axes[3], fraction=0.046, pad=0.04)\n", - "\n", - " channel_fraction = float(np.mean(channel))\n", - " edge_density = float(np.mean(np.abs(np.diff(channel, axis=0))) + np.mean(np.abs(np.diff(channel, axis=1))))\n", - " fig.suptitle(\n", - " f\"channel_fraction={channel_fraction:.3f}, edge_density={edge_density:.3f}, \"\n", - " f\"max_temp={float(np.max(temperature)):.2f}C\",\n", - " y=1.03,\n", - " )\n", - "\n", - " fig.tight_layout()\n", - " if open_window:\n", - " plt.show()\n", - " return fig, axes\n", + " # TODO 8: create 4-panel interpretation plot\n", + " # Suggested panels: design vars, task-space path, error-vs-time, torque-vs-time\n", + " raise NotImplementedError('Implement render')\n", "\n", " def random_design(self):\n", - " # TODO 6: implement smooth random design initialization\n", + " # TODO 9: sample uniformly in design bounds\n", " raise NotImplementedError('Implement random_design')\n" ] }, @@ -255,7 +213,10 @@ "Run minimal checks to verify interface consistency and simulator behavior.\n", "\n", "\n", - "Use the multi-panel render to read **where heat enters**, **how material is distributed**, and **where thermal bottlenecks remain**.\n" + "Use the multi-panel render to read **where heat enters**, **how material is distributed**, and **where thermal bottlenecks remain**.\n", + "\n", + "\n", + "Use the final figure to interpret whether the design/controller combination reaches the target robustly with acceptable energy use.\n" ] }, { @@ -264,29 +225,27 @@ "metadata": {}, "outputs": [], "source": [ - "# Run this cell after finishing TODOs in BatteryColdPlate2DProblem\n", - "problem = BatteryColdPlate2DProblem(\n", + "# Run this cell after finishing TODOs in PlanarManipulatorCoDesignProblem\n", + "problem = PlanarManipulatorCoDesignProblem(\n", " seed=42,\n", - " resolution=32,\n", - " max_iter=20,\n", - " heat_load_left=1.4,\n", - " heat_load_right=1.1,\n", - " inlet_temp_c=24.0,\n", - " flow_budget=0.33,\n", + " target_x=0.9,\n", + " target_y=0.45,\n", + " payload_kg=0.8,\n", + " disturbance_scale=0.04,\n", + " sim_steps=220,\n", + " max_iter=40,\n", ")\n", "start, _ = problem.random_design()\n", "\n", "cfg = {\n", - " 'heat_load_left': 1.4,\n", - " 'heat_load_right': 1.1,\n", - " 'inlet_temp_c': 24.0,\n", - " 'flow_budget': 0.33,\n", - " 'resolution': 32,\n", - " 'max_iter': 20,\n", - " 'solver_iters': 100,\n", - " 'min_channel_fraction': 0.18,\n", - " 'max_channel_fraction': 0.55,\n", - " 'max_edge_density': 0.35,\n", + " 'target_x': 0.9,\n", + " 'target_y': 0.45,\n", + " 'payload_kg': 0.8,\n", + " 'disturbance_scale': 0.04,\n", + " 'sim_steps': 220,\n", + " 'dt': 1.0 / 120.0,\n", + " 'torque_limit': 12.0,\n", + " 'max_iter': 40,\n", "}\n", "\n", "print('design space:', problem.design_space)\n", @@ -300,14 +259,12 @@ "opt_design, history = problem.optimize(start, config=cfg)\n", "objf = problem.simulate(opt_design, config=cfg)\n", "\n", - "print('initial objectives [max_temp_c, flow_penalty]:', obj0.tolist())\n", - "print('final objectives [max_temp_c, flow_penalty]:', objf.tolist())\n", + "print('initial objectives [tracking_error_m, energy_J]:', obj0.tolist())\n", + "print('final objectives [tracking_error_m, energy_J]:', objf.tolist())\n", "print('optimization steps:', len(history))\n", + "print('How to read plots: vars | task-space path | error timeline | torque timeline')\n", "\n", - "problem.render(opt_design)\n", - "\n", - "\n", - "print('How to read plots: design(1=solid,0=channel) | conductivity | heat-load | temperature')\n" + "problem.render(opt_design)\n" ] }, { @@ -316,7 +273,7 @@ "source": [ "## Mapping to real EngiBench contributions\n", "\n", - "Translate this battery cold-plate scaffold into domain-specific simulators and datasets with documented assumptions.\n" + "Translate this robotics co-design scaffold into domain-specific simulators and datasets (robotics, controls, etc.) with documented assumptions.\n" ] }, { diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index db677fb..bec934f 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -60,7 +60,7 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium']\n", + "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium', 'pybullet']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", @@ -97,14 +97,16 @@ "from engibench.constraint import constraint\n", "from engibench.core import ObjectiveDirection\n", "from engibench.core import OptiStep\n", - "from engibench.core import Problem\n" + "from engibench.core import Problem\n", + "\n", + "import pybullet as p\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Step 2 - Implement battery cold-plate problem contract\n", + "### Step 2 - Implement PyBullet manipulator co-design problem contract\n", "\n", "Ensure methods are deterministic and constraints/objectives are semantically explicit.\n" ] @@ -115,217 +117,264 @@ "metadata": {}, "outputs": [], "source": [ - "class BatteryColdPlate2DProblem(Problem[np.ndarray]):\n", - " \"\"\"Scaffold for a battery cold-plate topology problem (not currently in EngiBench).\"\"\"\n", + "class PlanarManipulatorCoDesignProblem(Problem[np.ndarray]):\n", + " \"\"\"Robotics co-design scaffold using a real PyBullet rollout loop.\"\"\"\n", "\n", " version = 0\n", " objectives = (\n", - " (\"max_temperature_c\", ObjectiveDirection.MINIMIZE),\n", - " (\"flow_penalty\", ObjectiveDirection.MINIMIZE),\n", + " (\"final_tracking_error_m\", ObjectiveDirection.MINIMIZE),\n", + " (\"actuation_energy_j\", ObjectiveDirection.MINIMIZE),\n", " )\n", "\n", " @dataclass\n", " class Conditions:\n", - " heat_load_left: Annotated[float, bounded(lower=0.1, upper=2.0)] = 1.0\n", - " heat_load_right: Annotated[float, bounded(lower=0.1, upper=2.0)] = 1.0\n", - " inlet_temp_c: Annotated[float, bounded(lower=10.0, upper=40.0)] = 25.0\n", - " flow_budget: Annotated[float, bounded(lower=0.15, upper=0.65)] = 0.35\n", + " target_x: Annotated[float, bounded(lower=0.20, upper=1.35)] = 0.85\n", + " target_y: Annotated[float, bounded(lower=0.05, upper=1.20)] = 0.45\n", + " payload_kg: Annotated[float, bounded(lower=0.0, upper=2.0)] = 0.8\n", + " disturbance_scale: Annotated[float, bounded(lower=0.0, upper=0.30)] = 0.05\n", "\n", " @dataclass\n", " class Config(Conditions):\n", - " resolution: Annotated[int, bounded(lower=16, upper=96)] = 32\n", - " max_iter: Annotated[int, bounded(lower=1, upper=200)] = 30\n", - " solver_iters: Annotated[int, bounded(lower=20, upper=500)] = 120\n", - " min_channel_fraction: Annotated[float, bounded(lower=0.05, upper=0.60)] = 0.18\n", - " max_channel_fraction: Annotated[float, bounded(lower=0.10, upper=0.85)] = 0.55\n", - " max_edge_density: Annotated[float, bounded(lower=0.01, upper=1.00)] = 0.28\n", - "\n", - " dataset_id = \"IDEALLab/battery_cold_plate_2d_v0\" # placeholder for future dataset integration\n", + " sim_steps: Annotated[int, bounded(lower=60, upper=1200)] = 240\n", + " dt: Annotated[float, bounded(lower=1e-4, upper=0.05)] = 1.0 / 120.0\n", + " torque_limit: Annotated[float, bounded(lower=1.0, upper=50.0)] = 12.0\n", + " max_iter: Annotated[int, bounded(lower=1, upper=300)] = 60\n", + "\n", + " dataset_id = \"IDEALLab/planar_manipulator_codesign_v0\" # placeholder for future dataset integration\n", " container_id = None\n", "\n", " def __init__(self, seed: int = 0, **kwargs):\n", " super().__init__(seed=seed)\n", " self.config = self.Config(**kwargs)\n", " self.conditions = self.Conditions(\n", - " heat_load_left=self.config.heat_load_left,\n", - " heat_load_right=self.config.heat_load_right,\n", - " inlet_temp_c=self.config.inlet_temp_c,\n", - " flow_budget=self.config.flow_budget,\n", + " target_x=self.config.target_x,\n", + " target_y=self.config.target_y,\n", + " payload_kg=self.config.payload_kg,\n", + " disturbance_scale=self.config.disturbance_scale,\n", " )\n", + "\n", + " # Design vector = [link1_m, link2_m, motor_strength, kp, kd, damping]\n", " self.design_space = spaces.Box(\n", - " low=0.0,\n", - " high=1.0,\n", - " shape=(self.config.resolution, self.config.resolution),\n", + " low=np.array([0.25, 0.20, 2.0, 5.0, 0.2, 0.0], dtype=np.float32),\n", + " high=np.array([1.00, 0.95, 30.0, 120.0, 18.0, 1.5], dtype=np.float32),\n", " dtype=np.float32,\n", " )\n", "\n", " @constraint\n", - " def channel_fraction(design: np.ndarray, min_channel_fraction: float, max_channel_fraction: float, **_) -> None:\n", - " cf = float(np.mean(1.0 - design))\n", - " assert min_channel_fraction <= cf <= max_channel_fraction, (\n", - " f\"channel_fraction={cf:.3f} outside [{min_channel_fraction:.3f}, {max_channel_fraction:.3f}]\"\n", - " )\n", + " def reachable_workspace(design: np.ndarray, target_x: float, target_y: float, **_) -> None:\n", + " l1, l2 = float(design[0]), float(design[1])\n", + " r = float(np.sqrt(target_x**2 + target_y**2))\n", + " assert l1 + l2 >= r + 0.03, f\"target radius {r:.3f} exceeds reach {l1+l2:.3f}\"\n", "\n", " @constraint\n", - " def edge_density(design: np.ndarray, max_edge_density: float, **_) -> None:\n", - " tv = float(np.mean(np.abs(np.diff(design, axis=0))) + np.mean(np.abs(np.diff(design, axis=1))))\n", - " assert tv <= max_edge_density, f\"edge_density={tv:.3f} exceeds {max_edge_density:.3f}\"\n", - "\n", - " self.design_constraints = [channel_fraction, edge_density]\n", - "\n", - " def _heat_map(self, cfg: dict) -> np.ndarray:\n", - " h, w = self.design_space.shape\n", - " yy, xx = np.indices((h, w), dtype=np.float32)\n", - " s = 0.08 * min(h, w)\n", - "\n", - " left = np.exp(-(((xx - 0.22 * w) ** 2 + (yy - 0.35 * h) ** 2) / (2.0 * s**2)))\n", - " right = np.exp(-(((xx - 0.78 * w) ** 2 + (yy - 0.65 * h) ** 2) / (2.0 * s**2)))\n", - " heat = cfg[\"heat_load_left\"] * left + cfg[\"heat_load_right\"] * right\n", - " return heat.astype(np.float32)\n", - "\n", - " def _solve_temperature(self, conductivity: np.ndarray, heat: np.ndarray, inlet_temp: float, n_iter: int) -> np.ndarray:\n", - " T = np.full_like(conductivity, float(inlet_temp), dtype=np.float32)\n", - " ambient = float(inlet_temp) + 5.0\n", - "\n", - " for _ in range(int(n_iter)):\n", - " T_old = T.copy()\n", - "\n", - " k_c = conductivity[1:-1, 1:-1]\n", - " k_e = 0.5 * (k_c + conductivity[1:-1, 2:])\n", - " k_w = 0.5 * (k_c + conductivity[1:-1, :-2])\n", - " k_n = 0.5 * (k_c + conductivity[:-2, 1:-1])\n", - " k_s = 0.5 * (k_c + conductivity[2:, 1:-1])\n", - "\n", - " numer = (\n", - " k_e * T_old[1:-1, 2:]\n", - " + k_w * T_old[1:-1, :-2]\n", - " + k_n * T_old[:-2, 1:-1]\n", - " + k_s * T_old[2:, 1:-1]\n", - " + 0.02 * heat[1:-1, 1:-1]\n", - " )\n", - " denom = k_e + k_w + k_n + k_s + 1e-6\n", - " T[1:-1, 1:-1] = numer / denom\n", - "\n", - " T[:, 0] = inlet_temp\n", - " T[:, -1] = 0.7 * T[:, -2] + 0.3 * ambient\n", - " T[0, :] = T[1, :]\n", - " T[-1, :] = T[-2, :]\n", - "\n", - " return T\n", - "\n", - " def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n", - " cfg = {**self.__dict__[\"config\"].__dict__, **(config or {})}\n", - "\n", - " x = np.clip(design.astype(np.float32), 0.0, 1.0)\n", - " channel = 1.0 - x\n", + " def gain_consistency(design: np.ndarray, **_) -> None:\n", + " kp, kd = float(design[3]), float(design[4])\n", + " assert kd <= 2.2 * np.sqrt(max(kp, 1e-6)), f\"kd={kd:.3f} too high for kp={kp:.3f}\"\n", + "\n", + " self.design_constraints = [reachable_workspace, gain_consistency]\n", + "\n", + " def _build_robot(self, l1: float, l2: float, payload_kg: float, damping: float) -> tuple[int, int]:\n", + " p.resetSimulation()\n", + " p.setGravity(0, 0, -9.81)\n", + "\n", + " link_masses = [0.5 + 0.2 * payload_kg, 0.35 + 0.25 * payload_kg]\n", + " link_collision = [-1, -1]\n", + " link_visual = [\n", + " p.createVisualShape(p.GEOM_CAPSULE, radius=0.025, length=l1, rgbaColor=[0.2, 0.5, 0.9, 1.0]),\n", + " p.createVisualShape(p.GEOM_CAPSULE, radius=0.020, length=l2, rgbaColor=[0.9, 0.4, 0.2, 1.0]),\n", + " ]\n", + " qx = p.getQuaternionFromEuler([0.0, np.pi / 2.0, 0.0])\n", + "\n", + " robot = p.createMultiBody(\n", + " baseMass=0.0,\n", + " baseCollisionShapeIndex=-1,\n", + " baseVisualShapeIndex=-1,\n", + " basePosition=[0, 0, 0],\n", + " linkMasses=link_masses,\n", + " linkCollisionShapeIndices=link_collision,\n", + " linkVisualShapeIndices=link_visual,\n", + " linkPositions=[[0, 0, 0], [l1, 0, 0]],\n", + " linkOrientations=[qx, qx],\n", + " linkInertialFramePositions=[[l1 / 2.0, 0, 0], [l2 / 2.0, 0, 0]],\n", + " linkInertialFrameOrientations=[[0, 0, 0, 1], [0, 0, 0, 1]],\n", + " linkParentIndices=[0, 1],\n", + " linkJointTypes=[p.JOINT_REVOLUTE, p.JOINT_REVOLUTE],\n", + " linkJointAxis=[[0, 0, 1], [0, 0, 1]],\n", + " )\n", "\n", - " k_channel = 0.25\n", - " k_solid = 4.5\n", - " conductivity = k_channel + x * (k_solid - k_channel)\n", + " for j in [0, 1]:\n", + " p.changeDynamics(robot, j, linearDamping=0.0, angularDamping=float(damping))\n", + "\n", + " return robot, 1\n", + "\n", + " def _inverse_kinematics_2link(self, x: float, y: float, l1: float, l2: float) -> tuple[float, float]:\n", + " r2 = x * x + y * y\n", + " c2 = (r2 - l1 * l1 - l2 * l2) / (2.0 * l1 * l2)\n", + " c2 = float(np.clip(c2, -1.0, 1.0))\n", + " s2 = float(np.sqrt(max(0.0, 1.0 - c2 * c2)))\n", + " q2 = float(np.arctan2(s2, c2))\n", + " q1 = float(np.arctan2(y, x) - np.arctan2(l2 * s2, l1 + l2 * c2))\n", + " return q1, q2\n", + "\n", + " def _forward_kinematics_2link(self, q1: float, q2: float, l1: float, l2: float) -> tuple[float, float]:\n", + " x = l1 * np.cos(q1) + l2 * np.cos(q1 + q2)\n", + " y = l1 * np.sin(q1) + l2 * np.sin(q1 + q2)\n", + " return float(x), float(y)\n", + "\n", + " def _rollout(self, design: np.ndarray, cfg: dict, return_trace: bool = False):\n", + " l1, l2, motor_strength, kp, kd, damping = [float(v) for v in design]\n", + "\n", + " cid = p.connect(p.DIRECT)\n", + " try:\n", + " robot, _ = self._build_robot(l1, l2, cfg[\"payload_kg\"], damping)\n", + " q1_t, q2_t = self._inverse_kinematics_2link(cfg[\"target_x\"], cfg[\"target_y\"], l1, l2)\n", + "\n", + " err_trace = []\n", + " tau_trace = []\n", + " ee_trace = []\n", + " energy = 0.0\n", + "\n", + " for _step in range(int(cfg[\"sim_steps\"])):\n", + " for j, q_t in enumerate([q1_t, q2_t]):\n", + " p.setJointMotorControl2(\n", + " bodyUniqueId=robot,\n", + " jointIndex=j,\n", + " controlMode=p.POSITION_CONTROL,\n", + " targetPosition=q_t,\n", + " positionGain=float(kp) / 120.0,\n", + " velocityGain=float(kd) / 50.0,\n", + " force=float(cfg[\"torque_limit\"]) * float(motor_strength),\n", + " )\n", + "\n", + " if cfg[\"disturbance_scale\"] > 0:\n", + " disturb = self.np_random.normal(0.0, cfg[\"disturbance_scale\"], size=2)\n", + " p.applyExternalTorque(robot, 0, [0, 0, float(disturb[0])], p.LINK_FRAME)\n", + " p.applyExternalTorque(robot, 1, [0, 0, float(disturb[1])], p.LINK_FRAME)\n", + "\n", + " p.stepSimulation()\n", + "\n", + " js0 = p.getJointState(robot, 0)\n", + " js1 = p.getJointState(robot, 1)\n", + " q1, q2 = float(js0[0]), float(js1[0])\n", + " dq1, dq2 = float(js0[1]), float(js1[1])\n", + " tau1, tau2 = float(js0[3]), float(js1[3])\n", + "\n", + " ee_x, ee_y = self._forward_kinematics_2link(q1, q2, l1, l2)\n", + " err = float(np.sqrt((ee_x - cfg[\"target_x\"]) ** 2 + (ee_y - cfg[\"target_y\"]) ** 2))\n", + "\n", + " err_trace.append(err)\n", + " tau_trace.append((tau1, tau2))\n", + " ee_trace.append((ee_x, ee_y))\n", + " energy += (abs(tau1 * dq1) + abs(tau2 * dq2)) * float(cfg[\"dt\"])\n", + "\n", + " final_error = float(err_trace[-1])\n", + " obj = np.array([final_error, float(energy)], dtype=np.float32)\n", + "\n", + " if return_trace:\n", + " trace = {\n", + " \"ee_trace\": np.array(ee_trace, dtype=np.float32),\n", + " \"err_trace\": np.array(err_trace, dtype=np.float32),\n", + " \"tau_trace\": np.array(tau_trace, dtype=np.float32),\n", + " \"target\": np.array([cfg[\"target_x\"], cfg[\"target_y\"]], dtype=np.float32),\n", + " \"design\": np.array(design, dtype=np.float32),\n", + " \"objectives\": obj,\n", + " }\n", + " return obj, trace\n", + "\n", + " return obj\n", + " finally:\n", + " p.disconnect(cid)\n", "\n", - " heat = self._heat_map(cfg)\n", - " T = self._solve_temperature(conductivity, heat, cfg[\"inlet_temp_c\"], cfg[\"solver_iters\"])\n", + " def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n", + " cfg = {**self.config.__dict__, **(config or {})}\n", + " x = np.clip(design.astype(np.float32), self.design_space.low, self.design_space.high)\n", + " return self._rollout(x, cfg, return_trace=False)\n", "\n", - " max_temp = float(np.max(T))\n", - " temp_std = float(np.std(T))\n", + " def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n", + " cfg = {**self.config.__dict__, **(config or {})}\n", + " x = np.clip(starting_point.astype(np.float32), self.design_space.low, self.design_space.high)\n", "\n", - " channel_fraction = float(np.mean(channel))\n", - " edge_density = float(np.mean(np.abs(np.diff(channel, axis=0))) + np.mean(np.abs(np.diff(channel, axis=1))))\n", - " flow_penalty = abs(channel_fraction - cfg[\"flow_budget\"]) + 0.15 * edge_density + 0.05 * temp_std\n", + " best = x.copy()\n", + " best_obj = self.simulate(best, cfg)\n", + " best_score = float(best_obj[0] + 0.02 * best_obj[1])\n", "\n", - " return np.array([max_temp, float(flow_penalty)], dtype=np.float32)\n", + " history = [OptiStep(obj_values=best_obj, step=0)]\n", + " step_scale = np.array([0.05, 0.05, 2.5, 8.0, 1.2, 0.08], dtype=np.float32)\n", "\n", - " def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n", - " cfg = {**self.__dict__[\"config\"].__dict__, **(config or {})}\n", - " x = np.clip(starting_point.astype(np.float32), 0.0, 1.0)\n", - " history = []\n", + " for step in range(1, int(cfg[\"max_iter\"]) + 1):\n", + " candidate = best + self.np_random.normal(0.0, 1.0, size=6).astype(np.float32) * step_scale\n", + " candidate = np.clip(candidate, self.design_space.low, self.design_space.high)\n", "\n", - " h, w = x.shape\n", - " yy, xx = np.indices((h, w), dtype=np.float32)\n", - " thermal_bias = np.exp(-(((xx - 0.50 * w) ** 2 + (yy - 0.50 * h) ** 2) / (2.0 * (0.28 * min(h, w)) ** 2)))\n", - " thermal_bias = (thermal_bias - thermal_bias.min()) / (thermal_bias.max() - thermal_bias.min() + 1e-8)\n", + " if self.check_constraints(candidate, cfg):\n", + " history.append(OptiStep(obj_values=np.array([np.inf, np.inf], dtype=np.float32), step=step))\n", + " continue\n", "\n", - " target_density = np.clip(1.0 - cfg[\"flow_budget\"], 0.2, 0.9)\n", + " obj = self.simulate(candidate, cfg)\n", + " score = float(obj[0] + 0.02 * obj[1])\n", + " if score < best_score:\n", + " best, best_obj, best_score = candidate, obj, score\n", "\n", - " for step in range(cfg[\"max_iter\"]):\n", - " neighbor = (\n", - " x\n", - " + np.roll(x, 1, axis=0)\n", - " + np.roll(x, -1, axis=0)\n", - " + np.roll(x, 1, axis=1)\n", - " + np.roll(x, -1, axis=1)\n", - " ) / 5.0\n", - " x = 0.70 * x + 0.20 * neighbor + 0.08 * target_density + 0.02 * thermal_bias\n", - " x = np.clip(x, 0.0, 1.0)\n", + " history.append(OptiStep(obj_values=best_obj, step=step))\n", "\n", - " history.append(OptiStep(obj_values=self.simulate(x, cfg), step=step))\n", - "\n", - " return x, history\n", + " return best, history\n", "\n", " def render(self, design: np.ndarray, *, open_window: bool = False):\n", " import matplotlib.pyplot as plt\n", "\n", - " # Visual semantics:\n", - " # - solid/channel map: 1.0 means conductive solid, 0.0 means coolant channel\n", - " # - conductivity map: effective thermal conductivity used by solver\n", - " # - heat map: imposed module heat loads (problem conditions)\n", - " # - temperature map: solved steady-state thermal field\n", - " x = np.clip(design.astype(np.float32), 0.0, 1.0)\n", - " channel = 1.0 - x\n", - "\n", " cfg = self.config.__dict__\n", - " k_channel = 0.25\n", - " k_solid = 4.5\n", - " conductivity = k_channel + x * (k_solid - k_channel)\n", - " heat = self._heat_map(cfg)\n", - " temperature = self._solve_temperature(conductivity, heat, cfg[\"inlet_temp_c\"], cfg[\"solver_iters\"])\n", - "\n", - " fig, axes = plt.subplots(1, 4, figsize=(16, 4))\n", - "\n", - " im0 = axes[0].imshow(x, cmap=\"gray\", vmin=0, vmax=1)\n", - " axes[0].set_title(\"Design map\\n(1=solid, 0=channel)\")\n", - " axes[0].axis(\"off\")\n", - " fig.colorbar(im0, ax=axes[0], fraction=0.046, pad=0.04)\n", - "\n", - " im1 = axes[1].imshow(conductivity, cmap=\"cividis\")\n", - " axes[1].set_title(\"Conductivity field\")\n", - " axes[1].axis(\"off\")\n", - " fig.colorbar(im1, ax=axes[1], fraction=0.046, pad=0.04)\n", - "\n", - " im2 = axes[2].imshow(heat, cmap=\"magma\")\n", - " axes[2].set_title(\"Heat-load map\")\n", - " axes[2].axis(\"off\")\n", - " fig.colorbar(im2, ax=axes[2], fraction=0.046, pad=0.04)\n", - "\n", - " im3 = axes[3].imshow(temperature, cmap=\"inferno\")\n", - " axes[3].set_title(\"Temperature field [C]\")\n", - " axes[3].axis(\"off\")\n", - " fig.colorbar(im3, ax=axes[3], fraction=0.046, pad=0.04)\n", - "\n", - " channel_fraction = float(np.mean(channel))\n", - " edge_density = float(np.mean(np.abs(np.diff(channel, axis=0))) + np.mean(np.abs(np.diff(channel, axis=1))))\n", + " x = np.clip(design.astype(np.float32), self.design_space.low, self.design_space.high)\n", + " obj, trace = self._rollout(x, cfg, return_trace=True)\n", + "\n", + " ee = trace[\"ee_trace\"]\n", + " err = trace[\"err_trace\"]\n", + " target = trace[\"target\"]\n", + " tau = trace[\"tau_trace\"]\n", + "\n", + " fig, axes = plt.subplots(1, 4, figsize=(17, 4.2))\n", + "\n", + " labels = [\"link1\", \"link2\", \"motor\", \"kp\", \"kd\", \"damping\"]\n", + " axes[0].bar(labels, x, color=['#4c78a8', '#4c78a8', '#f58518', '#54a24b', '#e45756', '#72b7b2'])\n", + " axes[0].set_title(\"Design variables\")\n", + " axes[0].tick_params(axis='x', rotation=35)\n", + "\n", + " axes[1].plot(ee[:, 0], ee[:, 1], lw=2, label=\"end-effector path\")\n", + " axes[1].scatter([target[0]], [target[1]], c='red', marker='x', s=70, label='target')\n", + " r = x[0] + x[1]\n", + " circle = plt.Circle((0, 0), r, color='gray', fill=False, linestyle='--', alpha=0.5)\n", + " axes[1].add_patch(circle)\n", + " axes[1].set_aspect('equal', 'box')\n", + " axes[1].set_title(\"Task-space trajectory\")\n", + " axes[1].set_xlabel('x [m]')\n", + " axes[1].set_ylabel('y [m]')\n", + " axes[1].legend(fontsize=8)\n", + "\n", + " axes[2].plot(err, color='#e45756')\n", + " axes[2].set_title(\"Tracking error over time\")\n", + " axes[2].set_xlabel(\"step\")\n", + " axes[2].set_ylabel(\"error [m]\")\n", + " axes[2].grid(alpha=0.3)\n", + "\n", + " axes[3].plot(np.abs(tau[:, 0]), label='|tau1|')\n", + " axes[3].plot(np.abs(tau[:, 1]), label='|tau2|')\n", + " axes[3].set_title(\"Actuation effort\")\n", + " axes[3].set_xlabel(\"step\")\n", + " axes[3].set_ylabel(\"torque [Nm]\")\n", + " axes[3].legend(fontsize=8)\n", + " axes[3].grid(alpha=0.3)\n", + "\n", " fig.suptitle(\n", - " f\"channel_fraction={channel_fraction:.3f}, edge_density={edge_density:.3f}, \"\n", - " f\"max_temp={float(np.max(temperature)):.2f}C\",\n", + " f\"Objectives: final_error={obj[0]:.4f} m, energy={obj[1]:.3f} J\",\n", " y=1.03,\n", " )\n", - "\n", " fig.tight_layout()\n", + "\n", " if open_window:\n", " plt.show()\n", " return fig, axes\n", "\n", " def random_design(self):\n", - " x = self.np_random.random(self.design_space.shape).astype(np.float32)\n", - " for _ in range(3):\n", - " x = (\n", - " x\n", - " + np.roll(x, 1, axis=0)\n", - " + np.roll(x, -1, axis=0)\n", - " + np.roll(x, 1, axis=1)\n", - " + np.roll(x, -1, axis=1)\n", - " ) / 5.0\n", - " return np.clip(x, 0.0, 1.0), -1\n" + " d = self.np_random.uniform(self.design_space.low, self.design_space.high).astype(np.float32)\n", + " return d, -1\n" ] }, { @@ -337,7 +386,10 @@ "Validate behavior with simple checks before scaling to real domains.\n", "\n", "\n", - "Use the multi-panel render to read **where heat enters**, **how material is distributed**, and **where thermal bottlenecks remain**.\n" + "Use the multi-panel render to read **where heat enters**, **how material is distributed**, and **where thermal bottlenecks remain**.\n", + "\n", + "\n", + "Use the final figure to interpret whether the design/controller combination reaches the target robustly with acceptable energy use.\n" ] }, { @@ -346,28 +398,26 @@ "metadata": {}, "outputs": [], "source": [ - "problem = BatteryColdPlate2DProblem(\n", + "problem = PlanarManipulatorCoDesignProblem(\n", " seed=42,\n", - " resolution=32,\n", - " max_iter=20,\n", - " heat_load_left=1.4,\n", - " heat_load_right=1.1,\n", - " inlet_temp_c=24.0,\n", - " flow_budget=0.33,\n", + " target_x=0.9,\n", + " target_y=0.45,\n", + " payload_kg=0.8,\n", + " disturbance_scale=0.04,\n", + " sim_steps=220,\n", + " max_iter=40,\n", ")\n", "start, _ = problem.random_design()\n", "\n", "cfg = {\n", - " 'heat_load_left': 1.4,\n", - " 'heat_load_right': 1.1,\n", - " 'inlet_temp_c': 24.0,\n", - " 'flow_budget': 0.33,\n", - " 'resolution': 32,\n", - " 'max_iter': 20,\n", - " 'solver_iters': 100,\n", - " 'min_channel_fraction': 0.18,\n", - " 'max_channel_fraction': 0.55,\n", - " 'max_edge_density': 0.35,\n", + " 'target_x': 0.9,\n", + " 'target_y': 0.45,\n", + " 'payload_kg': 0.8,\n", + " 'disturbance_scale': 0.04,\n", + " 'sim_steps': 220,\n", + " 'dt': 1.0 / 120.0,\n", + " 'torque_limit': 12.0,\n", + " 'max_iter': 40,\n", "}\n", "\n", "print('design space:', problem.design_space)\n", @@ -381,14 +431,12 @@ "opt_design, history = problem.optimize(start, config=cfg)\n", "objf = problem.simulate(opt_design, config=cfg)\n", "\n", - "print('initial objectives [max_temp_c, flow_penalty]:', obj0.tolist())\n", - "print('final objectives [max_temp_c, flow_penalty]:', objf.tolist())\n", + "print('initial objectives [tracking_error_m, energy_J]:', obj0.tolist())\n", + "print('final objectives [tracking_error_m, energy_J]:', objf.tolist())\n", "print('optimization steps:', len(history))\n", + "print('How to read plots: vars | task-space path | error timeline | torque timeline')\n", "\n", - "problem.render(opt_design)\n", - "\n", - "\n", - "print('How to read plots: design(1=solid,0=channel) | conductivity | heat-load | temperature')\n" + "problem.render(opt_design)\n" ] }, { From ed97f11def87ff22e65c85b965f2c1b9cb4c3d7f Mon Sep 17 00:00:00 2001 From: Soheyl Date: Tue, 10 Mar 2026 14:59:33 +0100 Subject: [PATCH 25/37] Switch workshop notebook bootstrap cells to plain pip commands --- .../participant/00_setup_api_warmup.ipynb | 89 ++++++++++++++++--- .../dcc26/participant/01_train_generate.ipynb | 53 +++++++---- .../participant/02_evaluate_metrics.ipynb | 46 +++++++--- .../03_add_new_problem_scaffold.ipynb | 24 +++-- .../dcc26/solutions/00_setup_api_warmup.ipynb | 87 +++++++++++++++--- .../dcc26/solutions/01_train_generate.ipynb | 57 ++++++++---- .../dcc26/solutions/02_evaluate_metrics.ipynb | 47 +++++++--- .../03_add_new_problem_scaffold.ipynb | 24 +++-- 8 files changed, 337 insertions(+), 90 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 12b801a..cd978cb 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "e84ac3df", "metadata": {}, "source": [ "# Notebook 00 (Participant): Setup + API Warmup\n", @@ -12,6 +13,7 @@ }, { "cell_type": "markdown", + "id": "d6bb2d73", "metadata": {}, "source": [ "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" @@ -19,6 +21,7 @@ }, { "cell_type": "markdown", + "id": "a74f46db", "metadata": {}, "source": [ "## Notebook map\n", @@ -33,6 +36,7 @@ }, { "cell_type": "markdown", + "id": "e71be25d", "metadata": {}, "source": [ "## Standalone guide\n", @@ -45,6 +49,7 @@ }, { "cell_type": "markdown", + "id": "16df002f", "metadata": {}, "source": [ "## Why this warmup matters\n", @@ -55,6 +60,7 @@ }, { "cell_type": "markdown", + "id": "d483dfbe", "metadata": {}, "source": [ "## Optional install cell (fresh Colab)\n", @@ -66,27 +72,27 @@ { "cell_type": "code", "execution_count": null, + "id": "de02c488", "metadata": {}, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", - "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", + "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", + " !pip install engibench[beams2d] matplotlib seaborn\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n" + " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" ] }, { "cell_type": "markdown", + "id": "35d3b7b8", "metadata": {}, "source": [ "### Step 1 - Initialize reproducible session\n", @@ -98,12 +104,28 @@ { "cell_type": "code", "execution_count": null, + "id": "cbdaa6d8", "metadata": {}, "outputs": [], - "source": "import random\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nimport engibench\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nprint('engibench version:', engibench.__version__)\nprint('seed:', SEED)" + "source": [ + "import random\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "import engibench\n", + "from engibench.problems.beams2d.v0 import Beams2D\n", + "\n", + "SEED = 7\n", + "random.seed(SEED)\n", + "np.random.seed(SEED)\n", + "\n", + "print('engibench version:', engibench.__version__)\n", + "print('seed:', SEED)" + ] }, { "cell_type": "markdown", + "id": "2e7a064e", "metadata": {}, "source": [ "### Step 2 - Instantiate the benchmark problem (TODO)\n", @@ -115,12 +137,27 @@ { "cell_type": "code", "execution_count": null, + "id": "1ab92d5b", "metadata": {}, "outputs": [], - "source": "# TODO 1: instantiate the problem with the global SEED\n# problem = ...\n\n# TODO 2: print these fields\n# - type(problem).__name__\n# - problem.design_space\n# - problem.objectives\n# - problem.conditions\n# - problem.conditions_keys\n# - problem.dataset_id\n\nraise NotImplementedError('Complete TODO 1/2 in this cell')" + "source": [ + "# TODO 1: instantiate the problem with the global SEED\n", + "# problem = ...\n", + "\n", + "# TODO 2: print these fields\n", + "# - type(problem).__name__\n", + "# - problem.design_space\n", + "# - problem.objectives\n", + "# - problem.conditions\n", + "# - problem.conditions_keys\n", + "# - problem.dataset_id\n", + "\n", + "raise NotImplementedError('Complete TODO 1/2 in this cell')" + ] }, { "cell_type": "markdown", + "id": "2d0d6faf", "metadata": {}, "source": [ "### Step 3 - Inspect dataset structure (TODO)\n", @@ -132,12 +169,24 @@ { "cell_type": "code", "execution_count": null, + "id": "851ef517", "metadata": {}, "outputs": [], - "source": "# TODO 3: load dataset and extract one sample\n# dataset = problem.dataset\n# sample_idx = 0\n# design = ...\n# config = ... # dict over problem.conditions_keys\n\n# print dataset summary and sample shapes/values\n\nraise NotImplementedError('Complete TODO 3 in this cell')" + "source": [ + "# TODO 3: load dataset and extract one sample\n", + "# dataset = problem.dataset\n", + "# sample_idx = 0\n", + "# design = ...\n", + "# config = ... # dict over problem.conditions_keys\n", + "\n", + "# print dataset summary and sample shapes/values\n", + "\n", + "raise NotImplementedError('Complete TODO 3 in this cell')" + ] }, { "cell_type": "markdown", + "id": "c3334a35", "metadata": {}, "source": [ "### Step 4 - Visualize one benchmark design\n", @@ -148,12 +197,19 @@ { "cell_type": "code", "execution_count": null, + "id": "34f0fb89", "metadata": {}, "outputs": [], - "source": "# Render the sampled design (run after TODO 3)\nfig, ax = problem.render(design)\nax.set_title('Participant: sampled Beams2D design')\nplt.show()" + "source": [ + "# Render the sampled design (run after TODO 3)\n", + "fig, ax = problem.render(design)\n", + "ax.set_title('Participant: sampled Beams2D design')\n", + "plt.show()" + ] }, { "cell_type": "markdown", + "id": "e7201d50", "metadata": {}, "source": [ "### Step 5 - Test constraint semantics (TODO)\n", @@ -165,12 +221,22 @@ { "cell_type": "code", "execution_count": null, + "id": "20936577", "metadata": {}, "outputs": [], - "source": "# TODO 4: run one explicit constraint check with an intentionally mismatched volfrac\n# bad_config = dict(config)\n# bad_config['volfrac'] = 0.2\n# violations = ...\n# print(len(violations)); print(violations) if any\n\nraise NotImplementedError('Complete TODO 4 in this cell')" + "source": [ + "# TODO 4: run one explicit constraint check with an intentionally mismatched volfrac\n", + "# bad_config = dict(config)\n", + "# bad_config['volfrac'] = 0.2\n", + "# violations = ...\n", + "# print(len(violations)); print(violations) if any\n", + "\n", + "raise NotImplementedError('Complete TODO 4 in this cell')" + ] }, { "cell_type": "markdown", + "id": "9b75437c", "metadata": {}, "source": [ "## Troubleshooting\n", @@ -181,6 +247,7 @@ }, { "cell_type": "markdown", + "id": "bbfd552e", "metadata": {}, "source": [ "## Next\n", @@ -190,6 +257,7 @@ }, { "cell_type": "markdown", + "id": "f41cb555", "metadata": {}, "source": [ "## Reflection prompts\n", @@ -200,6 +268,7 @@ }, { "cell_type": "markdown", + "id": "08c2b28a", "metadata": {}, "source": [ "## Takeaways\n", diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 1c2c979..de91ddc 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "0f062bbd", "metadata": {}, "source": [ "# Notebook 01 (Participant): Train + Generate with EngiOpt CGAN-2D\n", @@ -11,6 +12,7 @@ }, { "cell_type": "markdown", + "id": "51af8729", "metadata": {}, "source": [ "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" @@ -18,6 +20,7 @@ }, { "cell_type": "markdown", + "id": "71b11843", "metadata": {}, "source": [ "## Notebook map\n", @@ -32,6 +35,7 @@ }, { "cell_type": "markdown", + "id": "dcc7d40a", "metadata": {}, "source": [ "## Standalone guide\n", @@ -42,31 +46,30 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "edfc8c25", + "metadata": {}, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", - "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "FORCE_INSTALL = False # Set True to force install outside Colab\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing base dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", - " print('Installing EngiOpt from GitHub branch...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', ENGIOPT_GIT])\n", + " print('Installing dependencies...')\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib pandas tqdm tyro wandb\n", + " !pip install {ENGIOPT_GIT}\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n" + " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" ] }, { "cell_type": "markdown", + "id": "353f81db", "metadata": {}, "source": [ "## Part A: Setup\n", @@ -76,6 +79,7 @@ }, { "cell_type": "markdown", + "id": "ed1e478f", "metadata": {}, "source": [ "### EngiBench vs EngiOpt roles in this notebook\n", @@ -88,6 +92,7 @@ }, { "cell_type": "markdown", + "id": "c9b50673", "metadata": {}, "source": [ "### Step 1 - Configure reproducible environment\n", @@ -97,8 +102,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "2540a3a6", + "metadata": {}, "outputs": [], "source": [ "import json\n", @@ -159,6 +165,7 @@ }, { "cell_type": "markdown", + "id": "0998508c", "metadata": {}, "source": [ "### Step 2 - Build training slice from EngiBench dataset\n", @@ -168,8 +175,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "5047da37", + "metadata": {}, "outputs": [], "source": [ "problem = Beams2D(seed=SEED)\n", @@ -193,6 +201,7 @@ }, { "cell_type": "markdown", + "id": "5da6a98e", "metadata": {}, "source": [ "### Step 3 - Implement model setup (TODO)\n", @@ -203,8 +212,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "01999c27", + "metadata": {}, "outputs": [], "source": [ "# TODO 1: Instantiate model, optimizer, loss, and noise sampler.\n", @@ -221,6 +231,7 @@ }, { "cell_type": "markdown", + "id": "78d28002", "metadata": {}, "source": [ "### Step 4 - Implement train/load logic (TODO)\n", @@ -231,8 +242,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "8c1b69b4", + "metadata": {}, "outputs": [], "source": [ "TRAIN_FROM_SCRATCH = True\n", @@ -258,6 +270,7 @@ }, { "cell_type": "markdown", + "id": "9c006d13", "metadata": {}, "source": [ "### Step 5 - Implement generation logic (TODO)\n", @@ -268,8 +281,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "c36231fe", + "metadata": {}, "outputs": [], "source": [ "# TODO 3: Generate designs and prepare condition records.\n", @@ -287,6 +301,7 @@ }, { "cell_type": "markdown", + "id": "d0020828", "metadata": {}, "source": [ "### Step 6 - Implement artifact export (TODO)\n", @@ -297,8 +312,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "c16e7fbd", + "metadata": {}, "outputs": [], "source": [ "# TODO 4: Save Notebook 02 artifacts and (optionally) W&B artifact.\n", @@ -316,6 +332,7 @@ }, { "cell_type": "markdown", + "id": "5f367a8d", "metadata": {}, "source": [ "### Step 7 - Quick visual QA\n", @@ -325,8 +342,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "1f43530b", + "metadata": {}, "outputs": [], "source": [ "# Quick visual side-by-side snapshot\n", @@ -346,6 +364,7 @@ }, { "cell_type": "markdown", + "id": "af80a49e", "metadata": {}, "source": [ "## Troubleshooting\n", @@ -356,6 +375,7 @@ }, { "cell_type": "markdown", + "id": "a6c079de", "metadata": {}, "source": [ "## Next\n", @@ -365,6 +385,7 @@ }, { "cell_type": "markdown", + "id": "f1794303", "metadata": {}, "source": [ "## Takeaways\n", diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index 38942f8..c635c58 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "f51ce517", "metadata": {}, "source": [ "# Notebook 02 (Participant): Evaluation + Metrics\n", @@ -11,6 +12,7 @@ }, { "cell_type": "markdown", + "id": "71df2f22", "metadata": {}, "source": [ "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" @@ -18,6 +20,7 @@ }, { "cell_type": "markdown", + "id": "9efcdfa1", "metadata": {}, "source": [ "## Notebook map\n", @@ -32,6 +35,7 @@ }, { "cell_type": "markdown", + "id": "5fdfcb0f", "metadata": {}, "source": [ "## Standalone guide\n", @@ -41,31 +45,30 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "e82e4315", + "metadata": {}, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", - "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "FORCE_INSTALL = False # Set True to force install outside Colab\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing base dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", - " print('Installing EngiOpt from GitHub branch...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', ENGIOPT_GIT])\n", + " print('Installing dependencies...')\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib pandas tqdm tyro wandb\n", + " !pip install {ENGIOPT_GIT}\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n" + " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" ] }, { "cell_type": "markdown", + "id": "1a859723", "metadata": {}, "source": [ "## Artifact loading\n", @@ -76,6 +79,7 @@ }, { "cell_type": "markdown", + "id": "f39cd896", "metadata": {}, "source": [ "### Why these metrics matter for benchmarking\n", @@ -86,6 +90,7 @@ }, { "cell_type": "markdown", + "id": "81355ad2", "metadata": {}, "source": [ "### Step 1 - Resolve artifact source and recovery path\n", @@ -95,8 +100,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "c1cc3e0e", + "metadata": {}, "outputs": [], "source": [ "import json\n", @@ -299,6 +305,7 @@ }, { "cell_type": "markdown", + "id": "82f21c6f", "metadata": {}, "source": [ "### Step 2 - Implement per-sample evaluation (TODO)\n", @@ -308,8 +315,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "553ff3c9", + "metadata": {}, "outputs": [], "source": [ "problem = Beams2D(seed=7)\n", @@ -332,6 +340,7 @@ }, { "cell_type": "markdown", + "id": "fbcc2466", "metadata": {}, "source": [ "### Step 3 - Implement summary metrics (TODO)\n", @@ -341,8 +350,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "7b0115d7", + "metadata": {}, "outputs": [], "source": [ "# TODO 2: Implement summary metrics.\n", @@ -384,6 +394,7 @@ }, { "cell_type": "markdown", + "id": "e7d2eb8c", "metadata": {}, "source": [ "### Step 4 - Export evidence artifacts\n", @@ -393,8 +404,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "f441a31b", + "metadata": {}, "outputs": [], "source": [ "# Export metrics and figures\n", @@ -443,6 +455,7 @@ }, { "cell_type": "markdown", + "id": "a70e02fe", "metadata": {}, "source": [ "### Step 5 - Visual comparison for interpretation\n", @@ -452,8 +465,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "3ae72a84", + "metadata": {}, "outputs": [], "source": [ "# Visual side-by-side sample grid\n", @@ -476,6 +490,7 @@ }, { "cell_type": "markdown", + "id": "e1864c41", "metadata": {}, "source": [ "## Interpretation hints\n", @@ -486,6 +501,7 @@ }, { "cell_type": "markdown", + "id": "9fc8ab15", "metadata": {}, "source": [ "## Discussion bridge to workshop breakout\n", @@ -496,6 +512,7 @@ }, { "cell_type": "markdown", + "id": "e0bf0166", "metadata": {}, "source": [ "## Troubleshooting\n", @@ -506,6 +523,7 @@ }, { "cell_type": "markdown", + "id": "0905c130", "metadata": {}, "source": [ "## Takeaways\n", diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 67b2672..7226262 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "2c1598d9", "metadata": {}, "source": [ "# Notebook 03 (Participant): Add a New Problem Scaffold\n", @@ -11,6 +12,7 @@ }, { "cell_type": "markdown", + "id": "f05bea02", "metadata": {}, "source": [ "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" @@ -18,6 +20,7 @@ }, { "cell_type": "markdown", + "id": "a3ff5574", "metadata": {}, "source": [ "## Notebook map\n", @@ -32,6 +35,7 @@ }, { "cell_type": "markdown", + "id": "f451e420", "metadata": {}, "source": [ "## Standalone guide\n", @@ -41,6 +45,7 @@ }, { "cell_type": "markdown", + "id": "83b36f03", "metadata": {}, "source": [ "## What makes a new problem benchmark-ready\n", @@ -51,27 +56,27 @@ { "cell_type": "code", "execution_count": null, + "id": "06fe16ca", "metadata": {}, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", - "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium', 'pybullet']\n", + "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", + " !pip install engibench[beams2d] matplotlib gymnasium pybullet\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n" + " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" ] }, { "cell_type": "markdown", + "id": "064e668b", "metadata": {}, "source": [ "### Step 1 - Import scaffold dependencies\n", @@ -82,6 +87,7 @@ { "cell_type": "code", "execution_count": null, + "id": "39df1bf6", "metadata": {}, "outputs": [], "source": [ @@ -104,6 +110,7 @@ }, { "cell_type": "markdown", + "id": "d4a22b27", "metadata": {}, "source": [ "### Step 2 - Implement PyBullet manipulator co-design problem contract (TODO)\n", @@ -114,6 +121,7 @@ { "cell_type": "code", "execution_count": null, + "id": "3562f034", "metadata": {}, "outputs": [], "source": [ @@ -206,6 +214,7 @@ }, { "cell_type": "markdown", + "id": "363a7164", "metadata": {}, "source": [ "### Step 3 - Smoke-test your scaffold\n", @@ -222,6 +231,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ec683bc5", "metadata": {}, "outputs": [], "source": [ @@ -269,6 +279,7 @@ }, { "cell_type": "markdown", + "id": "5ab2097c", "metadata": {}, "source": [ "## Mapping to real EngiBench contributions\n", @@ -278,6 +289,7 @@ }, { "cell_type": "markdown", + "id": "e2e5bd63", "metadata": {}, "source": [ "## Contribution checklist\n", @@ -287,6 +299,7 @@ }, { "cell_type": "markdown", + "id": "ea79fd4f", "metadata": {}, "source": [ "## Troubleshooting\n", @@ -297,6 +310,7 @@ }, { "cell_type": "markdown", + "id": "7a014b05", "metadata": {}, "source": [ "## Takeaways\n", diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index 14428f8..b5ca1e3 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "eb3130a2", "metadata": {}, "source": [ "# Notebook 00: Setup + API Warmup (DCC26)\n", @@ -11,6 +12,7 @@ }, { "cell_type": "markdown", + "id": "5f88c0f0", "metadata": {}, "source": [ "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" @@ -18,6 +20,7 @@ }, { "cell_type": "markdown", + "id": "95c5209f", "metadata": {}, "source": [ "## Notebook map\n", @@ -32,6 +35,7 @@ }, { "cell_type": "markdown", + "id": "bbec7887", "metadata": {}, "source": [ "## Standalone guide\n", @@ -42,6 +46,7 @@ }, { "cell_type": "markdown", + "id": "f7c30d56", "metadata": {}, "source": [ "## Why this warmup matters\n", @@ -52,6 +57,7 @@ }, { "cell_type": "markdown", + "id": "952d991e", "metadata": {}, "source": [ "## Optional install cell (fresh Colab)\n", @@ -62,27 +68,27 @@ { "cell_type": "code", "execution_count": null, + "id": "946d0c0d", "metadata": {}, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", - "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", + "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", + " !pip install engibench[beams2d] matplotlib seaborn\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n" + " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" ] }, { "cell_type": "markdown", + "id": "002ea4f3", "metadata": {}, "source": [ "### Step 1 - Initialize reproducible session\n", @@ -93,12 +99,28 @@ { "cell_type": "code", "execution_count": null, + "id": "8d44722b", "metadata": {}, "outputs": [], - "source": "import random\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nimport engibench\nfrom engibench.problems.beams2d.v0 import Beams2D\n\nSEED = 7\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nprint('engibench version:', engibench.__version__)\nprint('seed:', SEED)" + "source": [ + "import random\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "import engibench\n", + "from engibench.problems.beams2d.v0 import Beams2D\n", + "\n", + "SEED = 7\n", + "random.seed(SEED)\n", + "np.random.seed(SEED)\n", + "\n", + "print('engibench version:', engibench.__version__)\n", + "print('seed:', SEED)" + ] }, { "cell_type": "markdown", + "id": "8049d01e", "metadata": {}, "source": [ "### Step 2 - Instantiate benchmark problem\n", @@ -109,12 +131,23 @@ { "cell_type": "code", "execution_count": null, + "id": "62611bc5", "metadata": {}, "outputs": [], - "source": "problem = Beams2D(seed=SEED)\n\nprint('Problem class:', type(problem).__name__)\nprint('Design space:', problem.design_space)\nprint('Objectives:', problem.objectives)\nprint('Conditions instance:', problem.conditions)\nprint('Condition keys:', problem.conditions_keys)\nprint('Dataset ID:', problem.dataset_id)" + "source": [ + "problem = Beams2D(seed=SEED)\n", + "\n", + "print('Problem class:', type(problem).__name__)\n", + "print('Design space:', problem.design_space)\n", + "print('Objectives:', problem.objectives)\n", + "print('Conditions instance:', problem.conditions)\n", + "print('Condition keys:', problem.conditions_keys)\n", + "print('Dataset ID:', problem.dataset_id)" + ] }, { "cell_type": "markdown", + "id": "7a0b4e5d", "metadata": {}, "source": [ "### Step 3 - Inspect dataset structure\n", @@ -125,12 +158,24 @@ { "cell_type": "code", "execution_count": null, + "id": "d990d6f3", "metadata": {}, "outputs": [], - "source": "dataset = problem.dataset\nprint(dataset)\n\nsample_idx = 0\ndesign = np.array(dataset['train']['optimal_design'][sample_idx])\nconfig = {k: dataset['train'][k][sample_idx] for k in problem.conditions_keys}\n\nprint('Sample design shape:', design.shape)\nprint('Sample config:', config)" + "source": [ + "dataset = problem.dataset\n", + "print(dataset)\n", + "\n", + "sample_idx = 0\n", + "design = np.array(dataset['train']['optimal_design'][sample_idx])\n", + "config = {k: dataset['train'][k][sample_idx] for k in problem.conditions_keys}\n", + "\n", + "print('Sample design shape:', design.shape)\n", + "print('Sample config:', config)" + ] }, { "cell_type": "markdown", + "id": "5dd9c588", "metadata": {}, "source": [ "### Step 4 - Visualize one benchmark design\n", @@ -141,12 +186,18 @@ { "cell_type": "code", "execution_count": null, + "id": "3935f407", "metadata": {}, "outputs": [], - "source": "fig, ax = problem.render(design)\nax.set_title('Sample Beams2D design from training split')\nplt.show()" + "source": [ + "fig, ax = problem.render(design)\n", + "ax.set_title('Sample Beams2D design from training split')\n", + "plt.show()" + ] }, { "cell_type": "markdown", + "id": "772416a2", "metadata": {}, "source": [ "### Step 5 - Test constraint semantics\n", @@ -157,12 +208,25 @@ { "cell_type": "code", "execution_count": null, + "id": "fe14de33", "metadata": {}, "outputs": [], - "source": "# One explicit constraint check with intentionally mismatched volume fraction\nbad_config = dict(config)\nbad_config['volfrac'] = 0.2\nviolations = problem.check_constraints(design=design, config=bad_config)\n\nprint('Violation count:', len(violations))\nif violations:\n print(violations)\nelse:\n print('No violations found')" + "source": [ + "# One explicit constraint check with intentionally mismatched volume fraction\n", + "bad_config = dict(config)\n", + "bad_config['volfrac'] = 0.2\n", + "violations = problem.check_constraints(design=design, config=bad_config)\n", + "\n", + "print('Violation count:', len(violations))\n", + "if violations:\n", + " print(violations)\n", + "else:\n", + " print('No violations found')" + ] }, { "cell_type": "markdown", + "id": "bec1e57f", "metadata": {}, "source": [ "## Troubleshooting\n", @@ -173,6 +237,7 @@ }, { "cell_type": "markdown", + "id": "a37bb417", "metadata": {}, "source": [ "## Next\n", @@ -182,6 +247,7 @@ }, { "cell_type": "markdown", + "id": "6fe3f1a4", "metadata": {}, "source": [ "## Reflection prompts\n", @@ -192,6 +258,7 @@ }, { "cell_type": "markdown", + "id": "547b3933", "metadata": {}, "source": [ "## Takeaways\n", diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 0ad9aa0..0f46ddc 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "5ec86a66", "metadata": {}, "source": [ "# Notebook 01: Train + Generate with EngiOpt CGAN-2D (DCC26)\n", @@ -11,6 +12,7 @@ }, { "cell_type": "markdown", + "id": "28e779fa", "metadata": {}, "source": [ "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" @@ -18,6 +20,7 @@ }, { "cell_type": "markdown", + "id": "ca81b1b9", "metadata": {}, "source": [ "## Notebook map\n", @@ -32,6 +35,7 @@ }, { "cell_type": "markdown", + "id": "52364318", "metadata": {}, "source": [ "## Standalone guide\n", @@ -42,31 +46,30 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "b6bc375a", + "metadata": {}, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", - "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "FORCE_INSTALL = False # Set True to force install outside Colab\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing base dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", - " print('Installing EngiOpt from GitHub branch...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', ENGIOPT_GIT])\n", + " print('Installing dependencies...')\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib pandas tqdm tyro wandb\n", + " !pip install {ENGIOPT_GIT}\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n" + " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" ] }, { "cell_type": "markdown", + "id": "1f65b99f", "metadata": {}, "source": [ "## Part A: Configuration and runtime controls\n", @@ -76,6 +79,7 @@ }, { "cell_type": "markdown", + "id": "f0e9f4a5", "metadata": {}, "source": [ "### EngiBench vs EngiOpt roles in this notebook\n", @@ -86,6 +90,7 @@ }, { "cell_type": "markdown", + "id": "7ede774c", "metadata": {}, "source": [ "### Step 1 - Configure reproducible environment\n", @@ -95,8 +100,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "3d947da0", + "metadata": {}, "outputs": [], "source": [ "import json\n", @@ -158,6 +164,7 @@ }, { "cell_type": "markdown", + "id": "acc18dbf", "metadata": {}, "source": [ "## Part B: Load Beams2D data and build a stable workshop subset\n", @@ -167,6 +174,7 @@ }, { "cell_type": "markdown", + "id": "281ef1bd", "metadata": {}, "source": [ "### Training diagnostics to monitor\n", @@ -177,6 +185,7 @@ }, { "cell_type": "markdown", + "id": "0ca03295", "metadata": {}, "source": [ "### Step 2 - Build training slice from EngiBench dataset\n", @@ -186,8 +195,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "684c4a1f", + "metadata": {}, "outputs": [], "source": [ "problem = Beams2D(seed=SEED)\n", @@ -213,6 +223,7 @@ }, { "cell_type": "markdown", + "id": "f4aec55d", "metadata": {}, "source": [ "### Step 3 - Define EngiOpt model and optimization objects\n", @@ -222,8 +233,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "9c5a8fdf", + "metadata": {}, "outputs": [], "source": [ "model = EngiOptCGAN2DGenerator(\n", @@ -242,6 +254,7 @@ }, { "cell_type": "markdown", + "id": "f796eff4", "metadata": {}, "source": [ "### Step 4 - Train (or load) with diagnostics\n", @@ -251,8 +264,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "9b09e4d3", + "metadata": {}, "outputs": [], "source": [ "TRAIN_FROM_SCRATCH = True\n", @@ -354,6 +368,7 @@ }, { "cell_type": "markdown", + "id": "9c46c7fb", "metadata": {}, "source": [ "## Part C: Condition-driven generation and quick sanity checks\n", @@ -363,6 +378,7 @@ }, { "cell_type": "markdown", + "id": "22cf9ccc", "metadata": {}, "source": [ "### Scientific checkpoint before Notebook 02\n", @@ -373,6 +389,7 @@ }, { "cell_type": "markdown", + "id": "91f5c0e3", "metadata": {}, "source": [ "### Step 5 - Generate conditioned designs\n", @@ -382,8 +399,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "615dbe0e", + "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(SEED)\n", @@ -419,6 +437,7 @@ }, { "cell_type": "markdown", + "id": "ccf7721d", "metadata": {}, "source": [ "### Step 6 - Export artifact contract\n", @@ -428,8 +447,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "dcb64597", + "metadata": {}, "outputs": [], "source": [ "# Artifact contract consumed by Notebook 02 and optional W&B logging.\n", @@ -477,6 +497,7 @@ }, { "cell_type": "markdown", + "id": "17f27f2d", "metadata": {}, "source": [ "### Step 7 - Quick visual QA\n", @@ -486,8 +507,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "3cc5a2bd", + "metadata": {}, "outputs": [], "source": [ "# Visual side-by-side snapshot (generated vs baseline)\n", @@ -507,6 +529,7 @@ }, { "cell_type": "markdown", + "id": "86f2eec2", "metadata": {}, "source": [ "## Troubleshooting\n", @@ -517,6 +540,7 @@ }, { "cell_type": "markdown", + "id": "3005644d", "metadata": {}, "source": [ "## Next\n", @@ -526,6 +550,7 @@ }, { "cell_type": "markdown", + "id": "33f6b236", "metadata": {}, "source": [ "## Takeaways\n", diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index a98e184..16b3f0e 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "a4862a7e", "metadata": {}, "source": [ "# Notebook 02: Evaluation + Metrics (DCC26)\n", @@ -11,6 +12,7 @@ }, { "cell_type": "markdown", + "id": "1daf74ad", "metadata": {}, "source": [ "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" @@ -18,6 +20,7 @@ }, { "cell_type": "markdown", + "id": "5becd891", "metadata": {}, "source": [ "## Notebook map\n", @@ -32,6 +35,7 @@ }, { "cell_type": "markdown", + "id": "fb72fd19", "metadata": {}, "source": [ "## Standalone guide\n", @@ -41,31 +45,30 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "f496a538", + "metadata": {}, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", - "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "FORCE_INSTALL = False # Set True to force install outside Colab\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing base dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", - " print('Installing EngiOpt from GitHub branch...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', ENGIOPT_GIT])\n", + " print('Installing dependencies...')\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib pandas tqdm tyro wandb\n", + " !pip install {ENGIOPT_GIT}\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n" + " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" ] }, { "cell_type": "markdown", + "id": "685cf46f", "metadata": {}, "source": [ "## Artifact loading strategy\n", @@ -76,6 +79,7 @@ }, { "cell_type": "markdown", + "id": "34f48177", "metadata": {}, "source": [ "### Why these metrics matter for benchmarking\n", @@ -86,6 +90,7 @@ }, { "cell_type": "markdown", + "id": "14103ff3", "metadata": {}, "source": [ "### Step 1 - Resolve artifact source and recovery path\n", @@ -95,8 +100,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "2f7f1979", + "metadata": {}, "outputs": [], "source": [ "import json\n", @@ -299,6 +305,7 @@ }, { "cell_type": "markdown", + "id": "8e6bf426", "metadata": {}, "source": [ "## Per-sample evaluation loop\n", @@ -308,6 +315,7 @@ }, { "cell_type": "markdown", + "id": "57092416", "metadata": {}, "source": [ "### Step 2 - Run per-sample physics evaluation\n", @@ -317,8 +325,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "439ed8ad", + "metadata": {}, "outputs": [], "source": [ "# Constraint and simulation evaluations are decoupled to diagnose failure modes clearly.\n", @@ -350,6 +359,7 @@ }, { "cell_type": "markdown", + "id": "b9bd54e5", "metadata": {}, "source": [ "### Step 3 - Compute benchmark metrics\n", @@ -359,8 +369,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "1aed4828", + "metadata": {}, "outputs": [], "source": [ "# Diversity proxy (intra-generated spread) and novelty proxy (distance to train set).\n", @@ -408,6 +419,7 @@ }, { "cell_type": "markdown", + "id": "2c900998", "metadata": {}, "source": [ "### Step 4 - Export evidence artifacts\n", @@ -417,8 +429,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "88ab86bc", + "metadata": {}, "outputs": [], "source": [ "results_path = ARTIFACT_DIR / 'per_sample_metrics.csv'\n", @@ -483,6 +496,7 @@ }, { "cell_type": "markdown", + "id": "786a08c7", "metadata": {}, "source": [ "### Step 5 - Visual comparison for interpretation\n", @@ -492,8 +506,9 @@ }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "417a4ca9", + "metadata": {}, "outputs": [], "source": [ "# Visual side-by-side sample grid\n", @@ -516,6 +531,7 @@ }, { "cell_type": "markdown", + "id": "3aca41c8", "metadata": {}, "source": [ "## Interpretation hints and discussion prompts\n", @@ -526,6 +542,7 @@ }, { "cell_type": "markdown", + "id": "649db8c8", "metadata": {}, "source": [ "## Discussion bridge to workshop breakout\n", @@ -535,6 +552,7 @@ }, { "cell_type": "markdown", + "id": "3d7e01e3", "metadata": {}, "source": [ "## Troubleshooting\n", @@ -545,6 +563,7 @@ }, { "cell_type": "markdown", + "id": "0144c8ad", "metadata": {}, "source": [ "## Takeaways\n", diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index bec934f..9cca816 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "7ab4a0f1", "metadata": {}, "source": [ "# Notebook 03: Add a New Problem Scaffold (DCC26)\n", @@ -11,6 +12,7 @@ }, { "cell_type": "markdown", + "id": "b3e4b78c", "metadata": {}, "source": [ "**Edit-safe start:** this notebook opens from GitHub in read-only source mode. Use **File -> Save a copy in Drive** before running edits so your changes stay in your own workspace.\n" @@ -18,6 +20,7 @@ }, { "cell_type": "markdown", + "id": "e5791875", "metadata": {}, "source": [ "## Notebook map\n", @@ -32,6 +35,7 @@ }, { "cell_type": "markdown", + "id": "cef985ef", "metadata": {}, "source": [ "## Standalone guide\n", @@ -41,6 +45,7 @@ }, { "cell_type": "markdown", + "id": "5e7ab8b8", "metadata": {}, "source": [ "## What makes a new problem benchmark-ready\n", @@ -51,27 +56,27 @@ { "cell_type": "code", "execution_count": null, + "id": "8d6c6ea1", "metadata": {}, "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", - "FORCE_INSTALL = False # Set True to force reinstall outside Colab\n", - "PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium', 'pybullet']\n", + "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " subprocess.check_call([sys.executable, '-m', 'pip', 'install', *PACKAGES])\n", + " !pip install engibench[beams2d] matplotlib gymnasium pybullet\n", " print('Dependency install complete.')\n", "else:\n", - " print('Skipping install (using current environment).')\n" + " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" ] }, { "cell_type": "markdown", + "id": "0bbd0ad4", "metadata": {}, "source": [ "### Step 1 - Import scaffold dependencies\n", @@ -82,6 +87,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f10f2643", "metadata": {}, "outputs": [], "source": [ @@ -104,6 +110,7 @@ }, { "cell_type": "markdown", + "id": "ea867bdb", "metadata": {}, "source": [ "### Step 2 - Implement PyBullet manipulator co-design problem contract\n", @@ -114,6 +121,7 @@ { "cell_type": "code", "execution_count": null, + "id": "402d904d", "metadata": {}, "outputs": [], "source": [ @@ -379,6 +387,7 @@ }, { "cell_type": "markdown", + "id": "2b9572ae", "metadata": {}, "source": [ "### Step 3 - Smoke-test the scaffold\n", @@ -395,6 +404,7 @@ { "cell_type": "code", "execution_count": null, + "id": "72a40876", "metadata": {}, "outputs": [], "source": [ @@ -441,6 +451,7 @@ }, { "cell_type": "markdown", + "id": "9ec93ba4", "metadata": {}, "source": [ "## Mapping to real EngiBench contributions\n", @@ -450,6 +461,7 @@ }, { "cell_type": "markdown", + "id": "2e15e82b", "metadata": {}, "source": [ "## Contribution checklist\n", @@ -459,6 +471,7 @@ }, { "cell_type": "markdown", + "id": "750f0558", "metadata": {}, "source": [ "## Troubleshooting\n", @@ -469,6 +482,7 @@ }, { "cell_type": "markdown", + "id": "afcfd2f2", "metadata": {}, "source": [ "## Takeaways\n", From 6cdad0fb31cb88b2229fb69fb1a827dbe965a208 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Tue, 10 Mar 2026 15:12:48 +0100 Subject: [PATCH 26/37] Add optional dataset and cVAE training extension to Notebook 03 --- .../03_add_new_problem_scaffold.ipynb | 386 ++++++++++++++++++ .../03_add_new_problem_scaffold.ipynb | 386 ++++++++++++++++++ 2 files changed, 772 insertions(+) diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 7226262..af0577c 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -69,6 +69,10 @@ "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", " !pip install engibench[beams2d] matplotlib gymnasium pybullet\n", + " try:\n", + " import torch # noqa: F401\n", + " except Exception:\n", + " !pip install torch torchvision\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" @@ -320,6 +324,388 @@ "2. What remains uncertain (and why)?\n", "3. What extra experiment would you run next to reduce that uncertainty?\n" ] + }, + { + "cell_type": "markdown", + "id": "216cd515", + "metadata": {}, + "source": [ + "## Optional extension - Build a dataset and train a generative model\n", + "\n", + "This section shows the full **offline benchmark loop** on the new problem:\n", + "\n", + "1. Sample feasible designs and conditions with the simulator in-the-loop.\n", + "2. Build a compact training dataset.\n", + "3. Train a small conditional VAE (cVAE) to generate designs from conditions.\n", + "4. Evaluate generated designs against a random-design baseline.\n", + "\n", + "Why this matters:\n", + "- It demonstrates that your scaffold is not just an API shell; it can support full generative benchmarking.\n", + "- It creates a reproducible path from simulator to learned design distribution.\n", + "\n", + "Runtime note:\n", + "- Keep this optional (`RUN_OPTIONAL_SECTION=False`) during live sessions unless you have extra time.\n", + "- With default settings it should finish in a few minutes on Colab CPU/GPU.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1e89486", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional extension controls (safe defaults)\n", + "from pathlib import Path\n", + "import sys\n", + "import torch as th\n", + "import torch.nn as nn\n", + "from torch.utils.data import DataLoader, TensorDataset\n", + "\n", + "RUN_OPTIONAL_SECTION = False # Set True to run this optional extension\n", + "N_FEASIBLE_SAMPLES = 240\n", + "TOP_FRACTION = 0.35\n", + "EPOCHS = 18\n", + "BATCH_SIZE = 64\n", + "LATENT_DIM = 4\n", + "BETA_KL = 1e-3\n", + "FAST_SIM_CFG = {'sim_steps': 80, 'dt': 1.0 / 120.0}\n", + "EVAL_SAMPLES = 40\n", + "\n", + "if 'problem' not in globals():\n", + " problem = PlanarManipulatorCoDesignProblem(seed=7)\n", + "\n", + "if 'google.colab' in sys.modules:\n", + " OPTIONAL_ARTIFACT_DIR = Path('/content/dcc26_optional_artifacts')\n", + "else:\n", + " OPTIONAL_ARTIFACT_DIR = Path('workshops/dcc26/optional_artifacts')\n", + "OPTIONAL_ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", + "\n", + "print(f'Optional artifacts dir: {OPTIONAL_ARTIFACT_DIR.resolve()}')\n", + "print('Optional section enabled:' if RUN_OPTIONAL_SECTION else 'Optional section disabled:', RUN_OPTIONAL_SECTION)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "527ee9bc", + "metadata": {}, + "outputs": [], + "source": [ + "# Build offline dataset from simulator rollouts\n", + "import numpy as np\n", + "\n", + "rng = np.random.default_rng(123)\n", + "\n", + "\n", + "def sample_condition_dict() -> dict:\n", + " return {\n", + " 'target_x': float(rng.uniform(0.20, 1.35)),\n", + " 'target_y': float(rng.uniform(0.05, 1.20)),\n", + " 'payload_kg': float(rng.uniform(0.0, 2.0)),\n", + " 'disturbance_scale': float(rng.uniform(0.0, 0.30)),\n", + " }\n", + "\n", + "\n", + "def cond_to_vec(cfg: dict) -> np.ndarray:\n", + " return np.array([\n", + " cfg['target_x'],\n", + " cfg['target_y'],\n", + " cfg['payload_kg'],\n", + " cfg['disturbance_scale'],\n", + " ], dtype=np.float32)\n", + "\n", + "\n", + "def objective_score(obj: np.ndarray) -> float:\n", + " # Same scalarization as quick optimization above: error + 0.02 * energy\n", + " return float(obj[0] + 0.02 * obj[1])\n", + "\n", + "\n", + "def make_dataset(problem_obj, n_feasible: int):\n", + " designs, conds, objs = [], [], []\n", + " max_attempts = n_feasible * 6\n", + " attempts = 0\n", + "\n", + " while len(designs) < n_feasible and attempts < max_attempts:\n", + " attempts += 1\n", + " d, _ = problem_obj.random_design()\n", + " cfg = sample_condition_dict()\n", + "\n", + " violations = problem_obj.check_constraints(d, cfg)\n", + " if len(violations) > 0:\n", + " continue\n", + "\n", + " obj = problem_obj.simulate(d, {**cfg, **FAST_SIM_CFG})\n", + " designs.append(d.astype(np.float32))\n", + " conds.append(cond_to_vec(cfg))\n", + " objs.append(obj.astype(np.float32))\n", + "\n", + " if len(designs) % 40 == 0:\n", + " print(f'Collected feasible samples: {len(designs)}/{n_feasible}')\n", + "\n", + " if len(designs) < max(32, n_feasible // 3):\n", + " raise RuntimeError(\n", + " f'Not enough feasible samples ({len(designs)}). Increase attempts or relax settings.'\n", + " )\n", + "\n", + " designs = np.stack(designs)\n", + " conds = np.stack(conds)\n", + " objs = np.stack(objs)\n", + " scores = np.array([objective_score(o) for o in objs], dtype=np.float32)\n", + "\n", + " keep_n = max(32, int(TOP_FRACTION * len(scores)))\n", + " top_idx = np.argsort(scores)[:keep_n]\n", + "\n", + " data = {\n", + " 'designs_all': designs,\n", + " 'conditions_all': conds,\n", + " 'objectives_all': objs,\n", + " 'scores_all': scores,\n", + " 'designs_top': designs[top_idx],\n", + " 'conditions_top': conds[top_idx],\n", + " 'objectives_top': objs[top_idx],\n", + " 'scores_top': scores[top_idx],\n", + " }\n", + " return data\n", + "\n", + "\n", + "if RUN_OPTIONAL_SECTION:\n", + " dataset = make_dataset(problem, N_FEASIBLE_SAMPLES)\n", + " np.savez(OPTIONAL_ARTIFACT_DIR / 'manipulator_dataset.npz', **dataset)\n", + " print('Saved dataset:', OPTIONAL_ARTIFACT_DIR / 'manipulator_dataset.npz')\n", + " print('All samples:', dataset['designs_all'].shape[0], '| Top samples:', dataset['designs_top'].shape[0])\n", + "else:\n", + " dataset = None\n", + " print('Skipped dataset creation. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + ] + }, + { + "cell_type": "markdown", + "id": "1f952fc5", + "metadata": {}, + "source": [ + "### Optional model - Conditional VAE for inverse design\n", + "\n", + "Modeling setup:\n", + "- **Condition input:** `(target_x, target_y, payload_kg, disturbance_scale)`\n", + "- **Generated output:** 6D design vector in the problem design space.\n", + "- **Training target:** top-performing feasible samples from the offline dataset.\n", + "\n", + "This gives a minimal but valid generative baseline for `p(design | condition)`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79320050", + "metadata": {}, + "outputs": [], + "source": [ + "# Define and train a small conditional VAE\n", + "class ConditionalVAE(nn.Module):\n", + " def __init__(self, cond_dim: int, design_dim: int, latent_dim: int = 4, hidden: int = 96):\n", + " super().__init__()\n", + " self.enc = nn.Sequential(\n", + " nn.Linear(cond_dim + design_dim, hidden),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden, hidden),\n", + " nn.ReLU(),\n", + " )\n", + " self.mu = nn.Linear(hidden, latent_dim)\n", + " self.logvar = nn.Linear(hidden, latent_dim)\n", + "\n", + " self.dec = nn.Sequential(\n", + " nn.Linear(cond_dim + latent_dim, hidden),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden, hidden),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden, design_dim),\n", + " nn.Sigmoid(),\n", + " )\n", + "\n", + " def encode(self, cond, design):\n", + " h = self.enc(th.cat([cond, design], dim=-1))\n", + " return self.mu(h), self.logvar(h)\n", + "\n", + " def reparameterize(self, mu, logvar):\n", + " std = th.exp(0.5 * logvar)\n", + " eps = th.randn_like(std)\n", + " return mu + eps * std\n", + "\n", + " def decode(self, cond, z):\n", + " return self.dec(th.cat([cond, z], dim=-1))\n", + "\n", + " def forward(self, cond, design):\n", + " mu, logvar = self.encode(cond, design)\n", + " z = self.reparameterize(mu, logvar)\n", + " recon = self.decode(cond, z)\n", + " return recon, mu, logvar\n", + "\n", + "\n", + "if RUN_OPTIONAL_SECTION:\n", + " lb = problem.design_space.low.astype(np.float32)\n", + " ub = problem.design_space.high.astype(np.float32)\n", + "\n", + " x_cond = dataset['conditions_top'].astype(np.float32)\n", + " y_design = dataset['designs_top'].astype(np.float32)\n", + " y_norm = np.clip((y_design - lb) / (ub - lb + 1e-8), 0.0, 1.0)\n", + "\n", + " cond_t = th.tensor(x_cond, dtype=th.float32)\n", + " design_t = th.tensor(y_norm, dtype=th.float32)\n", + " loader = DataLoader(TensorDataset(cond_t, design_t), batch_size=BATCH_SIZE, shuffle=True)\n", + "\n", + " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", + " model = ConditionalVAE(cond_dim=4, design_dim=6, latent_dim=LATENT_DIM).to(device)\n", + " opt = th.optim.Adam(model.parameters(), lr=2e-3)\n", + "\n", + " history = []\n", + " for epoch in range(1, EPOCHS + 1):\n", + " model.train()\n", + " epoch_loss = 0.0\n", + " for c_batch, d_batch in loader:\n", + " c_batch = c_batch.to(device)\n", + " d_batch = d_batch.to(device)\n", + "\n", + " recon, mu, logvar = model(c_batch, d_batch)\n", + " recon_loss = th.mean((recon - d_batch) ** 2)\n", + " kl = -0.5 * th.mean(1 + logvar - mu.pow(2) - logvar.exp())\n", + " loss = recon_loss + BETA_KL * kl\n", + "\n", + " opt.zero_grad()\n", + " loss.backward()\n", + " opt.step()\n", + " epoch_loss += float(loss.item())\n", + "\n", + " epoch_loss /= max(1, len(loader))\n", + " history.append(epoch_loss)\n", + " if epoch == 1 or epoch % 5 == 0 or epoch == EPOCHS:\n", + " print(f'Epoch {epoch:02d}/{EPOCHS} | loss={epoch_loss:.6f}')\n", + "\n", + " th.save(model.state_dict(), OPTIONAL_ARTIFACT_DIR / 'cvae_weights.pt')\n", + " print('Saved model:', OPTIONAL_ARTIFACT_DIR / 'cvae_weights.pt')\n", + "else:\n", + " model, history, device = None, [], th.device('cpu')\n", + " print('Skipped model training. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "723f3f4e", + "metadata": {}, + "outputs": [], + "source": [ + "# Evaluate generated designs vs random baseline\n", + "import matplotlib.pyplot as plt\n", + "\n", + "\n", + "def sample_baseline(problem_obj, cfg: dict, trials: int = 8):\n", + " best_obj = None\n", + " for _ in range(trials):\n", + " d, _ = problem_obj.random_design()\n", + " if len(problem_obj.check_constraints(d, cfg)) > 0:\n", + " continue\n", + " obj = problem_obj.simulate(d, {**cfg, **FAST_SIM_CFG})\n", + " if best_obj is None or objective_score(obj) < objective_score(best_obj):\n", + " best_obj = obj\n", + " if best_obj is None:\n", + " # fallback if all random candidates violate constraints\n", + " d, _ = problem_obj.random_design()\n", + " best_obj = problem_obj.simulate(d, {**cfg, **FAST_SIM_CFG})\n", + " return best_obj\n", + "\n", + "\n", + "def generate_design(model_obj, cfg_vec: np.ndarray, lb: np.ndarray, ub: np.ndarray):\n", + " with th.no_grad():\n", + " c = th.tensor(cfg_vec[None, :], dtype=th.float32, device=device)\n", + " z = th.randn((1, LATENT_DIM), dtype=th.float32, device=device)\n", + " d_norm = model_obj.decode(c, z).cpu().numpy()[0]\n", + " d = lb + np.clip(d_norm, 0.0, 1.0) * (ub - lb)\n", + " return d.astype(np.float32)\n", + "\n", + "\n", + "if RUN_OPTIONAL_SECTION:\n", + " lb = problem.design_space.low.astype(np.float32)\n", + " ub = problem.design_space.high.astype(np.float32)\n", + "\n", + " gen_objs = []\n", + " base_objs = []\n", + " feasible_count = 0\n", + "\n", + " for _ in range(EVAL_SAMPLES):\n", + " cfg = sample_condition_dict()\n", + " cfg_vec = cond_to_vec(cfg)\n", + "\n", + " # try a few generated candidates to satisfy constraints\n", + " gen_obj = None\n", + " for _retry in range(5):\n", + " d_gen = generate_design(model, cfg_vec, lb, ub)\n", + " if len(problem.check_constraints(d_gen, cfg)) == 0:\n", + " gen_obj = problem.simulate(d_gen, {**cfg, **FAST_SIM_CFG})\n", + " feasible_count += 1\n", + " break\n", + " if gen_obj is None:\n", + " d_fallback, _ = problem.random_design()\n", + " gen_obj = problem.simulate(d_fallback, {**cfg, **FAST_SIM_CFG})\n", + "\n", + " base_obj = sample_baseline(problem, cfg, trials=8)\n", + " gen_objs.append(gen_obj)\n", + " base_objs.append(base_obj)\n", + "\n", + " gen_objs = np.stack(gen_objs)\n", + " base_objs = np.stack(base_objs)\n", + "\n", + " summary = {\n", + " 'generated_error_mean': float(np.mean(gen_objs[:, 0])),\n", + " 'generated_energy_mean': float(np.mean(gen_objs[:, 1])),\n", + " 'baseline_error_mean': float(np.mean(base_objs[:, 0])),\n", + " 'baseline_energy_mean': float(np.mean(base_objs[:, 1])),\n", + " 'generated_feasible_rate': float(feasible_count / EVAL_SAMPLES),\n", + " }\n", + " print('Optional extension summary:')\n", + " for k, v in summary.items():\n", + " print(f' {k}: {v:.6f}')\n", + "\n", + " np.savez(OPTIONAL_ARTIFACT_DIR / 'optional_eval_summary.npz', gen_objs=gen_objs, base_objs=base_objs, **summary)\n", + "\n", + " fig, axes = plt.subplots(1, 3, figsize=(14, 4))\n", + " axes[0].plot(history)\n", + " axes[0].set_title('cVAE training loss')\n", + " axes[0].set_xlabel('epoch')\n", + " axes[0].set_ylabel('loss')\n", + " axes[0].grid(alpha=0.3)\n", + "\n", + " axes[1].hist(base_objs[:, 0], bins=12, alpha=0.6, label='baseline')\n", + " axes[1].hist(gen_objs[:, 0], bins=12, alpha=0.6, label='generated')\n", + " axes[1].set_title('Final tracking error')\n", + " axes[1].set_xlabel('error [m]')\n", + " axes[1].legend()\n", + "\n", + " axes[2].hist(base_objs[:, 1], bins=12, alpha=0.6, label='baseline')\n", + " axes[2].hist(gen_objs[:, 1], bins=12, alpha=0.6, label='generated')\n", + " axes[2].set_title('Actuation energy')\n", + " axes[2].set_xlabel('energy [J]')\n", + " axes[2].legend()\n", + "\n", + " fig.tight_layout()\n", + " plt.show()\n", + "else:\n", + " print('Skipped evaluation. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + ] + }, + { + "cell_type": "markdown", + "id": "f479ce8a", + "metadata": {}, + "source": [ + "### Discussion prompts for workshop synthesis\n", + "\n", + "Use this optional experiment to trigger discussion:\n", + "\n", + "1. Is simulator-generated offline data enough for credible inverse-design benchmarking?\n", + "2. Which metrics should become mandatory in cross-domain comparisons (quality, feasibility, diversity, cost)?\n", + "3. How would you package this scaffold into a reusable EngiBench-style contribution (dataset card, baselines, splits, eval protocol)?\n" + ] } ], "metadata": { diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 9cca816..4dc5d8d 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -69,6 +69,10 @@ "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", " !pip install engibench[beams2d] matplotlib gymnasium pybullet\n", + " try:\n", + " import torch # noqa: F401\n", + " except Exception:\n", + " !pip install torch torchvision\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" @@ -492,6 +496,388 @@ "2. What remains uncertain (and why)?\n", "3. What extra experiment would you run next to reduce that uncertainty?\n" ] + }, + { + "cell_type": "markdown", + "id": "41539f9b", + "metadata": {}, + "source": [ + "## Optional extension - Build a dataset and train a generative model\n", + "\n", + "This section shows the full **offline benchmark loop** on the new problem:\n", + "\n", + "1. Sample feasible designs and conditions with the simulator in-the-loop.\n", + "2. Build a compact training dataset.\n", + "3. Train a small conditional VAE (cVAE) to generate designs from conditions.\n", + "4. Evaluate generated designs against a random-design baseline.\n", + "\n", + "Why this matters:\n", + "- It demonstrates that your scaffold is not just an API shell; it can support full generative benchmarking.\n", + "- It creates a reproducible path from simulator to learned design distribution.\n", + "\n", + "Runtime note:\n", + "- Keep this optional (`RUN_OPTIONAL_SECTION=False`) during live sessions unless you have extra time.\n", + "- With default settings it should finish in a few minutes on Colab CPU/GPU.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97dc85be", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional extension controls (safe defaults)\n", + "from pathlib import Path\n", + "import sys\n", + "import torch as th\n", + "import torch.nn as nn\n", + "from torch.utils.data import DataLoader, TensorDataset\n", + "\n", + "RUN_OPTIONAL_SECTION = False # Set True to run this optional extension\n", + "N_FEASIBLE_SAMPLES = 240\n", + "TOP_FRACTION = 0.35\n", + "EPOCHS = 18\n", + "BATCH_SIZE = 64\n", + "LATENT_DIM = 4\n", + "BETA_KL = 1e-3\n", + "FAST_SIM_CFG = {'sim_steps': 80, 'dt': 1.0 / 120.0}\n", + "EVAL_SAMPLES = 40\n", + "\n", + "if 'problem' not in globals():\n", + " problem = PlanarManipulatorCoDesignProblem(seed=7)\n", + "\n", + "if 'google.colab' in sys.modules:\n", + " OPTIONAL_ARTIFACT_DIR = Path('/content/dcc26_optional_artifacts')\n", + "else:\n", + " OPTIONAL_ARTIFACT_DIR = Path('workshops/dcc26/optional_artifacts')\n", + "OPTIONAL_ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", + "\n", + "print(f'Optional artifacts dir: {OPTIONAL_ARTIFACT_DIR.resolve()}')\n", + "print('Optional section enabled:' if RUN_OPTIONAL_SECTION else 'Optional section disabled:', RUN_OPTIONAL_SECTION)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f29c9d4", + "metadata": {}, + "outputs": [], + "source": [ + "# Build offline dataset from simulator rollouts\n", + "import numpy as np\n", + "\n", + "rng = np.random.default_rng(123)\n", + "\n", + "\n", + "def sample_condition_dict() -> dict:\n", + " return {\n", + " 'target_x': float(rng.uniform(0.20, 1.35)),\n", + " 'target_y': float(rng.uniform(0.05, 1.20)),\n", + " 'payload_kg': float(rng.uniform(0.0, 2.0)),\n", + " 'disturbance_scale': float(rng.uniform(0.0, 0.30)),\n", + " }\n", + "\n", + "\n", + "def cond_to_vec(cfg: dict) -> np.ndarray:\n", + " return np.array([\n", + " cfg['target_x'],\n", + " cfg['target_y'],\n", + " cfg['payload_kg'],\n", + " cfg['disturbance_scale'],\n", + " ], dtype=np.float32)\n", + "\n", + "\n", + "def objective_score(obj: np.ndarray) -> float:\n", + " # Same scalarization as quick optimization above: error + 0.02 * energy\n", + " return float(obj[0] + 0.02 * obj[1])\n", + "\n", + "\n", + "def make_dataset(problem_obj, n_feasible: int):\n", + " designs, conds, objs = [], [], []\n", + " max_attempts = n_feasible * 6\n", + " attempts = 0\n", + "\n", + " while len(designs) < n_feasible and attempts < max_attempts:\n", + " attempts += 1\n", + " d, _ = problem_obj.random_design()\n", + " cfg = sample_condition_dict()\n", + "\n", + " violations = problem_obj.check_constraints(d, cfg)\n", + " if len(violations) > 0:\n", + " continue\n", + "\n", + " obj = problem_obj.simulate(d, {**cfg, **FAST_SIM_CFG})\n", + " designs.append(d.astype(np.float32))\n", + " conds.append(cond_to_vec(cfg))\n", + " objs.append(obj.astype(np.float32))\n", + "\n", + " if len(designs) % 40 == 0:\n", + " print(f'Collected feasible samples: {len(designs)}/{n_feasible}')\n", + "\n", + " if len(designs) < max(32, n_feasible // 3):\n", + " raise RuntimeError(\n", + " f'Not enough feasible samples ({len(designs)}). Increase attempts or relax settings.'\n", + " )\n", + "\n", + " designs = np.stack(designs)\n", + " conds = np.stack(conds)\n", + " objs = np.stack(objs)\n", + " scores = np.array([objective_score(o) for o in objs], dtype=np.float32)\n", + "\n", + " keep_n = max(32, int(TOP_FRACTION * len(scores)))\n", + " top_idx = np.argsort(scores)[:keep_n]\n", + "\n", + " data = {\n", + " 'designs_all': designs,\n", + " 'conditions_all': conds,\n", + " 'objectives_all': objs,\n", + " 'scores_all': scores,\n", + " 'designs_top': designs[top_idx],\n", + " 'conditions_top': conds[top_idx],\n", + " 'objectives_top': objs[top_idx],\n", + " 'scores_top': scores[top_idx],\n", + " }\n", + " return data\n", + "\n", + "\n", + "if RUN_OPTIONAL_SECTION:\n", + " dataset = make_dataset(problem, N_FEASIBLE_SAMPLES)\n", + " np.savez(OPTIONAL_ARTIFACT_DIR / 'manipulator_dataset.npz', **dataset)\n", + " print('Saved dataset:', OPTIONAL_ARTIFACT_DIR / 'manipulator_dataset.npz')\n", + " print('All samples:', dataset['designs_all'].shape[0], '| Top samples:', dataset['designs_top'].shape[0])\n", + "else:\n", + " dataset = None\n", + " print('Skipped dataset creation. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + ] + }, + { + "cell_type": "markdown", + "id": "c513ee3c", + "metadata": {}, + "source": [ + "### Optional model - Conditional VAE for inverse design\n", + "\n", + "Modeling setup:\n", + "- **Condition input:** `(target_x, target_y, payload_kg, disturbance_scale)`\n", + "- **Generated output:** 6D design vector in the problem design space.\n", + "- **Training target:** top-performing feasible samples from the offline dataset.\n", + "\n", + "This gives a minimal but valid generative baseline for `p(design | condition)`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc837146", + "metadata": {}, + "outputs": [], + "source": [ + "# Define and train a small conditional VAE\n", + "class ConditionalVAE(nn.Module):\n", + " def __init__(self, cond_dim: int, design_dim: int, latent_dim: int = 4, hidden: int = 96):\n", + " super().__init__()\n", + " self.enc = nn.Sequential(\n", + " nn.Linear(cond_dim + design_dim, hidden),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden, hidden),\n", + " nn.ReLU(),\n", + " )\n", + " self.mu = nn.Linear(hidden, latent_dim)\n", + " self.logvar = nn.Linear(hidden, latent_dim)\n", + "\n", + " self.dec = nn.Sequential(\n", + " nn.Linear(cond_dim + latent_dim, hidden),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden, hidden),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden, design_dim),\n", + " nn.Sigmoid(),\n", + " )\n", + "\n", + " def encode(self, cond, design):\n", + " h = self.enc(th.cat([cond, design], dim=-1))\n", + " return self.mu(h), self.logvar(h)\n", + "\n", + " def reparameterize(self, mu, logvar):\n", + " std = th.exp(0.5 * logvar)\n", + " eps = th.randn_like(std)\n", + " return mu + eps * std\n", + "\n", + " def decode(self, cond, z):\n", + " return self.dec(th.cat([cond, z], dim=-1))\n", + "\n", + " def forward(self, cond, design):\n", + " mu, logvar = self.encode(cond, design)\n", + " z = self.reparameterize(mu, logvar)\n", + " recon = self.decode(cond, z)\n", + " return recon, mu, logvar\n", + "\n", + "\n", + "if RUN_OPTIONAL_SECTION:\n", + " lb = problem.design_space.low.astype(np.float32)\n", + " ub = problem.design_space.high.astype(np.float32)\n", + "\n", + " x_cond = dataset['conditions_top'].astype(np.float32)\n", + " y_design = dataset['designs_top'].astype(np.float32)\n", + " y_norm = np.clip((y_design - lb) / (ub - lb + 1e-8), 0.0, 1.0)\n", + "\n", + " cond_t = th.tensor(x_cond, dtype=th.float32)\n", + " design_t = th.tensor(y_norm, dtype=th.float32)\n", + " loader = DataLoader(TensorDataset(cond_t, design_t), batch_size=BATCH_SIZE, shuffle=True)\n", + "\n", + " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", + " model = ConditionalVAE(cond_dim=4, design_dim=6, latent_dim=LATENT_DIM).to(device)\n", + " opt = th.optim.Adam(model.parameters(), lr=2e-3)\n", + "\n", + " history = []\n", + " for epoch in range(1, EPOCHS + 1):\n", + " model.train()\n", + " epoch_loss = 0.0\n", + " for c_batch, d_batch in loader:\n", + " c_batch = c_batch.to(device)\n", + " d_batch = d_batch.to(device)\n", + "\n", + " recon, mu, logvar = model(c_batch, d_batch)\n", + " recon_loss = th.mean((recon - d_batch) ** 2)\n", + " kl = -0.5 * th.mean(1 + logvar - mu.pow(2) - logvar.exp())\n", + " loss = recon_loss + BETA_KL * kl\n", + "\n", + " opt.zero_grad()\n", + " loss.backward()\n", + " opt.step()\n", + " epoch_loss += float(loss.item())\n", + "\n", + " epoch_loss /= max(1, len(loader))\n", + " history.append(epoch_loss)\n", + " if epoch == 1 or epoch % 5 == 0 or epoch == EPOCHS:\n", + " print(f'Epoch {epoch:02d}/{EPOCHS} | loss={epoch_loss:.6f}')\n", + "\n", + " th.save(model.state_dict(), OPTIONAL_ARTIFACT_DIR / 'cvae_weights.pt')\n", + " print('Saved model:', OPTIONAL_ARTIFACT_DIR / 'cvae_weights.pt')\n", + "else:\n", + " model, history, device = None, [], th.device('cpu')\n", + " print('Skipped model training. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25af8209", + "metadata": {}, + "outputs": [], + "source": [ + "# Evaluate generated designs vs random baseline\n", + "import matplotlib.pyplot as plt\n", + "\n", + "\n", + "def sample_baseline(problem_obj, cfg: dict, trials: int = 8):\n", + " best_obj = None\n", + " for _ in range(trials):\n", + " d, _ = problem_obj.random_design()\n", + " if len(problem_obj.check_constraints(d, cfg)) > 0:\n", + " continue\n", + " obj = problem_obj.simulate(d, {**cfg, **FAST_SIM_CFG})\n", + " if best_obj is None or objective_score(obj) < objective_score(best_obj):\n", + " best_obj = obj\n", + " if best_obj is None:\n", + " # fallback if all random candidates violate constraints\n", + " d, _ = problem_obj.random_design()\n", + " best_obj = problem_obj.simulate(d, {**cfg, **FAST_SIM_CFG})\n", + " return best_obj\n", + "\n", + "\n", + "def generate_design(model_obj, cfg_vec: np.ndarray, lb: np.ndarray, ub: np.ndarray):\n", + " with th.no_grad():\n", + " c = th.tensor(cfg_vec[None, :], dtype=th.float32, device=device)\n", + " z = th.randn((1, LATENT_DIM), dtype=th.float32, device=device)\n", + " d_norm = model_obj.decode(c, z).cpu().numpy()[0]\n", + " d = lb + np.clip(d_norm, 0.0, 1.0) * (ub - lb)\n", + " return d.astype(np.float32)\n", + "\n", + "\n", + "if RUN_OPTIONAL_SECTION:\n", + " lb = problem.design_space.low.astype(np.float32)\n", + " ub = problem.design_space.high.astype(np.float32)\n", + "\n", + " gen_objs = []\n", + " base_objs = []\n", + " feasible_count = 0\n", + "\n", + " for _ in range(EVAL_SAMPLES):\n", + " cfg = sample_condition_dict()\n", + " cfg_vec = cond_to_vec(cfg)\n", + "\n", + " # try a few generated candidates to satisfy constraints\n", + " gen_obj = None\n", + " for _retry in range(5):\n", + " d_gen = generate_design(model, cfg_vec, lb, ub)\n", + " if len(problem.check_constraints(d_gen, cfg)) == 0:\n", + " gen_obj = problem.simulate(d_gen, {**cfg, **FAST_SIM_CFG})\n", + " feasible_count += 1\n", + " break\n", + " if gen_obj is None:\n", + " d_fallback, _ = problem.random_design()\n", + " gen_obj = problem.simulate(d_fallback, {**cfg, **FAST_SIM_CFG})\n", + "\n", + " base_obj = sample_baseline(problem, cfg, trials=8)\n", + " gen_objs.append(gen_obj)\n", + " base_objs.append(base_obj)\n", + "\n", + " gen_objs = np.stack(gen_objs)\n", + " base_objs = np.stack(base_objs)\n", + "\n", + " summary = {\n", + " 'generated_error_mean': float(np.mean(gen_objs[:, 0])),\n", + " 'generated_energy_mean': float(np.mean(gen_objs[:, 1])),\n", + " 'baseline_error_mean': float(np.mean(base_objs[:, 0])),\n", + " 'baseline_energy_mean': float(np.mean(base_objs[:, 1])),\n", + " 'generated_feasible_rate': float(feasible_count / EVAL_SAMPLES),\n", + " }\n", + " print('Optional extension summary:')\n", + " for k, v in summary.items():\n", + " print(f' {k}: {v:.6f}')\n", + "\n", + " np.savez(OPTIONAL_ARTIFACT_DIR / 'optional_eval_summary.npz', gen_objs=gen_objs, base_objs=base_objs, **summary)\n", + "\n", + " fig, axes = plt.subplots(1, 3, figsize=(14, 4))\n", + " axes[0].plot(history)\n", + " axes[0].set_title('cVAE training loss')\n", + " axes[0].set_xlabel('epoch')\n", + " axes[0].set_ylabel('loss')\n", + " axes[0].grid(alpha=0.3)\n", + "\n", + " axes[1].hist(base_objs[:, 0], bins=12, alpha=0.6, label='baseline')\n", + " axes[1].hist(gen_objs[:, 0], bins=12, alpha=0.6, label='generated')\n", + " axes[1].set_title('Final tracking error')\n", + " axes[1].set_xlabel('error [m]')\n", + " axes[1].legend()\n", + "\n", + " axes[2].hist(base_objs[:, 1], bins=12, alpha=0.6, label='baseline')\n", + " axes[2].hist(gen_objs[:, 1], bins=12, alpha=0.6, label='generated')\n", + " axes[2].set_title('Actuation energy')\n", + " axes[2].set_xlabel('energy [J]')\n", + " axes[2].legend()\n", + "\n", + " fig.tight_layout()\n", + " plt.show()\n", + "else:\n", + " print('Skipped evaluation. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + ] + }, + { + "cell_type": "markdown", + "id": "efd692b9", + "metadata": {}, + "source": [ + "### Discussion prompts for workshop synthesis\n", + "\n", + "Use this optional experiment to trigger discussion:\n", + "\n", + "1. Is simulator-generated offline data enough for credible inverse-design benchmarking?\n", + "2. Which metrics should become mandatory in cross-domain comparisons (quality, feasibility, diversity, cost)?\n", + "3. How would you package this scaffold into a reusable EngiBench-style contribution (dataset card, baselines, splits, eval protocol)?\n" + ] } ], "metadata": { From 8575610c1b65b4232f11b14316c1b87f315a9642 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Tue, 10 Mar 2026 15:30:54 +0100 Subject: [PATCH 27/37] Switch Notebook 03 optional model to EngiOpt cgan_1d --- .../03_add_new_problem_scaffold.ipynb | 272 +++++++++--------- .../03_add_new_problem_scaffold.ipynb | 272 +++++++++--------- 2 files changed, 284 insertions(+), 260 deletions(-) diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index af0577c..a793b85 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -65,10 +65,12 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", + "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", " !pip install engibench[beams2d] matplotlib gymnasium pybullet\n", + " !pip install {ENGIOPT_GIT}\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", @@ -327,48 +329,45 @@ }, { "cell_type": "markdown", - "id": "216cd515", + "id": "ee70c214", "metadata": {}, "source": [ - "## Optional extension - Build a dataset and train a generative model\n", + "## Optional extension - Build dataset and train an EngiOpt generative model\n", "\n", - "This section shows the full **offline benchmark loop** on the new problem:\n", + "This extension runs a full offline loop using an **existing EngiOpt model** (`cgan_1d`) on top of your custom simulator problem:\n", "\n", - "1. Sample feasible designs and conditions with the simulator in-the-loop.\n", - "2. Build a compact training dataset.\n", - "3. Train a small conditional VAE (cVAE) to generate designs from conditions.\n", - "4. Evaluate generated designs against a random-design baseline.\n", + "1. Generate a feasible dataset from simulator rollouts.\n", + "2. Keep a top-performing subset.\n", + "3. Train EngiOpt `cgan_1d` (`Generator` + `Discriminator`) for conditional generation.\n", + "4. Compare generated designs vs a random-design baseline.\n", "\n", - "Why this matters:\n", - "- It demonstrates that your scaffold is not just an API shell; it can support full generative benchmarking.\n", - "- It creates a reproducible path from simulator to learned design distribution.\n", - "\n", - "Runtime note:\n", - "- Keep this optional (`RUN_OPTIONAL_SECTION=False`) during live sessions unless you have extra time.\n", - "- With default settings it should finish in a few minutes on Colab CPU/GPU.\n" + "Why this is useful:\n", + "- It demonstrates reuse of existing model infrastructure from EngiOpt.\n", + "- It clarifies how to adapt EngiOpt model classes to new, custom problem scaffolds.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "b1e89486", + "id": "af8e43e7", "metadata": {}, "outputs": [], "source": [ "# Optional extension controls (safe defaults)\n", "from pathlib import Path\n", "import sys\n", + "\n", + "import numpy as np\n", "import torch as th\n", "import torch.nn as nn\n", "from torch.utils.data import DataLoader, TensorDataset\n", "\n", "RUN_OPTIONAL_SECTION = False # Set True to run this optional extension\n", - "N_FEASIBLE_SAMPLES = 240\n", + "N_FEASIBLE_SAMPLES = 260\n", "TOP_FRACTION = 0.35\n", - "EPOCHS = 18\n", + "EPOCHS = 30\n", "BATCH_SIZE = 64\n", - "LATENT_DIM = 4\n", - "BETA_KL = 1e-3\n", + "LATENT_DIM = 8\n", "FAST_SIM_CFG = {'sim_steps': 80, 'dt': 1.0 / 120.0}\n", "EVAL_SAMPLES = 40\n", "\n", @@ -388,13 +387,11 @@ { "cell_type": "code", "execution_count": null, - "id": "527ee9bc", + "id": "8a16f8e2", "metadata": {}, "outputs": [], "source": [ "# Build offline dataset from simulator rollouts\n", - "import numpy as np\n", - "\n", "rng = np.random.default_rng(123)\n", "\n", "\n", @@ -417,13 +414,12 @@ "\n", "\n", "def objective_score(obj: np.ndarray) -> float:\n", - " # Same scalarization as quick optimization above: error + 0.02 * energy\n", " return float(obj[0] + 0.02 * obj[1])\n", "\n", "\n", "def make_dataset(problem_obj, n_feasible: int):\n", " designs, conds, objs = [], [], []\n", - " max_attempts = n_feasible * 6\n", + " max_attempts = n_feasible * 8\n", " attempts = 0\n", "\n", " while len(designs) < n_feasible and attempts < max_attempts:\n", @@ -431,8 +427,7 @@ " d, _ = problem_obj.random_design()\n", " cfg = sample_condition_dict()\n", "\n", - " violations = problem_obj.check_constraints(d, cfg)\n", - " if len(violations) > 0:\n", + " if len(problem_obj.check_constraints(d, cfg)) > 0:\n", " continue\n", "\n", " obj = problem_obj.simulate(d, {**cfg, **FAST_SIM_CFG})\n", @@ -443,17 +438,15 @@ " if len(designs) % 40 == 0:\n", " print(f'Collected feasible samples: {len(designs)}/{n_feasible}')\n", "\n", - " if len(designs) < max(32, n_feasible // 3):\n", - " raise RuntimeError(\n", - " f'Not enough feasible samples ({len(designs)}). Increase attempts or relax settings.'\n", - " )\n", + " if len(designs) < max(48, n_feasible // 3):\n", + " raise RuntimeError(f'Not enough feasible samples ({len(designs)}).')\n", "\n", " designs = np.stack(designs)\n", " conds = np.stack(conds)\n", " objs = np.stack(objs)\n", " scores = np.array([objective_score(o) for o in objs], dtype=np.float32)\n", "\n", - " keep_n = max(32, int(TOP_FRACTION * len(scores)))\n", + " keep_n = max(48, int(TOP_FRACTION * len(scores)))\n", " top_idx = np.argsort(scores)[:keep_n]\n", "\n", " data = {\n", @@ -481,117 +474,131 @@ }, { "cell_type": "markdown", - "id": "1f952fc5", + "id": "0231e825", "metadata": {}, "source": [ - "### Optional model - Conditional VAE for inverse design\n", + "### Optional model - EngiOpt `cgan_1d`\n", "\n", - "Modeling setup:\n", - "- **Condition input:** `(target_x, target_y, payload_kg, disturbance_scale)`\n", - "- **Generated output:** 6D design vector in the problem design space.\n", - "- **Training target:** top-performing feasible samples from the offline dataset.\n", + "This cell reuses `engiopt.cgan_1d.cgan_1d` classes directly:\n", + "- `Normalizer`\n", + "- `Generator`\n", + "- `Discriminator`\n", "\n", - "This gives a minimal but valid generative baseline for `p(design | condition)`.\n" + "Adapter note:\n", + "- `Discriminator` in this module expects module-level `design_shape` and `n_conds` symbols.\n", + "- We set those explicitly before instantiation to keep behavior aligned with the original script.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "79320050", + "id": "14ce7c39", "metadata": {}, "outputs": [], "source": [ - "# Define and train a small conditional VAE\n", - "class ConditionalVAE(nn.Module):\n", - " def __init__(self, cond_dim: int, design_dim: int, latent_dim: int = 4, hidden: int = 96):\n", - " super().__init__()\n", - " self.enc = nn.Sequential(\n", - " nn.Linear(cond_dim + design_dim, hidden),\n", - " nn.ReLU(),\n", - " nn.Linear(hidden, hidden),\n", - " nn.ReLU(),\n", - " )\n", - " self.mu = nn.Linear(hidden, latent_dim)\n", - " self.logvar = nn.Linear(hidden, latent_dim)\n", - "\n", - " self.dec = nn.Sequential(\n", - " nn.Linear(cond_dim + latent_dim, hidden),\n", - " nn.ReLU(),\n", - " nn.Linear(hidden, hidden),\n", - " nn.ReLU(),\n", - " nn.Linear(hidden, design_dim),\n", - " nn.Sigmoid(),\n", - " )\n", + "# Train EngiOpt cgan_1d on the top-performing subset\n", + "if RUN_OPTIONAL_SECTION:\n", + " import engiopt.cgan_1d.cgan_1d as cgan1d\n", "\n", - " def encode(self, cond, design):\n", - " h = self.enc(th.cat([cond, design], dim=-1))\n", - " return self.mu(h), self.logvar(h)\n", + " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", "\n", - " def reparameterize(self, mu, logvar):\n", - " std = th.exp(0.5 * logvar)\n", - " eps = th.randn_like(std)\n", - " return mu + eps * std\n", + " x_cond = dataset['conditions_top'].astype(np.float32)\n", + " y_design = dataset['designs_top'].astype(np.float32)\n", "\n", - " def decode(self, cond, z):\n", - " return self.dec(th.cat([cond, z], dim=-1))\n", + " cond_t = th.tensor(x_cond, dtype=th.float32, device=device)\n", + " design_t = th.tensor(y_design, dtype=th.float32, device=device)\n", "\n", - " def forward(self, cond, design):\n", - " mu, logvar = self.encode(cond, design)\n", - " z = self.reparameterize(mu, logvar)\n", - " recon = self.decode(cond, z)\n", - " return recon, mu, logvar\n", + " cond_min = cond_t.amin(dim=0)\n", + " cond_max = cond_t.amax(dim=0)\n", + " design_min = design_t.amin(dim=0)\n", + " design_max = design_t.amax(dim=0)\n", "\n", + " conds_normalizer = cgan1d.Normalizer(cond_min, cond_max)\n", + " design_normalizer = cgan1d.Normalizer(design_min, design_max)\n", "\n", - "if RUN_OPTIONAL_SECTION:\n", - " lb = problem.design_space.low.astype(np.float32)\n", - " ub = problem.design_space.high.astype(np.float32)\n", + " design_shape = (design_t.shape[1],)\n", + " n_conds = cond_t.shape[1]\n", "\n", - " x_cond = dataset['conditions_top'].astype(np.float32)\n", - " y_design = dataset['designs_top'].astype(np.float32)\n", - " y_norm = np.clip((y_design - lb) / (ub - lb + 1e-8), 0.0, 1.0)\n", + " # Compatibility shim for cgan_1d.Discriminator internal references.\n", + " cgan1d.design_shape = design_shape\n", + " cgan1d.n_conds = n_conds\n", "\n", - " cond_t = th.tensor(x_cond, dtype=th.float32)\n", - " design_t = th.tensor(y_norm, dtype=th.float32)\n", - " loader = DataLoader(TensorDataset(cond_t, design_t), batch_size=BATCH_SIZE, shuffle=True)\n", + " generator = cgan1d.Generator(\n", + " latent_dim=LATENT_DIM,\n", + " n_conds=n_conds,\n", + " design_shape=design_shape,\n", + " design_normalizer=design_normalizer,\n", + " conds_normalizer=conds_normalizer,\n", + " ).to(device)\n", "\n", - " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", - " model = ConditionalVAE(cond_dim=4, design_dim=6, latent_dim=LATENT_DIM).to(device)\n", - " opt = th.optim.Adam(model.parameters(), lr=2e-3)\n", + " discriminator = cgan1d.Discriminator(\n", + " conds_normalizer=conds_normalizer,\n", + " design_normalizer=design_normalizer,\n", + " ).to(device)\n", + "\n", + " loader = DataLoader(TensorDataset(design_t, cond_t), batch_size=BATCH_SIZE, shuffle=True)\n", + "\n", + " adv_loss = nn.BCELoss()\n", + " opt_g = th.optim.Adam(generator.parameters(), lr=2e-4, betas=(0.5, 0.999))\n", + " opt_d = th.optim.Adam(discriminator.parameters(), lr=2e-4, betas=(0.5, 0.999))\n", + "\n", + " g_hist, d_hist = [], []\n", "\n", - " history = []\n", " for epoch in range(1, EPOCHS + 1):\n", - " model.train()\n", - " epoch_loss = 0.0\n", - " for c_batch, d_batch in loader:\n", - " c_batch = c_batch.to(device)\n", - " d_batch = d_batch.to(device)\n", - "\n", - " recon, mu, logvar = model(c_batch, d_batch)\n", - " recon_loss = th.mean((recon - d_batch) ** 2)\n", - " kl = -0.5 * th.mean(1 + logvar - mu.pow(2) - logvar.exp())\n", - " loss = recon_loss + BETA_KL * kl\n", - "\n", - " opt.zero_grad()\n", - " loss.backward()\n", - " opt.step()\n", - " epoch_loss += float(loss.item())\n", - "\n", - " epoch_loss /= max(1, len(loader))\n", - " history.append(epoch_loss)\n", + " g_epoch, d_epoch, n_steps = 0.0, 0.0, 0\n", + " for real_design, cond in loader:\n", + " bs = real_design.shape[0]\n", + " valid = th.ones((bs, 1), device=device)\n", + " fake = th.zeros((bs, 1), device=device)\n", + "\n", + " # Generator update\n", + " opt_g.zero_grad()\n", + " z = th.randn((bs, LATENT_DIM), device=device)\n", + " gen_design = generator(z, cond)\n", + " g_loss = adv_loss(discriminator(gen_design, cond), valid)\n", + " g_loss.backward()\n", + " opt_g.step()\n", + "\n", + " # Discriminator update\n", + " opt_d.zero_grad()\n", + " real_loss = adv_loss(discriminator(real_design, cond), valid)\n", + " fake_loss = adv_loss(discriminator(gen_design.detach(), cond), fake)\n", + " d_loss = 0.5 * (real_loss + fake_loss)\n", + " d_loss.backward()\n", + " opt_d.step()\n", + "\n", + " g_epoch += float(g_loss.item())\n", + " d_epoch += float(d_loss.item())\n", + " n_steps += 1\n", + "\n", + " g_hist.append(g_epoch / max(1, n_steps))\n", + " d_hist.append(d_epoch / max(1, n_steps))\n", " if epoch == 1 or epoch % 5 == 0 or epoch == EPOCHS:\n", - " print(f'Epoch {epoch:02d}/{EPOCHS} | loss={epoch_loss:.6f}')\n", - "\n", - " th.save(model.state_dict(), OPTIONAL_ARTIFACT_DIR / 'cvae_weights.pt')\n", - " print('Saved model:', OPTIONAL_ARTIFACT_DIR / 'cvae_weights.pt')\n", + " print(f'Epoch {epoch:02d}/{EPOCHS} | g_loss={g_hist[-1]:.6f} | d_loss={d_hist[-1]:.6f}')\n", + "\n", + " th.save(\n", + " {\n", + " 'generator': generator.state_dict(),\n", + " 'discriminator': discriminator.state_dict(),\n", + " 'cond_min': cond_min.cpu(),\n", + " 'cond_max': cond_max.cpu(),\n", + " 'design_min': design_min.cpu(),\n", + " 'design_max': design_max.cpu(),\n", + " },\n", + " OPTIONAL_ARTIFACT_DIR / 'engiopt_cgan1d_weights.pt',\n", + " )\n", + " print('Saved model:', OPTIONAL_ARTIFACT_DIR / 'engiopt_cgan1d_weights.pt')\n", "else:\n", - " model, history, device = None, [], th.device('cpu')\n", + " generator, discriminator = None, None\n", + " g_hist, d_hist = [], []\n", + " device = th.device('cpu')\n", " print('Skipped model training. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "723f3f4e", + "id": "bea5477f", "metadata": {}, "outputs": [], "source": [ @@ -609,19 +616,17 @@ " if best_obj is None or objective_score(obj) < objective_score(best_obj):\n", " best_obj = obj\n", " if best_obj is None:\n", - " # fallback if all random candidates violate constraints\n", " d, _ = problem_obj.random_design()\n", " best_obj = problem_obj.simulate(d, {**cfg, **FAST_SIM_CFG})\n", " return best_obj\n", "\n", "\n", - "def generate_design(model_obj, cfg_vec: np.ndarray, lb: np.ndarray, ub: np.ndarray):\n", + "def generate_design(generator_obj, cfg_vec: np.ndarray, lb: np.ndarray, ub: np.ndarray):\n", " with th.no_grad():\n", " c = th.tensor(cfg_vec[None, :], dtype=th.float32, device=device)\n", " z = th.randn((1, LATENT_DIM), dtype=th.float32, device=device)\n", - " d_norm = model_obj.decode(c, z).cpu().numpy()[0]\n", - " d = lb + np.clip(d_norm, 0.0, 1.0) * (ub - lb)\n", - " return d.astype(np.float32)\n", + " d = generator_obj(z, c).cpu().numpy()[0]\n", + " return np.clip(d.astype(np.float32), lb, ub)\n", "\n", "\n", "if RUN_OPTIONAL_SECTION:\n", @@ -636,10 +641,9 @@ " cfg = sample_condition_dict()\n", " cfg_vec = cond_to_vec(cfg)\n", "\n", - " # try a few generated candidates to satisfy constraints\n", " gen_obj = None\n", - " for _retry in range(5):\n", - " d_gen = generate_design(model, cfg_vec, lb, ub)\n", + " for _retry in range(6):\n", + " d_gen = generate_design(generator, cfg_vec, lb, ub)\n", " if len(problem.check_constraints(d_gen, cfg)) == 0:\n", " gen_obj = problem.simulate(d_gen, {**cfg, **FAST_SIM_CFG})\n", " feasible_count += 1\n", @@ -662,17 +666,27 @@ " 'baseline_energy_mean': float(np.mean(base_objs[:, 1])),\n", " 'generated_feasible_rate': float(feasible_count / EVAL_SAMPLES),\n", " }\n", - " print('Optional extension summary:')\n", + "\n", + " print('Optional extension summary (EngiOpt cgan_1d):')\n", " for k, v in summary.items():\n", " print(f' {k}: {v:.6f}')\n", "\n", - " np.savez(OPTIONAL_ARTIFACT_DIR / 'optional_eval_summary.npz', gen_objs=gen_objs, base_objs=base_objs, **summary)\n", + " np.savez(\n", + " OPTIONAL_ARTIFACT_DIR / 'optional_eval_summary_engiopt_cgan1d.npz',\n", + " gen_objs=gen_objs,\n", + " base_objs=base_objs,\n", + " g_hist=np.array(g_hist, dtype=np.float32),\n", + " d_hist=np.array(d_hist, dtype=np.float32),\n", + " **summary,\n", + " )\n", "\n", " fig, axes = plt.subplots(1, 3, figsize=(14, 4))\n", - " axes[0].plot(history)\n", - " axes[0].set_title('cVAE training loss')\n", + "\n", + " axes[0].plot(g_hist, label='g_loss')\n", + " axes[0].plot(d_hist, label='d_loss')\n", + " axes[0].set_title('EngiOpt cgan_1d training losses')\n", " axes[0].set_xlabel('epoch')\n", - " axes[0].set_ylabel('loss')\n", + " axes[0].legend()\n", " axes[0].grid(alpha=0.3)\n", "\n", " axes[1].hist(base_objs[:, 0], bins=12, alpha=0.6, label='baseline')\n", @@ -695,16 +709,14 @@ }, { "cell_type": "markdown", - "id": "f479ce8a", + "id": "594df993", "metadata": {}, "source": [ "### Discussion prompts for workshop synthesis\n", "\n", - "Use this optional experiment to trigger discussion:\n", - "\n", - "1. Is simulator-generated offline data enough for credible inverse-design benchmarking?\n", - "2. Which metrics should become mandatory in cross-domain comparisons (quality, feasibility, diversity, cost)?\n", - "3. How would you package this scaffold into a reusable EngiBench-style contribution (dataset card, baselines, splits, eval protocol)?\n" + "1. How portable are EngiOpt models across domains with different design representations?\n", + "2. What is the minimum adapter contract needed to reuse a model on a new problem?\n", + "3. Should benchmark reporting require both feasibility and objective trade-off metrics for generated designs?\n" ] } ], diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 4dc5d8d..db8ea8c 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -65,10 +65,12 @@ "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", + "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", " !pip install engibench[beams2d] matplotlib gymnasium pybullet\n", + " !pip install {ENGIOPT_GIT}\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", @@ -499,48 +501,45 @@ }, { "cell_type": "markdown", - "id": "41539f9b", + "id": "206a966d", "metadata": {}, "source": [ - "## Optional extension - Build a dataset and train a generative model\n", + "## Optional extension - Build dataset and train an EngiOpt generative model\n", "\n", - "This section shows the full **offline benchmark loop** on the new problem:\n", + "This extension runs a full offline loop using an **existing EngiOpt model** (`cgan_1d`) on top of your custom simulator problem:\n", "\n", - "1. Sample feasible designs and conditions with the simulator in-the-loop.\n", - "2. Build a compact training dataset.\n", - "3. Train a small conditional VAE (cVAE) to generate designs from conditions.\n", - "4. Evaluate generated designs against a random-design baseline.\n", + "1. Generate a feasible dataset from simulator rollouts.\n", + "2. Keep a top-performing subset.\n", + "3. Train EngiOpt `cgan_1d` (`Generator` + `Discriminator`) for conditional generation.\n", + "4. Compare generated designs vs a random-design baseline.\n", "\n", - "Why this matters:\n", - "- It demonstrates that your scaffold is not just an API shell; it can support full generative benchmarking.\n", - "- It creates a reproducible path from simulator to learned design distribution.\n", - "\n", - "Runtime note:\n", - "- Keep this optional (`RUN_OPTIONAL_SECTION=False`) during live sessions unless you have extra time.\n", - "- With default settings it should finish in a few minutes on Colab CPU/GPU.\n" + "Why this is useful:\n", + "- It demonstrates reuse of existing model infrastructure from EngiOpt.\n", + "- It clarifies how to adapt EngiOpt model classes to new, custom problem scaffolds.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "97dc85be", + "id": "9054cfce", "metadata": {}, "outputs": [], "source": [ "# Optional extension controls (safe defaults)\n", "from pathlib import Path\n", "import sys\n", + "\n", + "import numpy as np\n", "import torch as th\n", "import torch.nn as nn\n", "from torch.utils.data import DataLoader, TensorDataset\n", "\n", "RUN_OPTIONAL_SECTION = False # Set True to run this optional extension\n", - "N_FEASIBLE_SAMPLES = 240\n", + "N_FEASIBLE_SAMPLES = 260\n", "TOP_FRACTION = 0.35\n", - "EPOCHS = 18\n", + "EPOCHS = 30\n", "BATCH_SIZE = 64\n", - "LATENT_DIM = 4\n", - "BETA_KL = 1e-3\n", + "LATENT_DIM = 8\n", "FAST_SIM_CFG = {'sim_steps': 80, 'dt': 1.0 / 120.0}\n", "EVAL_SAMPLES = 40\n", "\n", @@ -560,13 +559,11 @@ { "cell_type": "code", "execution_count": null, - "id": "4f29c9d4", + "id": "9780c307", "metadata": {}, "outputs": [], "source": [ "# Build offline dataset from simulator rollouts\n", - "import numpy as np\n", - "\n", "rng = np.random.default_rng(123)\n", "\n", "\n", @@ -589,13 +586,12 @@ "\n", "\n", "def objective_score(obj: np.ndarray) -> float:\n", - " # Same scalarization as quick optimization above: error + 0.02 * energy\n", " return float(obj[0] + 0.02 * obj[1])\n", "\n", "\n", "def make_dataset(problem_obj, n_feasible: int):\n", " designs, conds, objs = [], [], []\n", - " max_attempts = n_feasible * 6\n", + " max_attempts = n_feasible * 8\n", " attempts = 0\n", "\n", " while len(designs) < n_feasible and attempts < max_attempts:\n", @@ -603,8 +599,7 @@ " d, _ = problem_obj.random_design()\n", " cfg = sample_condition_dict()\n", "\n", - " violations = problem_obj.check_constraints(d, cfg)\n", - " if len(violations) > 0:\n", + " if len(problem_obj.check_constraints(d, cfg)) > 0:\n", " continue\n", "\n", " obj = problem_obj.simulate(d, {**cfg, **FAST_SIM_CFG})\n", @@ -615,17 +610,15 @@ " if len(designs) % 40 == 0:\n", " print(f'Collected feasible samples: {len(designs)}/{n_feasible}')\n", "\n", - " if len(designs) < max(32, n_feasible // 3):\n", - " raise RuntimeError(\n", - " f'Not enough feasible samples ({len(designs)}). Increase attempts or relax settings.'\n", - " )\n", + " if len(designs) < max(48, n_feasible // 3):\n", + " raise RuntimeError(f'Not enough feasible samples ({len(designs)}).')\n", "\n", " designs = np.stack(designs)\n", " conds = np.stack(conds)\n", " objs = np.stack(objs)\n", " scores = np.array([objective_score(o) for o in objs], dtype=np.float32)\n", "\n", - " keep_n = max(32, int(TOP_FRACTION * len(scores)))\n", + " keep_n = max(48, int(TOP_FRACTION * len(scores)))\n", " top_idx = np.argsort(scores)[:keep_n]\n", "\n", " data = {\n", @@ -653,117 +646,131 @@ }, { "cell_type": "markdown", - "id": "c513ee3c", + "id": "57dac117", "metadata": {}, "source": [ - "### Optional model - Conditional VAE for inverse design\n", + "### Optional model - EngiOpt `cgan_1d`\n", "\n", - "Modeling setup:\n", - "- **Condition input:** `(target_x, target_y, payload_kg, disturbance_scale)`\n", - "- **Generated output:** 6D design vector in the problem design space.\n", - "- **Training target:** top-performing feasible samples from the offline dataset.\n", + "This cell reuses `engiopt.cgan_1d.cgan_1d` classes directly:\n", + "- `Normalizer`\n", + "- `Generator`\n", + "- `Discriminator`\n", "\n", - "This gives a minimal but valid generative baseline for `p(design | condition)`.\n" + "Adapter note:\n", + "- `Discriminator` in this module expects module-level `design_shape` and `n_conds` symbols.\n", + "- We set those explicitly before instantiation to keep behavior aligned with the original script.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "cc837146", + "id": "f1d6074e", "metadata": {}, "outputs": [], "source": [ - "# Define and train a small conditional VAE\n", - "class ConditionalVAE(nn.Module):\n", - " def __init__(self, cond_dim: int, design_dim: int, latent_dim: int = 4, hidden: int = 96):\n", - " super().__init__()\n", - " self.enc = nn.Sequential(\n", - " nn.Linear(cond_dim + design_dim, hidden),\n", - " nn.ReLU(),\n", - " nn.Linear(hidden, hidden),\n", - " nn.ReLU(),\n", - " )\n", - " self.mu = nn.Linear(hidden, latent_dim)\n", - " self.logvar = nn.Linear(hidden, latent_dim)\n", - "\n", - " self.dec = nn.Sequential(\n", - " nn.Linear(cond_dim + latent_dim, hidden),\n", - " nn.ReLU(),\n", - " nn.Linear(hidden, hidden),\n", - " nn.ReLU(),\n", - " nn.Linear(hidden, design_dim),\n", - " nn.Sigmoid(),\n", - " )\n", + "# Train EngiOpt cgan_1d on the top-performing subset\n", + "if RUN_OPTIONAL_SECTION:\n", + " import engiopt.cgan_1d.cgan_1d as cgan1d\n", "\n", - " def encode(self, cond, design):\n", - " h = self.enc(th.cat([cond, design], dim=-1))\n", - " return self.mu(h), self.logvar(h)\n", + " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", "\n", - " def reparameterize(self, mu, logvar):\n", - " std = th.exp(0.5 * logvar)\n", - " eps = th.randn_like(std)\n", - " return mu + eps * std\n", + " x_cond = dataset['conditions_top'].astype(np.float32)\n", + " y_design = dataset['designs_top'].astype(np.float32)\n", "\n", - " def decode(self, cond, z):\n", - " return self.dec(th.cat([cond, z], dim=-1))\n", + " cond_t = th.tensor(x_cond, dtype=th.float32, device=device)\n", + " design_t = th.tensor(y_design, dtype=th.float32, device=device)\n", "\n", - " def forward(self, cond, design):\n", - " mu, logvar = self.encode(cond, design)\n", - " z = self.reparameterize(mu, logvar)\n", - " recon = self.decode(cond, z)\n", - " return recon, mu, logvar\n", + " cond_min = cond_t.amin(dim=0)\n", + " cond_max = cond_t.amax(dim=0)\n", + " design_min = design_t.amin(dim=0)\n", + " design_max = design_t.amax(dim=0)\n", "\n", + " conds_normalizer = cgan1d.Normalizer(cond_min, cond_max)\n", + " design_normalizer = cgan1d.Normalizer(design_min, design_max)\n", "\n", - "if RUN_OPTIONAL_SECTION:\n", - " lb = problem.design_space.low.astype(np.float32)\n", - " ub = problem.design_space.high.astype(np.float32)\n", + " design_shape = (design_t.shape[1],)\n", + " n_conds = cond_t.shape[1]\n", "\n", - " x_cond = dataset['conditions_top'].astype(np.float32)\n", - " y_design = dataset['designs_top'].astype(np.float32)\n", - " y_norm = np.clip((y_design - lb) / (ub - lb + 1e-8), 0.0, 1.0)\n", + " # Compatibility shim for cgan_1d.Discriminator internal references.\n", + " cgan1d.design_shape = design_shape\n", + " cgan1d.n_conds = n_conds\n", "\n", - " cond_t = th.tensor(x_cond, dtype=th.float32)\n", - " design_t = th.tensor(y_norm, dtype=th.float32)\n", - " loader = DataLoader(TensorDataset(cond_t, design_t), batch_size=BATCH_SIZE, shuffle=True)\n", + " generator = cgan1d.Generator(\n", + " latent_dim=LATENT_DIM,\n", + " n_conds=n_conds,\n", + " design_shape=design_shape,\n", + " design_normalizer=design_normalizer,\n", + " conds_normalizer=conds_normalizer,\n", + " ).to(device)\n", "\n", - " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", - " model = ConditionalVAE(cond_dim=4, design_dim=6, latent_dim=LATENT_DIM).to(device)\n", - " opt = th.optim.Adam(model.parameters(), lr=2e-3)\n", + " discriminator = cgan1d.Discriminator(\n", + " conds_normalizer=conds_normalizer,\n", + " design_normalizer=design_normalizer,\n", + " ).to(device)\n", + "\n", + " loader = DataLoader(TensorDataset(design_t, cond_t), batch_size=BATCH_SIZE, shuffle=True)\n", + "\n", + " adv_loss = nn.BCELoss()\n", + " opt_g = th.optim.Adam(generator.parameters(), lr=2e-4, betas=(0.5, 0.999))\n", + " opt_d = th.optim.Adam(discriminator.parameters(), lr=2e-4, betas=(0.5, 0.999))\n", + "\n", + " g_hist, d_hist = [], []\n", "\n", - " history = []\n", " for epoch in range(1, EPOCHS + 1):\n", - " model.train()\n", - " epoch_loss = 0.0\n", - " for c_batch, d_batch in loader:\n", - " c_batch = c_batch.to(device)\n", - " d_batch = d_batch.to(device)\n", - "\n", - " recon, mu, logvar = model(c_batch, d_batch)\n", - " recon_loss = th.mean((recon - d_batch) ** 2)\n", - " kl = -0.5 * th.mean(1 + logvar - mu.pow(2) - logvar.exp())\n", - " loss = recon_loss + BETA_KL * kl\n", - "\n", - " opt.zero_grad()\n", - " loss.backward()\n", - " opt.step()\n", - " epoch_loss += float(loss.item())\n", - "\n", - " epoch_loss /= max(1, len(loader))\n", - " history.append(epoch_loss)\n", + " g_epoch, d_epoch, n_steps = 0.0, 0.0, 0\n", + " for real_design, cond in loader:\n", + " bs = real_design.shape[0]\n", + " valid = th.ones((bs, 1), device=device)\n", + " fake = th.zeros((bs, 1), device=device)\n", + "\n", + " # Generator update\n", + " opt_g.zero_grad()\n", + " z = th.randn((bs, LATENT_DIM), device=device)\n", + " gen_design = generator(z, cond)\n", + " g_loss = adv_loss(discriminator(gen_design, cond), valid)\n", + " g_loss.backward()\n", + " opt_g.step()\n", + "\n", + " # Discriminator update\n", + " opt_d.zero_grad()\n", + " real_loss = adv_loss(discriminator(real_design, cond), valid)\n", + " fake_loss = adv_loss(discriminator(gen_design.detach(), cond), fake)\n", + " d_loss = 0.5 * (real_loss + fake_loss)\n", + " d_loss.backward()\n", + " opt_d.step()\n", + "\n", + " g_epoch += float(g_loss.item())\n", + " d_epoch += float(d_loss.item())\n", + " n_steps += 1\n", + "\n", + " g_hist.append(g_epoch / max(1, n_steps))\n", + " d_hist.append(d_epoch / max(1, n_steps))\n", " if epoch == 1 or epoch % 5 == 0 or epoch == EPOCHS:\n", - " print(f'Epoch {epoch:02d}/{EPOCHS} | loss={epoch_loss:.6f}')\n", - "\n", - " th.save(model.state_dict(), OPTIONAL_ARTIFACT_DIR / 'cvae_weights.pt')\n", - " print('Saved model:', OPTIONAL_ARTIFACT_DIR / 'cvae_weights.pt')\n", + " print(f'Epoch {epoch:02d}/{EPOCHS} | g_loss={g_hist[-1]:.6f} | d_loss={d_hist[-1]:.6f}')\n", + "\n", + " th.save(\n", + " {\n", + " 'generator': generator.state_dict(),\n", + " 'discriminator': discriminator.state_dict(),\n", + " 'cond_min': cond_min.cpu(),\n", + " 'cond_max': cond_max.cpu(),\n", + " 'design_min': design_min.cpu(),\n", + " 'design_max': design_max.cpu(),\n", + " },\n", + " OPTIONAL_ARTIFACT_DIR / 'engiopt_cgan1d_weights.pt',\n", + " )\n", + " print('Saved model:', OPTIONAL_ARTIFACT_DIR / 'engiopt_cgan1d_weights.pt')\n", "else:\n", - " model, history, device = None, [], th.device('cpu')\n", + " generator, discriminator = None, None\n", + " g_hist, d_hist = [], []\n", + " device = th.device('cpu')\n", " print('Skipped model training. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "25af8209", + "id": "e5469eb4", "metadata": {}, "outputs": [], "source": [ @@ -781,19 +788,17 @@ " if best_obj is None or objective_score(obj) < objective_score(best_obj):\n", " best_obj = obj\n", " if best_obj is None:\n", - " # fallback if all random candidates violate constraints\n", " d, _ = problem_obj.random_design()\n", " best_obj = problem_obj.simulate(d, {**cfg, **FAST_SIM_CFG})\n", " return best_obj\n", "\n", "\n", - "def generate_design(model_obj, cfg_vec: np.ndarray, lb: np.ndarray, ub: np.ndarray):\n", + "def generate_design(generator_obj, cfg_vec: np.ndarray, lb: np.ndarray, ub: np.ndarray):\n", " with th.no_grad():\n", " c = th.tensor(cfg_vec[None, :], dtype=th.float32, device=device)\n", " z = th.randn((1, LATENT_DIM), dtype=th.float32, device=device)\n", - " d_norm = model_obj.decode(c, z).cpu().numpy()[0]\n", - " d = lb + np.clip(d_norm, 0.0, 1.0) * (ub - lb)\n", - " return d.astype(np.float32)\n", + " d = generator_obj(z, c).cpu().numpy()[0]\n", + " return np.clip(d.astype(np.float32), lb, ub)\n", "\n", "\n", "if RUN_OPTIONAL_SECTION:\n", @@ -808,10 +813,9 @@ " cfg = sample_condition_dict()\n", " cfg_vec = cond_to_vec(cfg)\n", "\n", - " # try a few generated candidates to satisfy constraints\n", " gen_obj = None\n", - " for _retry in range(5):\n", - " d_gen = generate_design(model, cfg_vec, lb, ub)\n", + " for _retry in range(6):\n", + " d_gen = generate_design(generator, cfg_vec, lb, ub)\n", " if len(problem.check_constraints(d_gen, cfg)) == 0:\n", " gen_obj = problem.simulate(d_gen, {**cfg, **FAST_SIM_CFG})\n", " feasible_count += 1\n", @@ -834,17 +838,27 @@ " 'baseline_energy_mean': float(np.mean(base_objs[:, 1])),\n", " 'generated_feasible_rate': float(feasible_count / EVAL_SAMPLES),\n", " }\n", - " print('Optional extension summary:')\n", + "\n", + " print('Optional extension summary (EngiOpt cgan_1d):')\n", " for k, v in summary.items():\n", " print(f' {k}: {v:.6f}')\n", "\n", - " np.savez(OPTIONAL_ARTIFACT_DIR / 'optional_eval_summary.npz', gen_objs=gen_objs, base_objs=base_objs, **summary)\n", + " np.savez(\n", + " OPTIONAL_ARTIFACT_DIR / 'optional_eval_summary_engiopt_cgan1d.npz',\n", + " gen_objs=gen_objs,\n", + " base_objs=base_objs,\n", + " g_hist=np.array(g_hist, dtype=np.float32),\n", + " d_hist=np.array(d_hist, dtype=np.float32),\n", + " **summary,\n", + " )\n", "\n", " fig, axes = plt.subplots(1, 3, figsize=(14, 4))\n", - " axes[0].plot(history)\n", - " axes[0].set_title('cVAE training loss')\n", + "\n", + " axes[0].plot(g_hist, label='g_loss')\n", + " axes[0].plot(d_hist, label='d_loss')\n", + " axes[0].set_title('EngiOpt cgan_1d training losses')\n", " axes[0].set_xlabel('epoch')\n", - " axes[0].set_ylabel('loss')\n", + " axes[0].legend()\n", " axes[0].grid(alpha=0.3)\n", "\n", " axes[1].hist(base_objs[:, 0], bins=12, alpha=0.6, label='baseline')\n", @@ -867,16 +881,14 @@ }, { "cell_type": "markdown", - "id": "efd692b9", + "id": "90f7d4ff", "metadata": {}, "source": [ "### Discussion prompts for workshop synthesis\n", "\n", - "Use this optional experiment to trigger discussion:\n", - "\n", - "1. Is simulator-generated offline data enough for credible inverse-design benchmarking?\n", - "2. Which metrics should become mandatory in cross-domain comparisons (quality, feasibility, diversity, cost)?\n", - "3. How would you package this scaffold into a reusable EngiBench-style contribution (dataset card, baselines, splits, eval protocol)?\n" + "1. How portable are EngiOpt models across domains with different design representations?\n", + "2. What is the minimum adapter contract needed to reuse a model on a new problem?\n", + "3. Should benchmark reporting require both feasibility and objective trade-off metrics for generated designs?\n" ] } ], From c7cc5c2b18f9b5ace1a7ba99671ef9846d556549 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Mon, 16 Mar 2026 09:37:39 +0100 Subject: [PATCH 28/37] Fix Notebook 03 cgan_1d single-sample inference mode --- .../participant/03_add_new_problem_scaffold.ipynb | 15 +++++++++++---- .../solutions/03_add_new_problem_scaffold.ipynb | 15 +++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index a793b85..130f5b5 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -622,10 +622,17 @@ "\n", "\n", "def generate_design(generator_obj, cfg_vec: np.ndarray, lb: np.ndarray, ub: np.ndarray):\n", - " with th.no_grad():\n", - " c = th.tensor(cfg_vec[None, :], dtype=th.float32, device=device)\n", - " z = th.randn((1, LATENT_DIM), dtype=th.float32, device=device)\n", - " d = generator_obj(z, c).cpu().numpy()[0]\n", + " # cgan_1d Generator uses BatchNorm; switch to eval for single-sample inference.\n", + " was_training = generator_obj.training\n", + " generator_obj.eval()\n", + " try:\n", + " with th.no_grad():\n", + " c = th.tensor(cfg_vec[None, :], dtype=th.float32, device=device)\n", + " z = th.randn((1, LATENT_DIM), dtype=th.float32, device=device)\n", + " d = generator_obj(z, c).cpu().numpy()[0]\n", + " finally:\n", + " if was_training:\n", + " generator_obj.train()\n", " return np.clip(d.astype(np.float32), lb, ub)\n", "\n", "\n", diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index db8ea8c..7df2d6f 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -794,10 +794,17 @@ "\n", "\n", "def generate_design(generator_obj, cfg_vec: np.ndarray, lb: np.ndarray, ub: np.ndarray):\n", - " with th.no_grad():\n", - " c = th.tensor(cfg_vec[None, :], dtype=th.float32, device=device)\n", - " z = th.randn((1, LATENT_DIM), dtype=th.float32, device=device)\n", - " d = generator_obj(z, c).cpu().numpy()[0]\n", + " # cgan_1d Generator uses BatchNorm; switch to eval for single-sample inference.\n", + " was_training = generator_obj.training\n", + " generator_obj.eval()\n", + " try:\n", + " with th.no_grad():\n", + " c = th.tensor(cfg_vec[None, :], dtype=th.float32, device=device)\n", + " z = th.randn((1, LATENT_DIM), dtype=th.float32, device=device)\n", + " d = generator_obj(z, c).cpu().numpy()[0]\n", + " finally:\n", + " if was_training:\n", + " generator_obj.train()\n", " return np.clip(d.astype(np.float32), lb, ub)\n", "\n", "\n", From 630921e25f07a1c57509bec6f15bee3f9a98b4b5 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Mon, 16 Mar 2026 09:41:51 +0100 Subject: [PATCH 29/37] Pin pandas and rich in Colab installs for compatibility --- workshops/dcc26/participant/00_setup_api_warmup.ipynb | 2 +- workshops/dcc26/participant/01_train_generate.ipynb | 4 ++-- workshops/dcc26/participant/02_evaluate_metrics.ipynb | 4 ++-- .../dcc26/participant/03_add_new_problem_scaffold.ipynb | 6 +++--- workshops/dcc26/solutions/00_setup_api_warmup.ipynb | 2 +- workshops/dcc26/solutions/01_train_generate.ipynb | 4 ++-- workshops/dcc26/solutions/02_evaluate_metrics.ipynb | 4 ++-- workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb | 6 +++--- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index cd978cb..783b845 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -84,7 +84,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib seaborn\n", + " !pip install engibench[beams2d] matplotlib seaborn \"pandas<3\" \"rich<14\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index de91ddc..89859ef 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -60,8 +60,8 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib pandas tqdm tyro wandb\n", - " !pip install {ENGIOPT_GIT}\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas<3\" tqdm tyro wandb \"rich<14\"\n", + " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index c635c58..3e4da13 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -59,8 +59,8 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib pandas tqdm tyro wandb\n", - " !pip install {ENGIOPT_GIT}\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas<3\" tqdm tyro wandb \"rich<14\"\n", + " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 130f5b5..9ae8fdc 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -69,12 +69,12 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib gymnasium pybullet\n", - " !pip install {ENGIOPT_GIT}\n", + " !pip install engibench[beams2d] matplotlib gymnasium pybullet \"pandas<3\" \"rich<14\"\n", + " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " !pip install torch torchvision\n", + " !pip install torch torchvision \"pandas<3\" \"rich<14\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index b5ca1e3..b46b141 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -80,7 +80,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib seaborn\n", + " !pip install engibench[beams2d] matplotlib seaborn \"pandas<3\" \"rich<14\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 0f46ddc..b4f8dc1 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -60,8 +60,8 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib pandas tqdm tyro wandb\n", - " !pip install {ENGIOPT_GIT}\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas<3\" tqdm tyro wandb \"rich<14\"\n", + " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 16b3f0e..b0e00e7 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -59,8 +59,8 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib pandas tqdm tyro wandb\n", - " !pip install {ENGIOPT_GIT}\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas<3\" tqdm tyro wandb \"rich<14\"\n", + " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 7df2d6f..c4cd286 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -69,12 +69,12 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib gymnasium pybullet\n", - " !pip install {ENGIOPT_GIT}\n", + " !pip install engibench[beams2d] matplotlib gymnasium pybullet \"pandas<3\" \"rich<14\"\n", + " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " !pip install torch torchvision\n", + " !pip install torch torchvision \"pandas<3\" \"rich<14\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" From 3ccad8b2419584ac1e1363311b691e18ac708514 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Mon, 16 Mar 2026 10:12:00 +0100 Subject: [PATCH 30/37] Pin Colab-compatible pandas and rich versions in notebooks --- workshops/dcc26/participant/00_setup_api_warmup.ipynb | 2 +- workshops/dcc26/participant/01_train_generate.ipynb | 4 ++-- workshops/dcc26/participant/02_evaluate_metrics.ipynb | 4 ++-- .../dcc26/participant/03_add_new_problem_scaffold.ipynb | 6 +++--- workshops/dcc26/solutions/00_setup_api_warmup.ipynb | 2 +- workshops/dcc26/solutions/01_train_generate.ipynb | 4 ++-- workshops/dcc26/solutions/02_evaluate_metrics.ipynb | 4 ++-- workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb | 6 +++--- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 783b845..9dca020 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -84,7 +84,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib seaborn \"pandas<3\" \"rich<14\"\n", + " !pip install engibench[beams2d] matplotlib seaborn \"pandas==2.2.2\" \"rich==13.9.4\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 89859ef..2bf43b7 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -60,8 +60,8 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas<3\" tqdm tyro wandb \"rich<14\"\n", - " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas==2.2.2\" tqdm tyro wandb \"rich==13.9.4\"\n", + " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index 3e4da13..7ae539b 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -59,8 +59,8 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas<3\" tqdm tyro wandb \"rich<14\"\n", - " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas==2.2.2\" tqdm tyro wandb \"rich==13.9.4\"\n", + " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 9ae8fdc..0c0a4de 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -69,12 +69,12 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib gymnasium pybullet \"pandas<3\" \"rich<14\"\n", - " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", + " !pip install engibench[beams2d] matplotlib gymnasium pybullet \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " !pip install torch torchvision \"pandas<3\" \"rich<14\"\n", + " !pip install torch torchvision \"pandas==2.2.2\" \"rich==13.9.4\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index b46b141..1fea9bf 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -80,7 +80,7 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib seaborn \"pandas<3\" \"rich<14\"\n", + " !pip install engibench[beams2d] matplotlib seaborn \"pandas==2.2.2\" \"rich==13.9.4\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index b4f8dc1..35dfa0e 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -60,8 +60,8 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas<3\" tqdm tyro wandb \"rich<14\"\n", - " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas==2.2.2\" tqdm tyro wandb \"rich==13.9.4\"\n", + " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index b0e00e7..b1d4570 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -59,8 +59,8 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas<3\" tqdm tyro wandb \"rich<14\"\n", - " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", + " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas==2.2.2\" tqdm tyro wandb \"rich==13.9.4\"\n", + " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index c4cd286..5145889 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -69,12 +69,12 @@ "\n", "if IN_COLAB or FORCE_INSTALL:\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib gymnasium pybullet \"pandas<3\" \"rich<14\"\n", - " !pip install {ENGIOPT_GIT} \"pandas<3\" \"rich<14\"\n", + " !pip install engibench[beams2d] matplotlib gymnasium pybullet \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " !pip install torch torchvision \"pandas<3\" \"rich<14\"\n", + " !pip install torch torchvision \"pandas==2.2.2\" \"rich==13.9.4\"\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" From 1f7d0a3e7ea39ca4738decee5b9d9bd5af87f0fa Mon Sep 17 00:00:00 2001 From: Soheyl Date: Mon, 16 Mar 2026 10:35:38 +0100 Subject: [PATCH 31/37] Add auto/locked Colab compatibility mode to notebook bootstraps --- .../participant/00_setup_api_warmup.ipynb | 49 +++++++++++++++++- .../dcc26/participant/01_train_generate.ipynb | 51 ++++++++++++++++++- .../participant/02_evaluate_metrics.ipynb | 51 ++++++++++++++++++- .../03_add_new_problem_scaffold.ipynb | 49 ++++++++++++++++-- .../dcc26/solutions/00_setup_api_warmup.ipynb | 49 +++++++++++++++++- .../dcc26/solutions/01_train_generate.ipynb | 51 ++++++++++++++++++- .../dcc26/solutions/02_evaluate_metrics.ipynb | 51 ++++++++++++++++++- .../03_add_new_problem_scaffold.ipynb | 49 ++++++++++++++++-- 8 files changed, 384 insertions(+), 16 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 9dca020..180054d 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -77,14 +77,61 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", + "import importlib.metadata as md\n", + "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", + "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", + "LOCKED_VERSIONS = {\n", + " 'pandas': '2.2.2',\n", + " 'rich': '13.9.4',\n", + "}\n", + "\n", + "\n", + "def installed_version(pkg: str):\n", + " try:\n", + " return md.version(pkg)\n", + " except Exception:\n", + " return None\n", + "\n", + "\n", + "def compat_pins() -> dict[str, str]:\n", + " if COLAB_COMPAT_MODE == 'locked':\n", + " return dict(LOCKED_VERSIONS)\n", + " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", + " pins = {}\n", + " for pkg in LOCKED_VERSIONS:\n", + " v = installed_version(pkg)\n", + " if v is not None:\n", + " pins[pkg] = v\n", + " return pins\n", + " return {}\n", + "\n", + "\n", + "def pip_install(packages: list[str]):\n", + " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", + " print('Running:', ' '.join(cmd))\n", + " subprocess.check_call(cmd)\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " pins = compat_pins()\n", + " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", + " if pin_args:\n", + " print('Compatibility pins:', ', '.join(pin_args))\n", + " else:\n", + " print('Compatibility pins: none')\n", + "\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib seaborn \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " pip_install(BASE_PACKAGES + pin_args)\n", + "\n", + " try:\n", + " import torch # noqa: F401\n", + " except Exception:\n", + " pip_install(['torch', 'torchvision'] + pin_args)\n", + "\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 2bf43b7..cfe350d 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -52,16 +52,63 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", + "import importlib.metadata as md\n", + "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", + "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", + "LOCKED_VERSIONS = {\n", + " 'pandas': '2.2.2',\n", + " 'rich': '13.9.4',\n", + "}\n", + "\n", + "\n", + "def installed_version(pkg: str):\n", + " try:\n", + " return md.version(pkg)\n", + " except Exception:\n", + " return None\n", + "\n", + "\n", + "def compat_pins() -> dict[str, str]:\n", + " if COLAB_COMPAT_MODE == 'locked':\n", + " return dict(LOCKED_VERSIONS)\n", + " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", + " pins = {}\n", + " for pkg in LOCKED_VERSIONS:\n", + " v = installed_version(pkg)\n", + " if v is not None:\n", + " pins[pkg] = v\n", + " return pins\n", + " return {}\n", + "\n", + "\n", + "def pip_install(packages: list[str]):\n", + " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", + " print('Running:', ' '.join(cmd))\n", + " subprocess.check_call(cmd)\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " pins = compat_pins()\n", + " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", + " if pin_args:\n", + " print('Compatibility pins:', ', '.join(pin_args))\n", + " else:\n", + " print('Compatibility pins: none')\n", + "\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas==2.2.2\" tqdm tyro wandb \"rich==13.9.4\"\n", - " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " pip_install(BASE_PACKAGES + pin_args)\n", + " pip_install([ENGIOPT_GIT] + pin_args)\n", + "\n", + " try:\n", + " import torch # noqa: F401\n", + " except Exception:\n", + " pip_install(['torch', 'torchvision'] + pin_args)\n", + "\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index 7ae539b..f0c2ebd 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -51,16 +51,63 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", + "import importlib.metadata as md\n", + "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", + "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", + "LOCKED_VERSIONS = {\n", + " 'pandas': '2.2.2',\n", + " 'rich': '13.9.4',\n", + "}\n", + "\n", + "\n", + "def installed_version(pkg: str):\n", + " try:\n", + " return md.version(pkg)\n", + " except Exception:\n", + " return None\n", + "\n", + "\n", + "def compat_pins() -> dict[str, str]:\n", + " if COLAB_COMPAT_MODE == 'locked':\n", + " return dict(LOCKED_VERSIONS)\n", + " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", + " pins = {}\n", + " for pkg in LOCKED_VERSIONS:\n", + " v = installed_version(pkg)\n", + " if v is not None:\n", + " pins[pkg] = v\n", + " return pins\n", + " return {}\n", + "\n", + "\n", + "def pip_install(packages: list[str]):\n", + " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", + " print('Running:', ' '.join(cmd))\n", + " subprocess.check_call(cmd)\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " pins = compat_pins()\n", + " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", + " if pin_args:\n", + " print('Compatibility pins:', ', '.join(pin_args))\n", + " else:\n", + " print('Compatibility pins: none')\n", + "\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas==2.2.2\" tqdm tyro wandb \"rich==13.9.4\"\n", - " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " pip_install(BASE_PACKAGES + pin_args)\n", + " pip_install([ENGIOPT_GIT] + pin_args)\n", + "\n", + " try:\n", + " import torch # noqa: F401\n", + " except Exception:\n", + " pip_install(['torch', 'torchvision'] + pin_args)\n", + "\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 0c0a4de..d66d2cb 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -61,20 +61,63 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", + "import importlib.metadata as md\n", + "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", + "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", + "LOCKED_VERSIONS = {\n", + " 'pandas': '2.2.2',\n", + " 'rich': '13.9.4',\n", + "}\n", + "\n", + "\n", + "def installed_version(pkg: str):\n", + " try:\n", + " return md.version(pkg)\n", + " except Exception:\n", + " return None\n", + "\n", + "\n", + "def compat_pins() -> dict[str, str]:\n", + " if COLAB_COMPAT_MODE == 'locked':\n", + " return dict(LOCKED_VERSIONS)\n", + " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", + " pins = {}\n", + " for pkg in LOCKED_VERSIONS:\n", + " v = installed_version(pkg)\n", + " if v is not None:\n", + " pins[pkg] = v\n", + " return pins\n", + " return {}\n", + "\n", + "\n", + "def pip_install(packages: list[str]):\n", + " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", + " print('Running:', ' '.join(cmd))\n", + " subprocess.check_call(cmd)\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium', 'pybullet']\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " pins = compat_pins()\n", + " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", + " if pin_args:\n", + " print('Compatibility pins:', ', '.join(pin_args))\n", + " else:\n", + " print('Compatibility pins: none')\n", + "\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib gymnasium pybullet \"pandas==2.2.2\" \"rich==13.9.4\"\n", - " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " pip_install(BASE_PACKAGES + pin_args)\n", + " pip_install([ENGIOPT_GIT] + pin_args)\n", + "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " !pip install torch torchvision \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " pip_install(['torch', 'torchvision'] + pin_args)\n", + "\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index 1fea9bf..e5faf1a 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -73,14 +73,61 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", + "import importlib.metadata as md\n", + "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", + "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", + "LOCKED_VERSIONS = {\n", + " 'pandas': '2.2.2',\n", + " 'rich': '13.9.4',\n", + "}\n", + "\n", + "\n", + "def installed_version(pkg: str):\n", + " try:\n", + " return md.version(pkg)\n", + " except Exception:\n", + " return None\n", + "\n", + "\n", + "def compat_pins() -> dict[str, str]:\n", + " if COLAB_COMPAT_MODE == 'locked':\n", + " return dict(LOCKED_VERSIONS)\n", + " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", + " pins = {}\n", + " for pkg in LOCKED_VERSIONS:\n", + " v = installed_version(pkg)\n", + " if v is not None:\n", + " pins[pkg] = v\n", + " return pins\n", + " return {}\n", + "\n", + "\n", + "def pip_install(packages: list[str]):\n", + " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", + " print('Running:', ' '.join(cmd))\n", + " subprocess.check_call(cmd)\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " pins = compat_pins()\n", + " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", + " if pin_args:\n", + " print('Compatibility pins:', ', '.join(pin_args))\n", + " else:\n", + " print('Compatibility pins: none')\n", + "\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib seaborn \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " pip_install(BASE_PACKAGES + pin_args)\n", + "\n", + " try:\n", + " import torch # noqa: F401\n", + " except Exception:\n", + " pip_install(['torch', 'torchvision'] + pin_args)\n", + "\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 35dfa0e..fab88f8 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -52,16 +52,63 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", + "import importlib.metadata as md\n", + "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", + "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", + "LOCKED_VERSIONS = {\n", + " 'pandas': '2.2.2',\n", + " 'rich': '13.9.4',\n", + "}\n", + "\n", + "\n", + "def installed_version(pkg: str):\n", + " try:\n", + " return md.version(pkg)\n", + " except Exception:\n", + " return None\n", + "\n", + "\n", + "def compat_pins() -> dict[str, str]:\n", + " if COLAB_COMPAT_MODE == 'locked':\n", + " return dict(LOCKED_VERSIONS)\n", + " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", + " pins = {}\n", + " for pkg in LOCKED_VERSIONS:\n", + " v = installed_version(pkg)\n", + " if v is not None:\n", + " pins[pkg] = v\n", + " return pins\n", + " return {}\n", + "\n", + "\n", + "def pip_install(packages: list[str]):\n", + " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", + " print('Running:', ' '.join(cmd))\n", + " subprocess.check_call(cmd)\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " pins = compat_pins()\n", + " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", + " if pin_args:\n", + " print('Compatibility pins:', ', '.join(pin_args))\n", + " else:\n", + " print('Compatibility pins: none')\n", + "\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas==2.2.2\" tqdm tyro wandb \"rich==13.9.4\"\n", - " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " pip_install(BASE_PACKAGES + pin_args)\n", + " pip_install([ENGIOPT_GIT] + pin_args)\n", + "\n", + " try:\n", + " import torch # noqa: F401\n", + " except Exception:\n", + " pip_install(['torch', 'torchvision'] + pin_args)\n", + "\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index b1d4570..4c35f8a 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -51,16 +51,63 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", + "import importlib.metadata as md\n", + "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", + "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", + "LOCKED_VERSIONS = {\n", + " 'pandas': '2.2.2',\n", + " 'rich': '13.9.4',\n", + "}\n", + "\n", + "\n", + "def installed_version(pkg: str):\n", + " try:\n", + " return md.version(pkg)\n", + " except Exception:\n", + " return None\n", + "\n", + "\n", + "def compat_pins() -> dict[str, str]:\n", + " if COLAB_COMPAT_MODE == 'locked':\n", + " return dict(LOCKED_VERSIONS)\n", + " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", + " pins = {}\n", + " for pkg in LOCKED_VERSIONS:\n", + " v = installed_version(pkg)\n", + " if v is not None:\n", + " pins[pkg] = v\n", + " return pins\n", + " return {}\n", + "\n", + "\n", + "def pip_install(packages: list[str]):\n", + " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", + " print('Running:', ' '.join(cmd))\n", + " subprocess.check_call(cmd)\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " pins = compat_pins()\n", + " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", + " if pin_args:\n", + " print('Compatibility pins:', ', '.join(pin_args))\n", + " else:\n", + " print('Compatibility pins: none')\n", + "\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] sqlitedict torch torchvision matplotlib \"pandas==2.2.2\" tqdm tyro wandb \"rich==13.9.4\"\n", - " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " pip_install(BASE_PACKAGES + pin_args)\n", + " pip_install([ENGIOPT_GIT] + pin_args)\n", + "\n", + " try:\n", + " import torch # noqa: F401\n", + " except Exception:\n", + " pip_install(['torch', 'torchvision'] + pin_args)\n", + "\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 5145889..5e6a908 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -61,20 +61,63 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", + "import importlib.metadata as md\n", + "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", + "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", + "LOCKED_VERSIONS = {\n", + " 'pandas': '2.2.2',\n", + " 'rich': '13.9.4',\n", + "}\n", + "\n", + "\n", + "def installed_version(pkg: str):\n", + " try:\n", + " return md.version(pkg)\n", + " except Exception:\n", + " return None\n", + "\n", + "\n", + "def compat_pins() -> dict[str, str]:\n", + " if COLAB_COMPAT_MODE == 'locked':\n", + " return dict(LOCKED_VERSIONS)\n", + " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", + " pins = {}\n", + " for pkg in LOCKED_VERSIONS:\n", + " v = installed_version(pkg)\n", + " if v is not None:\n", + " pins[pkg] = v\n", + " return pins\n", + " return {}\n", + "\n", + "\n", + "def pip_install(packages: list[str]):\n", + " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", + " print('Running:', ' '.join(cmd))\n", + " subprocess.check_call(cmd)\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium', 'pybullet']\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " pins = compat_pins()\n", + " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", + " if pin_args:\n", + " print('Compatibility pins:', ', '.join(pin_args))\n", + " else:\n", + " print('Compatibility pins: none')\n", + "\n", " print('Installing dependencies...')\n", - " !pip install engibench[beams2d] matplotlib gymnasium pybullet \"pandas==2.2.2\" \"rich==13.9.4\"\n", - " !pip install {ENGIOPT_GIT} \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " pip_install(BASE_PACKAGES + pin_args)\n", + " pip_install([ENGIOPT_GIT] + pin_args)\n", + "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " !pip install torch torchvision \"pandas==2.2.2\" \"rich==13.9.4\"\n", + " pip_install(['torch', 'torchvision'] + pin_args)\n", + "\n", " print('Dependency install complete.')\n", "else:\n", " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" From aac9cbb65939bb7f5d62facef24c7d464062517e Mon Sep 17 00:00:00 2001 From: Soheyl Date: Mon, 16 Mar 2026 12:59:54 +0100 Subject: [PATCH 32/37] Drop pandas pin from Colab auto-compat bootstrap --- workshops/dcc26/participant/00_setup_api_warmup.ipynb | 6 +++++- workshops/dcc26/participant/01_train_generate.ipynb | 8 ++++++-- workshops/dcc26/participant/02_evaluate_metrics.ipynb | 8 ++++++-- .../dcc26/participant/03_add_new_problem_scaffold.ipynb | 6 +++++- workshops/dcc26/solutions/00_setup_api_warmup.ipynb | 6 +++++- workshops/dcc26/solutions/01_train_generate.ipynb | 8 ++++++-- workshops/dcc26/solutions/02_evaluate_metrics.ipynb | 8 ++++++-- .../dcc26/solutions/03_add_new_problem_scaffold.ipynb | 6 +++++- 8 files changed, 44 insertions(+), 12 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 180054d..845d700 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -85,7 +85,7 @@ "FORCE_INSTALL = False # Set True to force install outside Colab\n", "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", "LOCKED_VERSIONS = {\n", - " 'pandas': '2.2.2',\n", + " # Keep rich pinned for Colab bigframes compatibility.\n", " 'rich': '13.9.4',\n", "}\n", "\n", @@ -117,6 +117,10 @@ "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " if IN_COLAB:\n", + " colab_pandas = installed_version('pandas')\n", + " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", + "\n", " pins = compat_pins()\n", " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", " if pin_args:\n", diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index cfe350d..912ca5c 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -60,7 +60,7 @@ "FORCE_INSTALL = False # Set True to force install outside Colab\n", "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", "LOCKED_VERSIONS = {\n", - " 'pandas': '2.2.2',\n", + " # Keep rich pinned for Colab bigframes compatibility.\n", " 'rich': '13.9.4',\n", "}\n", "\n", @@ -89,10 +89,14 @@ " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", " print('Running:', ' '.join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'matplotlib', 'tqdm', 'tyro', 'wandb']\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " if IN_COLAB:\n", + " colab_pandas = installed_version('pandas')\n", + " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", + "\n", " pins = compat_pins()\n", " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", " if pin_args:\n", diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index f0c2ebd..284dae8 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -59,7 +59,7 @@ "FORCE_INSTALL = False # Set True to force install outside Colab\n", "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", "LOCKED_VERSIONS = {\n", - " 'pandas': '2.2.2',\n", + " # Keep rich pinned for Colab bigframes compatibility.\n", " 'rich': '13.9.4',\n", "}\n", "\n", @@ -88,10 +88,14 @@ " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", " print('Running:', ' '.join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'matplotlib', 'tqdm', 'tyro', 'wandb']\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " if IN_COLAB:\n", + " colab_pandas = installed_version('pandas')\n", + " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", + "\n", " pins = compat_pins()\n", " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", " if pin_args:\n", diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index d66d2cb..1d10246 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -69,7 +69,7 @@ "FORCE_INSTALL = False # Set True to force install outside Colab\n", "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", "LOCKED_VERSIONS = {\n", - " 'pandas': '2.2.2',\n", + " # Keep rich pinned for Colab bigframes compatibility.\n", " 'rich': '13.9.4',\n", "}\n", "\n", @@ -102,6 +102,10 @@ "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " if IN_COLAB:\n", + " colab_pandas = installed_version('pandas')\n", + " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", + "\n", " pins = compat_pins()\n", " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", " if pin_args:\n", diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index e5faf1a..fafac7c 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -81,7 +81,7 @@ "FORCE_INSTALL = False # Set True to force install outside Colab\n", "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", "LOCKED_VERSIONS = {\n", - " 'pandas': '2.2.2',\n", + " # Keep rich pinned for Colab bigframes compatibility.\n", " 'rich': '13.9.4',\n", "}\n", "\n", @@ -113,6 +113,10 @@ "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " if IN_COLAB:\n", + " colab_pandas = installed_version('pandas')\n", + " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", + "\n", " pins = compat_pins()\n", " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", " if pin_args:\n", diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index fab88f8..7d4407e 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -60,7 +60,7 @@ "FORCE_INSTALL = False # Set True to force install outside Colab\n", "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", "LOCKED_VERSIONS = {\n", - " 'pandas': '2.2.2',\n", + " # Keep rich pinned for Colab bigframes compatibility.\n", " 'rich': '13.9.4',\n", "}\n", "\n", @@ -89,10 +89,14 @@ " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", " print('Running:', ' '.join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'matplotlib', 'tqdm', 'tyro', 'wandb']\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " if IN_COLAB:\n", + " colab_pandas = installed_version('pandas')\n", + " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", + "\n", " pins = compat_pins()\n", " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", " if pin_args:\n", diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 4c35f8a..f2d3f6c 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -59,7 +59,7 @@ "FORCE_INSTALL = False # Set True to force install outside Colab\n", "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", "LOCKED_VERSIONS = {\n", - " 'pandas': '2.2.2',\n", + " # Keep rich pinned for Colab bigframes compatibility.\n", " 'rich': '13.9.4',\n", "}\n", "\n", @@ -88,10 +88,14 @@ " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", " print('Running:', ' '.join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'torch', 'torchvision', 'matplotlib', 'pandas', 'tqdm', 'tyro', 'wandb']\n", + "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'matplotlib', 'tqdm', 'tyro', 'wandb']\n", "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " if IN_COLAB:\n", + " colab_pandas = installed_version('pandas')\n", + " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", + "\n", " pins = compat_pins()\n", " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", " if pin_args:\n", diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 5e6a908..28eea70 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -69,7 +69,7 @@ "FORCE_INSTALL = False # Set True to force install outside Colab\n", "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", "LOCKED_VERSIONS = {\n", - " 'pandas': '2.2.2',\n", + " # Keep rich pinned for Colab bigframes compatibility.\n", " 'rich': '13.9.4',\n", "}\n", "\n", @@ -102,6 +102,10 @@ "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", + " if IN_COLAB:\n", + " colab_pandas = installed_version('pandas')\n", + " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", + "\n", " pins = compat_pins()\n", " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", " if pin_args:\n", From e73c30d2a9973fdac586242bae6ca7a4deb2cfab Mon Sep 17 00:00:00 2001 From: Soheyl Date: Mon, 16 Mar 2026 13:14:17 +0100 Subject: [PATCH 33/37] Simplify Colab bootstrap by removing version pin detection --- .../participant/00_setup_api_warmup.ipynb | 41 +----------------- .../dcc26/participant/01_train_generate.ipynb | 43 ++----------------- .../participant/02_evaluate_metrics.ipynb | 43 ++----------------- .../03_add_new_problem_scaffold.ipynb | 43 ++----------------- .../dcc26/solutions/00_setup_api_warmup.ipynb | 41 +----------------- .../dcc26/solutions/01_train_generate.ipynb | 43 ++----------------- .../dcc26/solutions/02_evaluate_metrics.ipynb | 43 ++----------------- .../03_add_new_problem_scaffold.ipynb | 43 ++----------------- 8 files changed, 22 insertions(+), 318 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 845d700..4b150c8 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -77,37 +77,11 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import importlib.metadata as md\n", "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", - "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", - "LOCKED_VERSIONS = {\n", - " # Keep rich pinned for Colab bigframes compatibility.\n", - " 'rich': '13.9.4',\n", - "}\n", - "\n", - "\n", - "def installed_version(pkg: str):\n", - " try:\n", - " return md.version(pkg)\n", - " except Exception:\n", - " return None\n", - "\n", - "\n", - "def compat_pins() -> dict[str, str]:\n", - " if COLAB_COMPAT_MODE == 'locked':\n", - " return dict(LOCKED_VERSIONS)\n", - " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", - " pins = {}\n", - " for pkg in LOCKED_VERSIONS:\n", - " v = installed_version(pkg)\n", - " if v is not None:\n", - " pins[pkg] = v\n", - " return pins\n", - " return {}\n", "\n", "\n", "def pip_install(packages: list[str]):\n", @@ -117,24 +91,13 @@ "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " if IN_COLAB:\n", - " colab_pandas = installed_version('pandas')\n", - " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", - "\n", - " pins = compat_pins()\n", - " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", - " if pin_args:\n", - " print('Compatibility pins:', ', '.join(pin_args))\n", - " else:\n", - " print('Compatibility pins: none')\n", - "\n", " print('Installing dependencies...')\n", - " pip_install(BASE_PACKAGES + pin_args)\n", + " pip_install(BASE_PACKAGES)\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'] + pin_args)\n", + " pip_install(['torch', 'torchvision'])\n", "\n", " print('Dependency install complete.')\n", "else:\n", diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 912ca5c..7e32e50 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -52,37 +52,11 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import importlib.metadata as md\n", "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", - "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", - "LOCKED_VERSIONS = {\n", - " # Keep rich pinned for Colab bigframes compatibility.\n", - " 'rich': '13.9.4',\n", - "}\n", - "\n", - "\n", - "def installed_version(pkg: str):\n", - " try:\n", - " return md.version(pkg)\n", - " except Exception:\n", - " return None\n", - "\n", - "\n", - "def compat_pins() -> dict[str, str]:\n", - " if COLAB_COMPAT_MODE == 'locked':\n", - " return dict(LOCKED_VERSIONS)\n", - " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", - " pins = {}\n", - " for pkg in LOCKED_VERSIONS:\n", - " v = installed_version(pkg)\n", - " if v is not None:\n", - " pins[pkg] = v\n", - " return pins\n", - " return {}\n", "\n", "\n", "def pip_install(packages: list[str]):\n", @@ -93,25 +67,14 @@ "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " if IN_COLAB:\n", - " colab_pandas = installed_version('pandas')\n", - " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", - "\n", - " pins = compat_pins()\n", - " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", - " if pin_args:\n", - " print('Compatibility pins:', ', '.join(pin_args))\n", - " else:\n", - " print('Compatibility pins: none')\n", - "\n", " print('Installing dependencies...')\n", - " pip_install(BASE_PACKAGES + pin_args)\n", - " pip_install([ENGIOPT_GIT] + pin_args)\n", + " pip_install(BASE_PACKAGES)\n", + " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'] + pin_args)\n", + " pip_install(['torch', 'torchvision'])\n", "\n", " print('Dependency install complete.')\n", "else:\n", diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index 284dae8..11474d7 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -51,37 +51,11 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import importlib.metadata as md\n", "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", - "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", - "LOCKED_VERSIONS = {\n", - " # Keep rich pinned for Colab bigframes compatibility.\n", - " 'rich': '13.9.4',\n", - "}\n", - "\n", - "\n", - "def installed_version(pkg: str):\n", - " try:\n", - " return md.version(pkg)\n", - " except Exception:\n", - " return None\n", - "\n", - "\n", - "def compat_pins() -> dict[str, str]:\n", - " if COLAB_COMPAT_MODE == 'locked':\n", - " return dict(LOCKED_VERSIONS)\n", - " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", - " pins = {}\n", - " for pkg in LOCKED_VERSIONS:\n", - " v = installed_version(pkg)\n", - " if v is not None:\n", - " pins[pkg] = v\n", - " return pins\n", - " return {}\n", "\n", "\n", "def pip_install(packages: list[str]):\n", @@ -92,25 +66,14 @@ "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " if IN_COLAB:\n", - " colab_pandas = installed_version('pandas')\n", - " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", - "\n", - " pins = compat_pins()\n", - " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", - " if pin_args:\n", - " print('Compatibility pins:', ', '.join(pin_args))\n", - " else:\n", - " print('Compatibility pins: none')\n", - "\n", " print('Installing dependencies...')\n", - " pip_install(BASE_PACKAGES + pin_args)\n", - " pip_install([ENGIOPT_GIT] + pin_args)\n", + " pip_install(BASE_PACKAGES)\n", + " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'] + pin_args)\n", + " pip_install(['torch', 'torchvision'])\n", "\n", " print('Dependency install complete.')\n", "else:\n", diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 1d10246..9947fb5 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -61,37 +61,11 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import importlib.metadata as md\n", "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", - "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", - "LOCKED_VERSIONS = {\n", - " # Keep rich pinned for Colab bigframes compatibility.\n", - " 'rich': '13.9.4',\n", - "}\n", - "\n", - "\n", - "def installed_version(pkg: str):\n", - " try:\n", - " return md.version(pkg)\n", - " except Exception:\n", - " return None\n", - "\n", - "\n", - "def compat_pins() -> dict[str, str]:\n", - " if COLAB_COMPAT_MODE == 'locked':\n", - " return dict(LOCKED_VERSIONS)\n", - " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", - " pins = {}\n", - " for pkg in LOCKED_VERSIONS:\n", - " v = installed_version(pkg)\n", - " if v is not None:\n", - " pins[pkg] = v\n", - " return pins\n", - " return {}\n", "\n", "\n", "def pip_install(packages: list[str]):\n", @@ -102,25 +76,14 @@ "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " if IN_COLAB:\n", - " colab_pandas = installed_version('pandas')\n", - " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", - "\n", - " pins = compat_pins()\n", - " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", - " if pin_args:\n", - " print('Compatibility pins:', ', '.join(pin_args))\n", - " else:\n", - " print('Compatibility pins: none')\n", - "\n", " print('Installing dependencies...')\n", - " pip_install(BASE_PACKAGES + pin_args)\n", - " pip_install([ENGIOPT_GIT] + pin_args)\n", + " pip_install(BASE_PACKAGES)\n", + " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'] + pin_args)\n", + " pip_install(['torch', 'torchvision'])\n", "\n", " print('Dependency install complete.')\n", "else:\n", diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index fafac7c..403658e 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -73,37 +73,11 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import importlib.metadata as md\n", "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", - "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", - "LOCKED_VERSIONS = {\n", - " # Keep rich pinned for Colab bigframes compatibility.\n", - " 'rich': '13.9.4',\n", - "}\n", - "\n", - "\n", - "def installed_version(pkg: str):\n", - " try:\n", - " return md.version(pkg)\n", - " except Exception:\n", - " return None\n", - "\n", - "\n", - "def compat_pins() -> dict[str, str]:\n", - " if COLAB_COMPAT_MODE == 'locked':\n", - " return dict(LOCKED_VERSIONS)\n", - " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", - " pins = {}\n", - " for pkg in LOCKED_VERSIONS:\n", - " v = installed_version(pkg)\n", - " if v is not None:\n", - " pins[pkg] = v\n", - " return pins\n", - " return {}\n", "\n", "\n", "def pip_install(packages: list[str]):\n", @@ -113,24 +87,13 @@ "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " if IN_COLAB:\n", - " colab_pandas = installed_version('pandas')\n", - " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", - "\n", - " pins = compat_pins()\n", - " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", - " if pin_args:\n", - " print('Compatibility pins:', ', '.join(pin_args))\n", - " else:\n", - " print('Compatibility pins: none')\n", - "\n", " print('Installing dependencies...')\n", - " pip_install(BASE_PACKAGES + pin_args)\n", + " pip_install(BASE_PACKAGES)\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'] + pin_args)\n", + " pip_install(['torch', 'torchvision'])\n", "\n", " print('Dependency install complete.')\n", "else:\n", diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 7d4407e..17240ec 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -52,37 +52,11 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import importlib.metadata as md\n", "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", - "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", - "LOCKED_VERSIONS = {\n", - " # Keep rich pinned for Colab bigframes compatibility.\n", - " 'rich': '13.9.4',\n", - "}\n", - "\n", - "\n", - "def installed_version(pkg: str):\n", - " try:\n", - " return md.version(pkg)\n", - " except Exception:\n", - " return None\n", - "\n", - "\n", - "def compat_pins() -> dict[str, str]:\n", - " if COLAB_COMPAT_MODE == 'locked':\n", - " return dict(LOCKED_VERSIONS)\n", - " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", - " pins = {}\n", - " for pkg in LOCKED_VERSIONS:\n", - " v = installed_version(pkg)\n", - " if v is not None:\n", - " pins[pkg] = v\n", - " return pins\n", - " return {}\n", "\n", "\n", "def pip_install(packages: list[str]):\n", @@ -93,25 +67,14 @@ "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " if IN_COLAB:\n", - " colab_pandas = installed_version('pandas')\n", - " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", - "\n", - " pins = compat_pins()\n", - " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", - " if pin_args:\n", - " print('Compatibility pins:', ', '.join(pin_args))\n", - " else:\n", - " print('Compatibility pins: none')\n", - "\n", " print('Installing dependencies...')\n", - " pip_install(BASE_PACKAGES + pin_args)\n", - " pip_install([ENGIOPT_GIT] + pin_args)\n", + " pip_install(BASE_PACKAGES)\n", + " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'] + pin_args)\n", + " pip_install(['torch', 'torchvision'])\n", "\n", " print('Dependency install complete.')\n", "else:\n", diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index f2d3f6c..04322c8 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -51,37 +51,11 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import importlib.metadata as md\n", "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", - "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", - "LOCKED_VERSIONS = {\n", - " # Keep rich pinned for Colab bigframes compatibility.\n", - " 'rich': '13.9.4',\n", - "}\n", - "\n", - "\n", - "def installed_version(pkg: str):\n", - " try:\n", - " return md.version(pkg)\n", - " except Exception:\n", - " return None\n", - "\n", - "\n", - "def compat_pins() -> dict[str, str]:\n", - " if COLAB_COMPAT_MODE == 'locked':\n", - " return dict(LOCKED_VERSIONS)\n", - " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", - " pins = {}\n", - " for pkg in LOCKED_VERSIONS:\n", - " v = installed_version(pkg)\n", - " if v is not None:\n", - " pins[pkg] = v\n", - " return pins\n", - " return {}\n", "\n", "\n", "def pip_install(packages: list[str]):\n", @@ -92,25 +66,14 @@ "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " if IN_COLAB:\n", - " colab_pandas = installed_version('pandas')\n", - " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", - "\n", - " pins = compat_pins()\n", - " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", - " if pin_args:\n", - " print('Compatibility pins:', ', '.join(pin_args))\n", - " else:\n", - " print('Compatibility pins: none')\n", - "\n", " print('Installing dependencies...')\n", - " pip_install(BASE_PACKAGES + pin_args)\n", - " pip_install([ENGIOPT_GIT] + pin_args)\n", + " pip_install(BASE_PACKAGES)\n", + " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'] + pin_args)\n", + " pip_install(['torch', 'torchvision'])\n", "\n", " print('Dependency install complete.')\n", "else:\n", diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 28eea70..372e495 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -61,37 +61,11 @@ "outputs": [], "source": [ "# Colab/local dependency bootstrap\n", - "import importlib.metadata as md\n", "import subprocess\n", "import sys\n", "\n", "IN_COLAB = 'google.colab' in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", - "COLAB_COMPAT_MODE = 'auto' # 'auto' (recommended) or 'locked'\n", - "LOCKED_VERSIONS = {\n", - " # Keep rich pinned for Colab bigframes compatibility.\n", - " 'rich': '13.9.4',\n", - "}\n", - "\n", - "\n", - "def installed_version(pkg: str):\n", - " try:\n", - " return md.version(pkg)\n", - " except Exception:\n", - " return None\n", - "\n", - "\n", - "def compat_pins() -> dict[str, str]:\n", - " if COLAB_COMPAT_MODE == 'locked':\n", - " return dict(LOCKED_VERSIONS)\n", - " if COLAB_COMPAT_MODE == 'auto' and IN_COLAB:\n", - " pins = {}\n", - " for pkg in LOCKED_VERSIONS:\n", - " v = installed_version(pkg)\n", - " if v is not None:\n", - " pins[pkg] = v\n", - " return pins\n", - " return {}\n", "\n", "\n", "def pip_install(packages: list[str]):\n", @@ -102,25 +76,14 @@ "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " if IN_COLAB:\n", - " colab_pandas = installed_version('pandas')\n", - " print(f'Colab pandas before install: {colab_pandas} (EngiBench requires pandas>=2.2.3).')\n", - "\n", - " pins = compat_pins()\n", - " pin_args = [f\"{k}=={v}\" for k, v in pins.items()]\n", - " if pin_args:\n", - " print('Compatibility pins:', ', '.join(pin_args))\n", - " else:\n", - " print('Compatibility pins: none')\n", - "\n", " print('Installing dependencies...')\n", - " pip_install(BASE_PACKAGES + pin_args)\n", - " pip_install([ENGIOPT_GIT] + pin_args)\n", + " pip_install(BASE_PACKAGES)\n", + " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'] + pin_args)\n", + " pip_install(['torch', 'torchvision'])\n", "\n", " print('Dependency install complete.')\n", "else:\n", From 044c7ab5d80ec54596876be449c284e43d3ccb7f Mon Sep 17 00:00:00 2001 From: Soheyl Date: Mon, 16 Mar 2026 13:50:32 +0100 Subject: [PATCH 34/37] Upgrade participant notebooks with guided fill-in scaffolding --- .../participant/00_setup_api_warmup.ipynb | 131 ++++++++++--- .../dcc26/participant/01_train_generate.ipynb | 182 ++++++++++++------ .../participant/02_evaluate_metrics.ipynb | 106 +++++++--- .../03_add_new_problem_scaffold.ipynb | 110 ++++++++--- 4 files changed, 390 insertions(+), 139 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index 4b150c8..b91c242 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -31,7 +31,12 @@ "- implementation second,\n", "- interpretation third.\n", "\n", - "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n", + "\n", + "### Public exercise legend\n", + "- `PUBLIC FILL-IN CELL`: this is the part you edit during the workshop.\n", + "- `CHECKPOINT`: run immediately after your edits; if it fails, fix before moving on.\n", + "- `IF YOU ARE STUCK`: use the hint comments in that same cell (do not jump ahead).\n" ] }, { @@ -142,10 +147,16 @@ "id": "2e7a064e", "metadata": {}, "source": [ - "### Step 2 - Instantiate the benchmark problem (TODO)\n", + "### Step 2 - Instantiate the benchmark problem (PUBLIC FILL-IN)\n", "\n", "Create `Beams2D` and inspect its key fields.\n", - "Checkpoint: you should be able to explain which attributes define the benchmark contract.\n" + "\n", + "What this step teaches:\n", + "- how a benchmark problem defines design space + objectives + conditions,\n", + "- which fields are part of the public contract you must preserve for fair comparison.\n", + "\n", + "Success criteria:\n", + "- you can name each contract field and explain its role.\n" ] }, { @@ -155,18 +166,28 @@ "metadata": {}, "outputs": [], "source": [ - "# TODO 1: instantiate the problem with the global SEED\n", - "# problem = ...\n", - "\n", - "# TODO 2: print these fields\n", - "# - type(problem).__name__\n", - "# - problem.design_space\n", - "# - problem.objectives\n", - "# - problem.conditions\n", - "# - problem.conditions_keys\n", - "# - problem.dataset_id\n", - "\n", - "raise NotImplementedError('Complete TODO 1/2 in this cell')" + "# PUBLIC FILL-IN CELL 00-A\n", + "# Goal: instantiate the benchmark problem and inspect the full contract.\n", + "\n", + "# START FILL ---------------------------------------------------------------\n", + "problem = None # Example: Beams2D(seed=SEED)\n", + "# END FILL -----------------------------------------------------------------\n", + "\n", + "if problem is None:\n", + " raise RuntimeError('Set `problem` before running this cell (example: Beams2D(seed=SEED)).')\n", + "\n", + "print('Problem class:', type(problem).__name__)\n", + "print('Design space:', problem.design_space)\n", + "print('Objectives:', problem.objectives)\n", + "print('Conditions instance:', problem.conditions)\n", + "print('Condition keys:', problem.conditions_keys)\n", + "print('Dataset ID:', problem.dataset_id)\n", + "\n", + "# CHECKPOINT\n", + "assert hasattr(problem, 'design_space'), 'Problem is missing design_space'\n", + "assert hasattr(problem, 'objectives'), 'Problem is missing objectives'\n", + "assert len(problem.conditions_keys) > 0, 'conditions_keys should not be empty'\n", + "print('Checkpoint passed: problem contract is visible and ready.')\n" ] }, { @@ -174,10 +195,17 @@ "id": "2d0d6faf", "metadata": {}, "source": [ - "### Step 3 - Inspect dataset structure (TODO)\n", + "### Step 3 - Inspect dataset structure (PUBLIC FILL-IN)\n", + "\n", + "Load one train sample and inspect keys + shapes.\n", + "\n", + "What this step teaches:\n", + "- how conditions and designs are stored in the benchmark dataset,\n", + "- what must be serialized later when handing off artifacts between notebooks.\n", "\n", - "Load one train/test sample and inspect keys and shapes.\n", - "Checkpoint: confirm condition variables and design representation are explicit.\n" + "Success criteria:\n", + "- `design` has the expected spatial shape,\n", + "- `config` contains exactly the condition keys used by the problem.\n" ] }, { @@ -187,15 +215,33 @@ "metadata": {}, "outputs": [], "source": [ - "# TODO 3: load dataset and extract one sample\n", - "# dataset = problem.dataset\n", - "# sample_idx = 0\n", - "# design = ...\n", - "# config = ... # dict over problem.conditions_keys\n", + "# PUBLIC FILL-IN CELL 00-B\n", + "# Goal: inspect one training sample and build a valid config dictionary.\n", "\n", - "# print dataset summary and sample shapes/values\n", + "# START FILL ---------------------------------------------------------------\n", + "dataset = None # Example: problem.dataset\n", + "sample_idx = 0\n", + "# design = ... # np.array from dataset['train']['optimal_design'][sample_idx]\n", + "# config = ... # dict over problem.conditions_keys\n", + "# END FILL -----------------------------------------------------------------\n", "\n", - "raise NotImplementedError('Complete TODO 3 in this cell')" + "if dataset is None:\n", + " raise RuntimeError('Set `dataset = problem.dataset` before running.')\n", + "if 'design' not in locals() or 'config' not in locals():\n", + " raise RuntimeError('Define both `design` and `config` in the START FILL section.')\n", + "\n", + "print(dataset)\n", + "print('sample_idx:', sample_idx)\n", + "print('design shape:', np.array(design).shape)\n", + "print('config:', config)\n", + "\n", + "# CHECKPOINT\n", + "assert tuple(np.array(design).shape) == tuple(problem.design_space.shape), (\n", + " f'design shape mismatch: expected {problem.design_space.shape}, got {np.array(design).shape}'\n", + ")\n", + "missing = [k for k in problem.conditions_keys if k not in config]\n", + "assert not missing, f'config missing condition keys: {missing}'\n", + "print('Checkpoint passed: dataset sample + config are valid.')\n" ] }, { @@ -226,10 +272,16 @@ "id": "e7201d50", "metadata": {}, "source": [ - "### Step 5 - Test constraint semantics (TODO)\n", + "### Step 5 - Test constraint semantics (PUBLIC FILL-IN)\n", "\n", "Run a deliberate mismatch case.\n", - "Checkpoint: verify the violation output is understandable and actionable.\n" + "\n", + "Why this matters:\n", + "- robust benchmarking requires transparent failure modes,\n", + "- you should be able to explain *why* a design/config pair is invalid.\n", + "\n", + "Success criteria:\n", + "- you can trigger and inspect at least one violation message.\n" ] }, { @@ -239,13 +291,28 @@ "metadata": {}, "outputs": [], "source": [ - "# TODO 4: run one explicit constraint check with an intentionally mismatched volfrac\n", - "# bad_config = dict(config)\n", - "# bad_config['volfrac'] = 0.2\n", + "# PUBLIC FILL-IN CELL 00-C\n", + "# Goal: force a constraint mismatch and inspect violation diagnostics.\n", + "\n", + "# START FILL ---------------------------------------------------------------\n", + "bad_config = dict(config)\n", + "# bad_config['volfrac'] = ...\n", "# violations = ...\n", - "# print(len(violations)); print(violations) if any\n", + "# END FILL -----------------------------------------------------------------\n", + "\n", + "if 'violations' not in locals():\n", + " raise RuntimeError('Define `violations` in the START FILL section.')\n", + "\n", + "print('Violation count:', len(violations))\n", + "if violations:\n", + " for i, v in enumerate(violations[:5]):\n", + " print(f' [{i}]', v)\n", + "else:\n", + " print('No violations found. Try a more aggressive volfrac mismatch.')\n", "\n", - "raise NotImplementedError('Complete TODO 4 in this cell')" + "# CHECKPOINT\n", + "assert hasattr(violations, '__len__'), 'violations should be a sized collection'\n", + "print('Checkpoint passed: constraint semantics inspected.')\n" ] }, { diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 7e32e50..108d7a2 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -30,7 +30,12 @@ "- implementation second,\n", "- interpretation third.\n", "\n", - "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n", + "\n", + "### Public exercise legend\n", + "- `PUBLIC FILL-IN CELL`: edit this cell directly.\n", + "- `CHECKPOINT`: run and verify before continuing.\n", + "- `IF YOU ARE STUCK`: use hint comments in the same cell.\n" ] }, { @@ -218,10 +223,13 @@ "id": "5da6a98e", "metadata": {}, "source": [ - "### Step 3 - Implement model setup (TODO)\n", + "### Step 3 - Implement model setup (PUBLIC FILL-IN)\n", "\n", "Instantiate generator, optimizer, and loss exactly once.\n", - "Checkpoint: noise and condition tensors must align with model input dimensions.\n" + "\n", + "Success criteria:\n", + "- noise tensor shape is `(batch, LATENT_DIM)`,\n", + "- model output shape matches `problem.design_space.shape`.\n" ] }, { @@ -231,16 +239,32 @@ "metadata": {}, "outputs": [], "source": [ - "# TODO 1: Instantiate model, optimizer, loss, and noise sampler.\n", - "# Required objects:\n", - "# - model (EngiOptCGAN2DGenerator)\n", - "# - optimizer (Adam)\n", - "# - criterion (MSELoss)\n", - "# - sample_noise(batch_size)\n", - "\n", - "raise NotImplementedError('Complete TODO 1 model setup')\n", - "\n", - "# Completion check: calling `sample_noise(4)` should return shape (4, LATENT_DIM).\n" + "# PUBLIC FILL-IN CELL 01-A\n", + "# Goal: set up the EngiOpt generator + training primitives.\n", + "\n", + "# START FILL ---------------------------------------------------------------\n", + "model = None\n", + "optimizer = None\n", + "criterion = None\n", + "\n", + "def sample_noise(batch_size: int) -> th.Tensor:\n", + " # Return standard normal latent vectors on DEVICE.\n", + " raise NotImplementedError('Implement sample_noise')\n", + "# END FILL -----------------------------------------------------------------\n", + "\n", + "if model is None or optimizer is None or criterion is None:\n", + " raise RuntimeError('Define model, optimizer, and criterion in the START FILL block.')\n", + "\n", + "# CHECKPOINT: validate latent shape and forward-pass shape\n", + "z_probe = sample_noise(4)\n", + "assert tuple(z_probe.shape) == (4, LATENT_DIM), f'Expected (4, {LATENT_DIM}), got {tuple(z_probe.shape)}'\n", + "cond_probe = th.tensor(conds_np[:4], dtype=th.float32, device=DEVICE)\n", + "with th.no_grad():\n", + " pred_probe = model(z_probe, cond_probe)\n", + "assert tuple(pred_probe.shape[1:]) == tuple(problem.design_space.shape), (\n", + " f'Output shape mismatch: expected tail {problem.design_space.shape}, got {tuple(pred_probe.shape[1:])}'\n", + ")\n", + "print('Checkpoint passed: model setup is consistent with problem representation.')\n" ] }, { @@ -248,10 +272,13 @@ "id": "78d28002", "metadata": {}, "source": [ - "### Step 4 - Implement train/load logic (TODO)\n", + "### Step 4 - Implement train/load logic (PUBLIC FILL-IN)\n", "\n", "Track loss per epoch and persist checkpoint/history outputs.\n", - "Checkpoint: you can reload and run generation without retraining.\n" + "\n", + "Success criteria:\n", + "- training path writes checkpoint + history + curve,\n", + "- load path restores a checkpoint without retraining.\n" ] }, { @@ -265,21 +292,34 @@ "EPOCHS = 8\n", "BATCH_SIZE = 64\n", "\n", - "# TODO 2: Train or load checkpoint.\n", - "# Requirements:\n", - "# - if TRAIN_FROM_SCRATCH:\n", - "# 1) create DataLoader from (conds_np, targets_np)\n", - "# 2) run epoch loop and optimize reconstruction loss\n", - "# 3) collect train_losses list\n", - "# 4) save checkpoint to CKPT_PATH\n", - "# 5) save training history CSV to HISTORY_PATH\n", - "# 6) save training curve figure to TRAIN_CURVE_PATH\n", - "# - elif CKPT_PATH exists: load it\n", - "# - else: raise FileNotFoundError\n", - "\n", - "raise NotImplementedError('Complete TODO 2 training/loading')\n", - "\n", - "# Completion check: after training, CKPT_PATH and HISTORY_PATH should exist.\n" + "# PUBLIC FILL-IN CELL 01-B\n", + "# Goal: train quickly for workshop runtime or load an existing checkpoint.\n", + "\n", + "train_losses = []\n", + "\n", + "if TRAIN_FROM_SCRATCH:\n", + " # START FILL -----------------------------------------------------------\n", + " # 1) Build DataLoader from conds_np + targets_np\n", + " # 2) Run epoch loop with model.train()\n", + " # 3) For each batch: predict, compute loss, backward, optimizer step\n", + " # 4) Append epoch-average loss to train_losses\n", + " # 5) Save checkpoint to CKPT_PATH\n", + " # 6) Save history CSV to HISTORY_PATH\n", + " # 7) Save training curve figure to TRAIN_CURVE_PATH\n", + " raise NotImplementedError('Implement TRAIN_FROM_SCRATCH branch')\n", + " # END FILL -------------------------------------------------------------\n", + "elif CKPT_PATH.exists():\n", + " # START FILL -----------------------------------------------------------\n", + " # Load checkpoint into model and, if available, load history CSV.\n", + " raise NotImplementedError('Implement checkpoint load branch')\n", + " # END FILL -------------------------------------------------------------\n", + "else:\n", + " raise FileNotFoundError(f'Checkpoint not found at {CKPT_PATH}. Train first or provide checkpoint.')\n", + "\n", + "# CHECKPOINT\n", + "assert CKPT_PATH.exists(), f'Missing checkpoint: {CKPT_PATH}'\n", + "assert HISTORY_PATH.exists(), f'Missing history CSV: {HISTORY_PATH}'\n", + "print('Checkpoint passed: train/load artifacts are ready for generation step.')\n" ] }, { @@ -287,10 +327,13 @@ "id": "9c006d13", "metadata": {}, "source": [ - "### Step 5 - Implement generation logic (TODO)\n", + "### Step 5 - Implement generation logic (PUBLIC FILL-IN)\n", "\n", "Generate conditioned designs on held-out test conditions.\n", - "Checkpoint: generated and baseline arrays are shape-compatible.\n" + "\n", + "Success criteria:\n", + "- generated and baseline arrays are shape-compatible,\n", + "- condition records are JSON-serializable and aligned with samples.\n" ] }, { @@ -300,17 +343,29 @@ "metadata": {}, "outputs": [], "source": [ - "# TODO 3: Generate designs and prepare condition records.\n", - "# Requirements:\n", - "# - sample N_SAMPLES from test dataset\n", - "# - run model(sample_noise(...), condition_tensor)\n", - "# - map tanh output back to [0, 1]\n", - "# - create:\n", - "# gen_designs, baseline_designs, test_conds, conditions_records\n", - "\n", - "raise NotImplementedError('Complete TODO 3 generation')\n", - "\n", - "# Completion check: `gen_designs.shape == baseline_designs.shape` and len(conditions_records)==N_SAMPLES.\n" + "# PUBLIC FILL-IN CELL 01-C\n", + "# Goal: create generated designs + baseline designs + condition records.\n", + "\n", + "N_SAMPLES = 24\n", + "\n", + "# START FILL ---------------------------------------------------------------\n", + "# Suggested sequence:\n", + "# 1) sample indices from test_ds\n", + "# 2) build test_conds and baseline_designs\n", + "# 3) run model in eval/no_grad with sample_noise\n", + "# 4) map tanh output [-1,1] -> [0,1] and clip\n", + "# 5) build `conditions_records` as list[dict]\n", + "raise NotImplementedError('Implement generation block')\n", + "# END FILL -----------------------------------------------------------------\n", + "\n", + "# CHECKPOINT\n", + "assert 'gen_designs' in locals(), 'Define gen_designs'\n", + "assert 'baseline_designs' in locals(), 'Define baseline_designs'\n", + "assert 'test_conds' in locals(), 'Define test_conds'\n", + "assert 'conditions_records' in locals(), 'Define conditions_records'\n", + "assert gen_designs.shape == baseline_designs.shape, 'Generated and baseline shapes must match'\n", + "assert len(conditions_records) == gen_designs.shape[0], 'conditions_records length mismatch'\n", + "print('Checkpoint passed: generation outputs are valid and aligned.')\n" ] }, { @@ -318,10 +373,15 @@ "id": "d0020828", "metadata": {}, "source": [ - "### Step 6 - Implement artifact export (TODO)\n", + "### Step 6 - Implement artifact export (PUBLIC FILL-IN)\n", "\n", "Notebook 02 expects these files as a strict handoff contract.\n", - "Treat artifact naming/format as part of the benchmark interface.\n" + "Treat artifact naming and file format as part of benchmark reproducibility.\n", + "\n", + "Required files:\n", + "- `generated_designs.npy`\n", + "- `baseline_designs.npy`\n", + "- `conditions.json`\n" ] }, { @@ -331,17 +391,29 @@ "metadata": {}, "outputs": [], "source": [ - "# TODO 4: Save Notebook 02 artifacts and (optionally) W&B artifact.\n", - "# Required files:\n", - "# - generated_designs.npy\n", - "# - baseline_designs.npy\n", - "# - conditions.json\n", - "# Recommended extras:\n", + "# PUBLIC FILL-IN CELL 01-D\n", + "# Goal: export Notebook 02 handoff artifacts (plus optional extras).\n", + "\n", + "# START FILL ---------------------------------------------------------------\n", + "# Required exports:\n", + "# - np.save(ARTIFACT_DIR / 'generated_designs.npy', gen_designs)\n", + "# - np.save(ARTIFACT_DIR / 'baseline_designs.npy', baseline_designs)\n", + "# - json dump of conditions_records -> conditions.json\n", + "# Recommended exports:\n", "# - checkpoint (.pt), training_history.csv, training_curve.png\n", - "\n", - "raise NotImplementedError('Complete TODO 4 artifact export')\n", - "\n", - "# Completion check: printed file paths exist and can be loaded by Notebook 02.\n" + "raise NotImplementedError('Implement artifact export block')\n", + "# END FILL -----------------------------------------------------------------\n", + "\n", + "# CHECKPOINT\n", + "required_files = [\n", + " ARTIFACT_DIR / 'generated_designs.npy',\n", + " ARTIFACT_DIR / 'baseline_designs.npy',\n", + " ARTIFACT_DIR / 'conditions.json',\n", + "]\n", + "missing = [str(f) for f in required_files if not f.exists()]\n", + "if missing:\n", + " raise RuntimeError('Missing required artifacts:\\\\n' + '\\\\n'.join(missing))\n", + "print('Checkpoint passed: Notebook 02 handoff artifacts exist.')\n" ] }, { diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index 11474d7..07946c3 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -30,7 +30,12 @@ "- implementation second,\n", "- interpretation third.\n", "\n", - "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n", + "\n", + "### Public exercise legend\n", + "- `PUBLIC FILL-IN CELL`: complete this block directly.\n", + "- `CHECKPOINT`: verify before moving forward.\n", + "- Metric interpretation is as important as metric computation.\n" ] }, { @@ -322,9 +327,13 @@ "id": "82f21c6f", "metadata": {}, "source": [ - "### Step 2 - Implement per-sample evaluation (TODO)\n", + "### Step 2 - Implement per-sample evaluation (PUBLIC FILL-IN)\n", + "\n", + "Compute constraint violations + objective values for generated and baseline designs under identical conditions.\n", "\n", - "Compute constraints and simulator objective for generated and baseline designs under identical settings.\n" + "Success criteria:\n", + "- one row per sample,\n", + "- objective values and violation counts both captured.\n" ] }, { @@ -336,20 +345,45 @@ "source": [ "problem = Beams2D(seed=7)\n", "\n", - "# TODO 1: Implement the per-sample evaluation loop.\n", - "# For each (generated, baseline, config):\n", - "# - g_viol = problem.check_constraints(design=g, config=cfg)\n", - "# - b_viol = problem.check_constraints(design=b, config=cfg)\n", - "# - reset simulator before each objective call\n", - "# - append dict with: sample, gen_obj, base_obj, gen_minus_base, gen_violations, base_violations\n", + "# PUBLIC FILL-IN CELL 02-A\n", + "# Goal: evaluate each sample pair with identical condition config.\n", "\n", "rows = []\n", - "raise NotImplementedError('Complete TODO 1 evaluation loop')\n", + "\n", + "# START FILL ---------------------------------------------------------------\n", + "# for i, (g, b, cfg_raw) in enumerate(zip(gen_designs, baseline_designs, conditions, strict=True)):\n", + "# cfg = dict(cfg_raw)\n", + "# g_viol = problem.check_constraints(design=g, config=cfg)\n", + "# b_viol = problem.check_constraints(design=b, config=cfg)\n", + "#\n", + "# # reset between simulations for reproducibility / simulator hygiene\n", + "# problem.reset(seed=7 + i)\n", + "# g_obj = float(problem.simulate(design=g, config=cfg))\n", + "# problem.reset(seed=7 + i)\n", + "# b_obj = float(problem.simulate(design=b, config=cfg))\n", + "#\n", + "# rows.append({\n", + "# 'sample': i,\n", + "# 'gen_obj': g_obj,\n", + "# 'base_obj': b_obj,\n", + "# 'gen_minus_base': g_obj - b_obj,\n", + "# 'gen_violations': len(g_viol),\n", + "# 'base_violations': len(b_viol),\n", + "# })\n", + "raise NotImplementedError('Implement per-sample evaluation loop')\n", + "# END FILL -----------------------------------------------------------------\n", "\n", "results = pd.DataFrame(rows)\n", "results.head()\n", "\n", - "# Completion check: `results` should have one row per sample.\n" + "# CHECKPOINT\n", + "expected_cols = {\n", + " 'sample', 'gen_obj', 'base_obj', 'gen_minus_base', 'gen_violations', 'base_violations'\n", + "}\n", + "missing_cols = expected_cols.difference(results.columns)\n", + "assert not missing_cols, f'Missing result columns: {missing_cols}'\n", + "assert len(results) == len(gen_designs), 'results must have one row per sample'\n", + "print('Checkpoint passed: per-sample evaluation table is complete.')\n" ] }, { @@ -357,9 +391,13 @@ "id": "fbcc2466", "metadata": {}, "source": [ - "### Step 3 - Implement summary metrics (TODO)\n", + "### Step 3 - Implement summary metrics (PUBLIC FILL-IN)\n", "\n", - "Report objective, feasibility, diversity, and novelty summaries in one table.\n" + "Report objective, feasibility, diversity, and novelty in a single table.\n", + "\n", + "Success criteria:\n", + "- `summary_df` has one row,\n", + "- each metric has a clear interpretation for workshop discussion.\n" ] }, { @@ -369,16 +407,9 @@ "metadata": {}, "outputs": [], "source": [ - "# TODO 2: Implement summary metrics.\n", - "# Suggested metrics:\n", - "# - n_samples\n", - "# - gen_obj_mean / base_obj_mean\n", - "# - objective_gap_mean\n", - "# - improvement_rate\n", - "# - gen_violation_ratio / base_violation_ratio\n", - "# - gen_feasible_rate\n", - "# - gen_diversity_l2\n", - "# - gen_novelty_to_train_l2\n", + "# PUBLIC FILL-IN CELL 02-B\n", + "# Goal: aggregate per-sample metrics into one benchmark summary row.\n", + "\n", "\n", "def mean_pairwise_l2(designs: np.ndarray) -> float:\n", " flat = designs.reshape(designs.shape[0], -1)\n", @@ -401,9 +432,32 @@ " nn_dists.append(float(np.min(d)))\n", " return float(np.mean(nn_dists))\n", "\n", - "raise NotImplementedError('Complete TODO 2 summary metrics')\n", - "\n", - "# Completion check: `summary_df` should be one row with all metric columns populated.\n" + "# START FILL ---------------------------------------------------------------\n", + "# Build one summary dict with at least these keys:\n", + "# - n_samples\n", + "# - gen_obj_mean\n", + "# - base_obj_mean\n", + "# - objective_gap_mean\n", + "# - improvement_rate\n", + "# - gen_violation_ratio\n", + "# - base_violation_ratio\n", + "# - gen_feasible_rate\n", + "# - gen_diversity_l2\n", + "# - gen_novelty_to_train_l2\n", + "raise NotImplementedError('Implement summary metric dictionary + summary_df')\n", + "# END FILL -----------------------------------------------------------------\n", + "\n", + "# CHECKPOINT\n", + "assert 'summary_df' in locals(), 'Define summary_df'\n", + "assert len(summary_df) == 1, 'summary_df should be one-row summary'\n", + "required_summary = {\n", + " 'n_samples', 'gen_obj_mean', 'base_obj_mean', 'objective_gap_mean', 'improvement_rate',\n", + " 'gen_violation_ratio', 'base_violation_ratio', 'gen_feasible_rate',\n", + " 'gen_diversity_l2', 'gen_novelty_to_train_l2'\n", + "}\n", + "missing = required_summary.difference(summary_df.columns)\n", + "assert not missing, f'Missing summary columns: {missing}'\n", + "print('Checkpoint passed: summary table is ready for export/discussion.')\n" ] }, { diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index 9947fb5..e2bada8 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -30,7 +30,12 @@ "- implementation second,\n", "- interpretation third.\n", "\n", - "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n" + "If you are following asynchronously, run cells in order and use the success checks to validate each stage before moving on.\n", + "\n", + "### Public exercise legend\n", + "- `PUBLIC FILL-IN`: implement this method block.\n", + "- Follow the fill order: constraints -> simulator build -> rollout -> wrappers.\n", + "- Run smoke test only after all TODO blocks are implemented.\n" ] }, { @@ -129,9 +134,20 @@ "id": "d4a22b27", "metadata": {}, "source": [ - "### Step 2 - Implement PyBullet manipulator co-design problem contract (TODO)\n", + "### Step 2 - Implement PyBullet manipulator co-design problem contract (PUBLIC FILL-IN)\n", + "\n", + "Complete each required method with deterministic behavior and clear failure messages.\n", + "\n", + "Recommended fill order:\n", + "1. Constraints in `__init__`\n", + "2. `_build_robot`\n", + "3. IK/FK helpers\n", + "4. `_rollout`\n", + "5. `simulate`, `optimize`, `render`, `random_design`\n", "\n", - "Complete each required method with deterministic behavior and clear failure messages.\n" + "Success criteria:\n", + "- smoke test prints objectives and optimization progress,\n", + "- render figure is interpretable (path/error/torque views).\n" ] }, { @@ -184,48 +200,82 @@ " dtype=np.float32,\n", " )\n", "\n", - " # TODO 1: implement design constraints and assign self.design_constraints\n", - " # Suggested constraints:\n", - " # - reachable workspace: link1+link2 must reach target radius\n", - " # - gain consistency: kd must be bounded relative to kp\n", + " # PUBLIC FILL-IN 03-A1: constraints\n", + " # START FILL -------------------------------------------------------\n", + " # Define two @constraint functions:\n", + " # (1) reachable_workspace(design, target_x, target_y, **_) -> assert reachable radius\n", + " # (2) gain_consistency(design, **_) -> assert kd is reasonable for kp\n", + " # Then assign: self.design_constraints = [reachable_workspace, gain_consistency]\n", " raise NotImplementedError('Implement __init__ constraints')\n", + " # END FILL ---------------------------------------------------------\n", "\n", " def _build_robot(self, l1: float, l2: float, payload_kg: float, damping: float) -> tuple[int, int]:\n", - " # TODO 2: build 2-link planar robot in PyBullet DIRECT mode\n", + " # PUBLIC FILL-IN 03-A2: build robot\n", + " # START FILL -------------------------------------------------------\n", + " # Required behavior:\n", + " # - reset simulation and gravity\n", + " # - create a 2-link articulated body\n", + " # - apply damping to joints\n", + " # - return (robot_id, end_effector_link_index)\n", " raise NotImplementedError('Implement _build_robot')\n", + " # END FILL ---------------------------------------------------------\n", "\n", " def _inverse_kinematics_2link(self, x: float, y: float, l1: float, l2: float) -> tuple[float, float]:\n", - " # TODO 3: implement closed-form IK for 2-link planar arm\n", + " # PUBLIC FILL-IN 03-A3: 2-link IK\n", + " # START FILL -------------------------------------------------------\n", + " # Implement stable closed-form IK with clipping for numerical robustness.\n", " raise NotImplementedError('Implement _inverse_kinematics_2link')\n", + " # END FILL ---------------------------------------------------------\n", "\n", " def _forward_kinematics_2link(self, q1: float, q2: float, l1: float, l2: float) -> tuple[float, float]:\n", - " # TODO 4: implement FK for end-effector position\n", + " # PUBLIC FILL-IN 03-A4: 2-link FK\n", + " # START FILL -------------------------------------------------------\n", + " # Return end-effector (x, y) from joint angles and link lengths.\n", " raise NotImplementedError('Implement _forward_kinematics_2link')\n", + " # END FILL ---------------------------------------------------------\n", "\n", " def _rollout(self, design: np.ndarray, cfg: dict, return_trace: bool = False):\n", - " # TODO 5: run PyBullet rollout and compute objectives\n", + " # PUBLIC FILL-IN 03-A5: rollout + objective computation\n", + " # START FILL -------------------------------------------------------\n", " # Required outputs:\n", - " # - final tracking error [m]\n", - " # - actuation energy [J]\n", - " # Optional trace for rendering: ee path, error curve, torque trace\n", + " # - objective vector: [final_tracking_error_m, actuation_energy_j]\n", + " # - if return_trace=True: dict with ee_trace, err_trace, tau_trace, target\n", + " # Tips:\n", + " # - connect with p.DIRECT, always disconnect in finally\n", + " # - apply optional disturbances from cfg['disturbance_scale']\n", " raise NotImplementedError('Implement _rollout')\n", + " # END FILL ---------------------------------------------------------\n", "\n", " def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n", - " # TODO 6: call rollout with cfg merge and clipping to design bounds\n", + " # PUBLIC FILL-IN 03-A6: benchmark simulation wrapper\n", + " # START FILL -------------------------------------------------------\n", + " # Merge cfg, clip design to bounds, return objective vector from _rollout.\n", " raise NotImplementedError('Implement simulate')\n", + " # END FILL ---------------------------------------------------------\n", "\n", " def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n", - " # TODO 7: implement deterministic local search + OptiStep history\n", + " # PUBLIC FILL-IN 03-A7: simple deterministic optimizer\n", + " # START FILL -------------------------------------------------------\n", + " # Implement local search and return (best_design, history[OptiStep]).\n", + " # Keep deterministic behavior via self.np_random.\n", " raise NotImplementedError('Implement optimize')\n", + " # END FILL ---------------------------------------------------------\n", "\n", " def render(self, design: np.ndarray, *, open_window: bool = False):\n", - " # TODO 8: create 4-panel interpretation plot\n", - " # Suggested panels: design vars, task-space path, error-vs-time, torque-vs-time\n", + " # PUBLIC FILL-IN 03-A8: interpretation plotting\n", + " # START FILL -------------------------------------------------------\n", + " # Create 4 panels:\n", + " # (1) design vars, (2) task-space path + target,\n", + " # (3) error over time, (4) torque over time.\n", " raise NotImplementedError('Implement render')\n", + " # END FILL ---------------------------------------------------------\n", "\n", " def random_design(self):\n", - " # TODO 9: sample uniformly in design bounds\n", - " raise NotImplementedError('Implement random_design')\n" + " # PUBLIC FILL-IN 03-A9: random design sampler\n", + " # START FILL -------------------------------------------------------\n", + " # Return a random design in bounds and dummy reward -1.\n", + " raise NotImplementedError('Implement random_design')\n", + " # END FILL ---------------------------------------------------------\n" ] }, { @@ -235,13 +285,16 @@ "source": [ "### Step 3 - Smoke-test your scaffold\n", "\n", - "Run minimal checks to verify interface consistency and simulator behavior.\n", + "Run this only after completing all PUBLIC FILL-IN blocks above.\n", "\n", + "What success looks like:\n", + "- non-empty optimization history,\n", + "- final objective improves over initial objective,\n", + "- 4-panel figure renders without error.\n", "\n", - "Use the multi-panel render to read **where heat enters**, **how material is distributed**, and **where thermal bottlenecks remain**.\n", - "\n", - "\n", - "Use the final figure to interpret whether the design/controller combination reaches the target robustly with acceptable energy use.\n" + "If the cell fails:\n", + "- re-check one method at a time in fill-order,\n", + "- especially `_rollout` and `simulate` shape/return semantics.\n" ] }, { @@ -251,7 +304,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Run this cell after finishing TODOs in PlanarManipulatorCoDesignProblem\n", + "# Smoke test (run after implementing all PUBLIC FILL-IN blocks)\n", "problem = PlanarManipulatorCoDesignProblem(\n", " seed=42,\n", " target_x=0.9,\n", @@ -290,6 +343,11 @@ "print('optimization steps:', len(history))\n", "print('How to read plots: vars | task-space path | error timeline | torque timeline')\n", "\n", + "# CHECKPOINT\n", + "assert len(history) > 0, 'Optimization history should not be empty'\n", + "assert np.all(np.isfinite(obj0)), 'Initial objective contains non-finite values'\n", + "assert np.all(np.isfinite(objf)), 'Final objective contains non-finite values'\n", + "\n", "problem.render(opt_design)\n" ] }, From 95137d8ce98f49b6a37b74e945a5a5317756ca78 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Mon, 16 Mar 2026 14:35:52 +0100 Subject: [PATCH 35/37] Clean workshop docs and ignore rules before PR --- .gitignore | 1 + workshops/dcc26/README.md | 29 +++++++++++++------------- workshops/dcc26/requirements-colab.txt | 24 +++++++++++++-------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 6c3a452..d22cb1c 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,4 @@ logs/* .ruff_cache/ engibench_studies/* workshops/dcc26/artifacts/* +workshops/dcc26/optional_artifacts/* diff --git a/workshops/dcc26/README.md b/workshops/dcc26/README.md index 52e0abe..245f9f4 100644 --- a/workshops/dcc26/README.md +++ b/workshops/dcc26/README.md @@ -4,7 +4,7 @@ This folder contains the DCC'26 hands-on notebook suite for benchmarking AI meth It is split into two tracks: -- `participant/`: notebooks with `TODO` cells for attendees +- `participant/`: notebooks with guided `PUBLIC FILL-IN` cells for attendees - `solutions/`: fully completed facilitator notebooks ## Workshop flow (3.5h) @@ -26,8 +26,8 @@ It is split into two tracks: - Metric and artifact export - `participant/03_add_new_problem_scaffold.ipynb` and `solutions/03_add_new_problem_scaffold.ipynb` (25 min) - - Ambitious `Problem` scaffold (`BatteryColdPlate2DProblem`, not currently in EngiBench) - - Lightweight thermal-flow tradeoff simulator and optimization loop + - Ambitious `Problem` scaffold (`PlanarManipulatorCoDesignProblem`, not currently in EngiBench) + - PyBullet-based robotics co-design simulation and optimization loop - Mapping to contribution docs ## Runtime assumptions @@ -38,26 +38,27 @@ It is split into two tracks: ## Colab setup -Use the pinned requirements in `requirements-colab.txt`. +Use `requirements-colab.txt` only as a local convenience snapshot. +The notebook bootstrap cells are the runtime source of truth for Colab. All notebooks now include a conditional dependency bootstrap cell: - On Colab: installs required packages automatically. - On local envs: skips install by default (`FORCE_INSTALL = False`). -- Note: `engiopt` is installed from the EngiOpt GitHub branch in Notebook 01 bootstrap. +- Note: notebooks that use EngiOpt install it from the EngiOpt GitHub branch bootstrap. ## Open in Colab -Use these `#copy=true` links for workshop sharing so attendees are prompted to create their own Drive copy first. +Use these `?copy=true` links for workshop sharing so attendees are prompted to create their own Drive copy first. -- Participant 00: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/00_setup_api_warmup.ipynb#copy=true -- Participant 01: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/01_train_generate.ipynb#copy=true -- Participant 02: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/02_evaluate_metrics.ipynb#copy=true -- Participant 03: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb#copy=true -- Solution 00: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/00_setup_api_warmup.ipynb#copy=true -- Solution 01: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/01_train_generate.ipynb#copy=true -- Solution 02: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/02_evaluate_metrics.ipynb#copy=true -- Solution 03: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb#copy=true +- Participant 00: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/00_setup_api_warmup.ipynb?copy=true +- Participant 01: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/01_train_generate.ipynb?copy=true +- Participant 02: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/02_evaluate_metrics.ipynb?copy=true +- Participant 03: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb?copy=true +- Solution 00: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/00_setup_api_warmup.ipynb?copy=true +- Solution 01: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/01_train_generate.ipynb?copy=true +- Solution 02: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/02_evaluate_metrics.ipynb?copy=true +- Solution 03: https://colab.research.google.com/github/IDEALLab/EngiOpt/blob/codex/dcc26-workshop-notebooks/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb?copy=true ## Output artifacts diff --git a/workshops/dcc26/requirements-colab.txt b/workshops/dcc26/requirements-colab.txt index 75ffb20..aee3142 100644 --- a/workshops/dcc26/requirements-colab.txt +++ b/workshops/dcc26/requirements-colab.txt @@ -1,9 +1,15 @@ -engibench[beams2d]>=0.1.0 -engiopt>=0.0.1 -torch>=2.5.0 -torchvision>=0.20.1 -numpy>=1.26 -pandas>=2.2 -matplotlib>=3.9 -seaborn>=0.13 -scikit-learn>=1.6 +# Local convenience snapshot for workshop notebooks. +# Source of truth for Colab is the install/bootstrap cell inside each notebook. +# EngiOpt is intentionally installed from Git in notebook bootstrap cells. + +engibench[beams2d] +sqlitedict +matplotlib +seaborn +gymnasium +pybullet +tqdm +tyro +wandb +torch +torchvision From b36a5db72332249ec03b6b61c0584bc1ccb2913d Mon Sep 17 00:00:00 2001 From: Soheyl Date: Mon, 16 Mar 2026 14:49:40 +0100 Subject: [PATCH 36/37] Format workshop notebooks with ruff --- .../participant/00_setup_api_warmup.ipynb | 80 +++--- .../dcc26/participant/01_train_generate.ipynb | 127 +++++----- .../participant/02_evaluate_metrics.ipynb | 204 +++++++-------- .../03_add_new_problem_scaffold.ipynb | 217 ++++++++-------- .../dcc26/solutions/00_setup_api_warmup.ipynb | 50 ++-- .../dcc26/solutions/01_train_generate.ipynb | 186 +++++++------- .../dcc26/solutions/02_evaluate_metrics.ipynb | 233 +++++++++--------- .../03_add_new_problem_scaffold.ipynb | 215 ++++++++-------- 8 files changed, 676 insertions(+), 636 deletions(-) diff --git a/workshops/dcc26/participant/00_setup_api_warmup.ipynb b/workshops/dcc26/participant/00_setup_api_warmup.ipynb index b91c242..db000d8 100644 --- a/workshops/dcc26/participant/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/participant/00_setup_api_warmup.ipynb @@ -85,28 +85,30 @@ "import subprocess\n", "import sys\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "\n", "def pip_install(packages: list[str]):\n", - " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", - " print('Running:', ' '.join(cmd))\n", + " cmd = [sys.executable, \"-m\", \"pip\", \"install\", *packages]\n", + " print(\"Running:\", \" \".join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", + "\n", + "\n", + "BASE_PACKAGES = [\"engibench[beams2d]\", \"matplotlib\", \"seaborn\"]\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print(\"Installing dependencies...\")\n", " pip_install(BASE_PACKAGES)\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'])\n", + " pip_install([\"torch\", \"torchvision\"])\n", "\n", - " print('Dependency install complete.')\n", + " print(\"Dependency install complete.\")\n", "else:\n", - " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" + " print(\"Skipping install (using current environment). Set FORCE_INSTALL=True to install here.\")" ] }, { @@ -138,8 +140,8 @@ "random.seed(SEED)\n", "np.random.seed(SEED)\n", "\n", - "print('engibench version:', engibench.__version__)\n", - "print('seed:', SEED)" + "print(\"engibench version:\", engibench.__version__)\n", + "print(\"seed:\", SEED)" ] }, { @@ -174,20 +176,20 @@ "# END FILL -----------------------------------------------------------------\n", "\n", "if problem is None:\n", - " raise RuntimeError('Set `problem` before running this cell (example: Beams2D(seed=SEED)).')\n", + " raise RuntimeError(\"Set `problem` before running this cell (example: Beams2D(seed=SEED)).\")\n", "\n", - "print('Problem class:', type(problem).__name__)\n", - "print('Design space:', problem.design_space)\n", - "print('Objectives:', problem.objectives)\n", - "print('Conditions instance:', problem.conditions)\n", - "print('Condition keys:', problem.conditions_keys)\n", - "print('Dataset ID:', problem.dataset_id)\n", + "print(\"Problem class:\", type(problem).__name__)\n", + "print(\"Design space:\", problem.design_space)\n", + "print(\"Objectives:\", problem.objectives)\n", + "print(\"Conditions instance:\", problem.conditions)\n", + "print(\"Condition keys:\", problem.conditions_keys)\n", + "print(\"Dataset ID:\", problem.dataset_id)\n", "\n", "# CHECKPOINT\n", - "assert hasattr(problem, 'design_space'), 'Problem is missing design_space'\n", - "assert hasattr(problem, 'objectives'), 'Problem is missing objectives'\n", - "assert len(problem.conditions_keys) > 0, 'conditions_keys should not be empty'\n", - "print('Checkpoint passed: problem contract is visible and ready.')\n" + "assert hasattr(problem, \"design_space\"), \"Problem is missing design_space\"\n", + "assert hasattr(problem, \"objectives\"), \"Problem is missing objectives\"\n", + "assert len(problem.conditions_keys) > 0, \"conditions_keys should not be empty\"\n", + "print(\"Checkpoint passed: problem contract is visible and ready.\")" ] }, { @@ -219,29 +221,29 @@ "# Goal: inspect one training sample and build a valid config dictionary.\n", "\n", "# START FILL ---------------------------------------------------------------\n", - "dataset = None # Example: problem.dataset\n", + "dataset = None # Example: problem.dataset\n", "sample_idx = 0\n", "# design = ... # np.array from dataset['train']['optimal_design'][sample_idx]\n", "# config = ... # dict over problem.conditions_keys\n", "# END FILL -----------------------------------------------------------------\n", "\n", "if dataset is None:\n", - " raise RuntimeError('Set `dataset = problem.dataset` before running.')\n", - "if 'design' not in locals() or 'config' not in locals():\n", - " raise RuntimeError('Define both `design` and `config` in the START FILL section.')\n", + " raise RuntimeError(\"Set `dataset = problem.dataset` before running.\")\n", + "if \"design\" not in locals() or \"config\" not in locals():\n", + " raise RuntimeError(\"Define both `design` and `config` in the START FILL section.\")\n", "\n", "print(dataset)\n", - "print('sample_idx:', sample_idx)\n", - "print('design shape:', np.array(design).shape)\n", - "print('config:', config)\n", + "print(\"sample_idx:\", sample_idx)\n", + "print(\"design shape:\", np.array(design).shape)\n", + "print(\"config:\", config)\n", "\n", "# CHECKPOINT\n", "assert tuple(np.array(design).shape) == tuple(problem.design_space.shape), (\n", - " f'design shape mismatch: expected {problem.design_space.shape}, got {np.array(design).shape}'\n", + " f\"design shape mismatch: expected {problem.design_space.shape}, got {np.array(design).shape}\"\n", ")\n", "missing = [k for k in problem.conditions_keys if k not in config]\n", - "assert not missing, f'config missing condition keys: {missing}'\n", - "print('Checkpoint passed: dataset sample + config are valid.')\n" + "assert not missing, f\"config missing condition keys: {missing}\"\n", + "print(\"Checkpoint passed: dataset sample + config are valid.\")" ] }, { @@ -263,7 +265,7 @@ "source": [ "# Render the sampled design (run after TODO 3)\n", "fig, ax = problem.render(design)\n", - "ax.set_title('Participant: sampled Beams2D design')\n", + "ax.set_title(\"Participant: sampled Beams2D design\")\n", "plt.show()" ] }, @@ -300,19 +302,19 @@ "# violations = ...\n", "# END FILL -----------------------------------------------------------------\n", "\n", - "if 'violations' not in locals():\n", - " raise RuntimeError('Define `violations` in the START FILL section.')\n", + "if \"violations\" not in locals():\n", + " raise RuntimeError(\"Define `violations` in the START FILL section.\")\n", "\n", - "print('Violation count:', len(violations))\n", + "print(\"Violation count:\", len(violations))\n", "if violations:\n", " for i, v in enumerate(violations[:5]):\n", - " print(f' [{i}]', v)\n", + " print(f\" [{i}]\", v)\n", "else:\n", - " print('No violations found. Try a more aggressive volfrac mismatch.')\n", + " print(\"No violations found. Try a more aggressive volfrac mismatch.\")\n", "\n", "# CHECKPOINT\n", - "assert hasattr(violations, '__len__'), 'violations should be a sized collection'\n", - "print('Checkpoint passed: constraint semantics inspected.')\n" + "assert hasattr(violations, \"__len__\"), \"violations should be a sized collection\"\n", + "print(\"Checkpoint passed: constraint semantics inspected.\")" ] }, { diff --git a/workshops/dcc26/participant/01_train_generate.ipynb b/workshops/dcc26/participant/01_train_generate.ipynb index 108d7a2..2af18bd 100644 --- a/workshops/dcc26/participant/01_train_generate.ipynb +++ b/workshops/dcc26/participant/01_train_generate.ipynb @@ -60,30 +60,32 @@ "import subprocess\n", "import sys\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "\n", "def pip_install(packages: list[str]):\n", - " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", - " print('Running:', ' '.join(cmd))\n", + " cmd = [sys.executable, \"-m\", \"pip\", \"install\", *packages]\n", + " print(\"Running:\", \" \".join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'matplotlib', 'tqdm', 'tyro', 'wandb']\n", - "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", + "\n", + "\n", + "BASE_PACKAGES = [\"engibench[beams2d]\", \"sqlitedict\", \"matplotlib\", \"tqdm\", \"tyro\", \"wandb\"]\n", + "ENGIOPT_GIT = \"git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt\"\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print(\"Installing dependencies...\")\n", " pip_install(BASE_PACKAGES)\n", " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'])\n", + " pip_install([\"torch\", \"torchvision\"])\n", "\n", - " print('Dependency install complete.')\n", + " print(\"Dependency install complete.\")\n", "else:\n", - " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" + " print(\"Skipping install (using current environment). Set FORCE_INSTALL=True to install here.\")" ] }, { @@ -144,20 +146,20 @@ " from engiopt.cgan_2d.cgan_2d import Generator as EngiOptCGAN2DGenerator\n", "except ModuleNotFoundError as exc:\n", " raise ModuleNotFoundError(\n", - " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", + " \"Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.\"\n", " ) from exc\n", "\n", "USE_WANDB_ARTIFACTS = False\n", - "WANDB_PROJECT = 'dcc26-workshop'\n", + "WANDB_PROJECT = \"dcc26-workshop\"\n", "WANDB_ENTITY = None\n", - "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", - "WANDB_ARTIFACT_ALIAS = 'latest'\n", + "WANDB_ARTIFACT_NAME = \"dcc26_beams2d_generated_artifacts\"\n", + "WANDB_ARTIFACT_ALIAS = \"latest\"\n", "WANDB_LOG_TRAINING = True\n", "\n", "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", - " in_colab = 'google.colab' in sys.modules\n", - " path = Path('/content/dcc26_artifacts') if in_colab else Path('workshops/dcc26/artifacts')\n", + " in_colab = \"google.colab\" in sys.modules\n", + " path = Path(\"/content/dcc26_artifacts\") if in_colab else Path(\"workshops/dcc26/artifacts\")\n", " if create:\n", " path.mkdir(parents=True, exist_ok=True)\n", " return path\n", @@ -170,16 +172,16 @@ "if th.cuda.is_available():\n", " th.cuda.manual_seed_all(SEED)\n", "\n", - "DEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", - "print('device:', DEVICE)\n", + "DEVICE = th.device(\"cuda\" if th.cuda.is_available() else \"cpu\")\n", + "print(\"device:\", DEVICE)\n", "\n", "ARTIFACT_DIR = resolve_artifact_dir(create=True)\n", - "print('artifact dir:', ARTIFACT_DIR)\n", + "print(\"artifact dir:\", ARTIFACT_DIR)\n", "\n", - "CKPT_PATH = ARTIFACT_DIR / 'engiopt_cgan2d_generator_supervised.pt'\n", - "HISTORY_PATH = ARTIFACT_DIR / 'training_history.csv'\n", - "TRAIN_CURVE_PATH = ARTIFACT_DIR / 'training_curve.png'\n", - "LATENT_DIM = 32\n" + "CKPT_PATH = ARTIFACT_DIR / \"engiopt_cgan2d_generator_supervised.pt\"\n", + "HISTORY_PATH = ARTIFACT_DIR / \"training_history.csv\"\n", + "TRAIN_CURVE_PATH = ARTIFACT_DIR / \"training_curve.png\"\n", + "LATENT_DIM = 32" ] }, { @@ -200,22 +202,22 @@ "outputs": [], "source": [ "problem = Beams2D(seed=SEED)\n", - "train_ds = problem.dataset['train']\n", - "test_ds = problem.dataset['test']\n", + "train_ds = problem.dataset[\"train\"]\n", + "test_ds = problem.dataset[\"test\"]\n", "\n", "condition_keys = problem.conditions_keys\n", - "print('condition keys:', condition_keys)\n", + "print(\"condition keys:\", condition_keys)\n", "\n", "N_TRAIN = 512\n", "subset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n", "\n", "conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", - "designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", + "designs_np = np.array(train_ds[\"optimal_design\"])[subset_idx].astype(np.float32)\n", "targets_np = (designs_np * 2.0) - 1.0\n", "\n", - "print('conditions shape:', conds_np.shape)\n", - "print('designs shape:', designs_np.shape)\n", - "print('target range:', float(targets_np.min()), 'to', float(targets_np.max()))\n" + "print(\"conditions shape:\", conds_np.shape)\n", + "print(\"designs shape:\", designs_np.shape)\n", + "print(\"target range:\", float(targets_np.min()), \"to\", float(targets_np.max()))" ] }, { @@ -247,24 +249,27 @@ "optimizer = None\n", "criterion = None\n", "\n", + "\n", "def sample_noise(batch_size: int) -> th.Tensor:\n", " # Return standard normal latent vectors on DEVICE.\n", - " raise NotImplementedError('Implement sample_noise')\n", + " raise NotImplementedError(\"Implement sample_noise\")\n", + "\n", + "\n", "# END FILL -----------------------------------------------------------------\n", "\n", "if model is None or optimizer is None or criterion is None:\n", - " raise RuntimeError('Define model, optimizer, and criterion in the START FILL block.')\n", + " raise RuntimeError(\"Define model, optimizer, and criterion in the START FILL block.\")\n", "\n", "# CHECKPOINT: validate latent shape and forward-pass shape\n", "z_probe = sample_noise(4)\n", - "assert tuple(z_probe.shape) == (4, LATENT_DIM), f'Expected (4, {LATENT_DIM}), got {tuple(z_probe.shape)}'\n", + "assert tuple(z_probe.shape) == (4, LATENT_DIM), f\"Expected (4, {LATENT_DIM}), got {tuple(z_probe.shape)}\"\n", "cond_probe = th.tensor(conds_np[:4], dtype=th.float32, device=DEVICE)\n", "with th.no_grad():\n", " pred_probe = model(z_probe, cond_probe)\n", "assert tuple(pred_probe.shape[1:]) == tuple(problem.design_space.shape), (\n", - " f'Output shape mismatch: expected tail {problem.design_space.shape}, got {tuple(pred_probe.shape[1:])}'\n", + " f\"Output shape mismatch: expected tail {problem.design_space.shape}, got {tuple(pred_probe.shape[1:])}\"\n", ")\n", - "print('Checkpoint passed: model setup is consistent with problem representation.')\n" + "print(\"Checkpoint passed: model setup is consistent with problem representation.\")" ] }, { @@ -306,20 +311,20 @@ " # 5) Save checkpoint to CKPT_PATH\n", " # 6) Save history CSV to HISTORY_PATH\n", " # 7) Save training curve figure to TRAIN_CURVE_PATH\n", - " raise NotImplementedError('Implement TRAIN_FROM_SCRATCH branch')\n", + " raise NotImplementedError(\"Implement TRAIN_FROM_SCRATCH branch\")\n", " # END FILL -------------------------------------------------------------\n", "elif CKPT_PATH.exists():\n", " # START FILL -----------------------------------------------------------\n", " # Load checkpoint into model and, if available, load history CSV.\n", - " raise NotImplementedError('Implement checkpoint load branch')\n", + " raise NotImplementedError(\"Implement checkpoint load branch\")\n", " # END FILL -------------------------------------------------------------\n", "else:\n", - " raise FileNotFoundError(f'Checkpoint not found at {CKPT_PATH}. Train first or provide checkpoint.')\n", + " raise FileNotFoundError(f\"Checkpoint not found at {CKPT_PATH}. Train first or provide checkpoint.\")\n", "\n", "# CHECKPOINT\n", - "assert CKPT_PATH.exists(), f'Missing checkpoint: {CKPT_PATH}'\n", - "assert HISTORY_PATH.exists(), f'Missing history CSV: {HISTORY_PATH}'\n", - "print('Checkpoint passed: train/load artifacts are ready for generation step.')\n" + "assert CKPT_PATH.exists(), f\"Missing checkpoint: {CKPT_PATH}\"\n", + "assert HISTORY_PATH.exists(), f\"Missing history CSV: {HISTORY_PATH}\"\n", + "print(\"Checkpoint passed: train/load artifacts are ready for generation step.\")" ] }, { @@ -355,17 +360,17 @@ "# 3) run model in eval/no_grad with sample_noise\n", "# 4) map tanh output [-1,1] -> [0,1] and clip\n", "# 5) build `conditions_records` as list[dict]\n", - "raise NotImplementedError('Implement generation block')\n", + "raise NotImplementedError(\"Implement generation block\")\n", "# END FILL -----------------------------------------------------------------\n", "\n", "# CHECKPOINT\n", - "assert 'gen_designs' in locals(), 'Define gen_designs'\n", - "assert 'baseline_designs' in locals(), 'Define baseline_designs'\n", - "assert 'test_conds' in locals(), 'Define test_conds'\n", - "assert 'conditions_records' in locals(), 'Define conditions_records'\n", - "assert gen_designs.shape == baseline_designs.shape, 'Generated and baseline shapes must match'\n", - "assert len(conditions_records) == gen_designs.shape[0], 'conditions_records length mismatch'\n", - "print('Checkpoint passed: generation outputs are valid and aligned.')\n" + "assert \"gen_designs\" in locals(), \"Define gen_designs\"\n", + "assert \"baseline_designs\" in locals(), \"Define baseline_designs\"\n", + "assert \"test_conds\" in locals(), \"Define test_conds\"\n", + "assert \"conditions_records\" in locals(), \"Define conditions_records\"\n", + "assert gen_designs.shape == baseline_designs.shape, \"Generated and baseline shapes must match\"\n", + "assert len(conditions_records) == gen_designs.shape[0], \"conditions_records length mismatch\"\n", + "print(\"Checkpoint passed: generation outputs are valid and aligned.\")" ] }, { @@ -401,19 +406,19 @@ "# - json dump of conditions_records -> conditions.json\n", "# Recommended exports:\n", "# - checkpoint (.pt), training_history.csv, training_curve.png\n", - "raise NotImplementedError('Implement artifact export block')\n", + "raise NotImplementedError(\"Implement artifact export block\")\n", "# END FILL -----------------------------------------------------------------\n", "\n", "# CHECKPOINT\n", "required_files = [\n", - " ARTIFACT_DIR / 'generated_designs.npy',\n", - " ARTIFACT_DIR / 'baseline_designs.npy',\n", - " ARTIFACT_DIR / 'conditions.json',\n", + " ARTIFACT_DIR / \"generated_designs.npy\",\n", + " ARTIFACT_DIR / \"baseline_designs.npy\",\n", + " ARTIFACT_DIR / \"conditions.json\",\n", "]\n", "missing = [str(f) for f in required_files if not f.exists()]\n", "if missing:\n", - " raise RuntimeError('Missing required artifacts:\\\\n' + '\\\\n'.join(missing))\n", - "print('Checkpoint passed: Notebook 02 handoff artifacts exist.')\n" + " raise RuntimeError(\"Missing required artifacts:\\\\n\" + \"\\\\n\".join(missing))\n", + "print(\"Checkpoint passed: Notebook 02 handoff artifacts exist.\")" ] }, { @@ -436,16 +441,16 @@ "# Quick visual side-by-side snapshot\n", "fig, axes = plt.subplots(2, 6, figsize=(14, 5))\n", "for i in range(6):\n", - " axes[0, i].imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n", - " axes[0, i].set_title(f'gen {i}')\n", - " axes[0, i].axis('off')\n", + " axes[0, i].imshow(gen_designs[i], cmap=\"gray\", vmin=0, vmax=1)\n", + " axes[0, i].set_title(f\"gen {i}\")\n", + " axes[0, i].axis(\"off\")\n", "\n", - " axes[1, i].imshow(baseline_designs[i], cmap='gray', vmin=0, vmax=1)\n", - " axes[1, i].set_title(f'base {i}')\n", - " axes[1, i].axis('off')\n", + " axes[1, i].imshow(baseline_designs[i], cmap=\"gray\", vmin=0, vmax=1)\n", + " axes[1, i].set_title(f\"base {i}\")\n", + " axes[1, i].axis(\"off\")\n", "\n", "fig.tight_layout()\n", - "plt.show()\n" + "plt.show()" ] }, { diff --git a/workshops/dcc26/participant/02_evaluate_metrics.ipynb b/workshops/dcc26/participant/02_evaluate_metrics.ipynb index 07946c3..f17fb16 100644 --- a/workshops/dcc26/participant/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/participant/02_evaluate_metrics.ipynb @@ -59,30 +59,32 @@ "import subprocess\n", "import sys\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "\n", "def pip_install(packages: list[str]):\n", - " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", - " print('Running:', ' '.join(cmd))\n", + " cmd = [sys.executable, \"-m\", \"pip\", \"install\", *packages]\n", + " print(\"Running:\", \" \".join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'matplotlib', 'tqdm', 'tyro', 'wandb']\n", - "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", + "\n", + "\n", + "BASE_PACKAGES = [\"engibench[beams2d]\", \"sqlitedict\", \"matplotlib\", \"tqdm\", \"tyro\", \"wandb\"]\n", + "ENGIOPT_GIT = \"git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt\"\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print(\"Installing dependencies...\")\n", " pip_install(BASE_PACKAGES)\n", " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'])\n", + " pip_install([\"torch\", \"torchvision\"])\n", "\n", - " print('Dependency install complete.')\n", + " print(\"Dependency install complete.\")\n", "else:\n", - " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" + " print(\"Skipping install (using current environment). Set FORCE_INSTALL=True to install here.\")" ] }, { @@ -142,22 +144,22 @@ " from engiopt.cgan_2d.cgan_2d import Generator as EngiOptCGAN2DGenerator\n", "except ModuleNotFoundError as exc:\n", " raise ModuleNotFoundError(\n", - " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", + " \"Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.\"\n", " ) from exc\n", "\n", "USE_WANDB_ARTIFACTS = False\n", - "WANDB_PROJECT = 'dcc26-workshop'\n", + "WANDB_PROJECT = \"dcc26-workshop\"\n", "WANDB_ENTITY = None\n", - "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", - "WANDB_ARTIFACT_ALIAS = 'latest'\n", + "WANDB_ARTIFACT_NAME = \"dcc26_beams2d_generated_artifacts\"\n", + "WANDB_ARTIFACT_ALIAS = \"latest\"\n", "\n", "# Self-heal path for workshop robustness\n", "AUTO_BUILD_ARTIFACTS_IF_MISSING = True\n", "\n", "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", - " in_colab = 'google.colab' in sys.modules\n", - " path = Path('/content/dcc26_artifacts') if in_colab else Path('workshops/dcc26/artifacts')\n", + " in_colab = \"google.colab\" in sys.modules\n", + " path = Path(\"/content/dcc26_artifacts\") if in_colab else Path(\"workshops/dcc26/artifacts\")\n", " if create:\n", " path.mkdir(parents=True, exist_ok=True)\n", " return path\n", @@ -172,7 +174,7 @@ " batch_size: int = 64,\n", " latent_dim: int = 32,\n", ") -> None:\n", - " print('Building Notebook 01-style artifacts locally with EngiOpt...')\n", + " print(\"Building Notebook 01-style artifacts locally with EngiOpt...\")\n", "\n", " random.seed(seed)\n", " np.random.seed(seed)\n", @@ -180,10 +182,10 @@ " if th.cuda.is_available():\n", " th.cuda.manual_seed_all(seed)\n", "\n", - " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", + " device = th.device(\"cuda\" if th.cuda.is_available() else \"cpu\")\n", " problem = Beams2D(seed=seed)\n", - " train_ds = problem.dataset['train']\n", - " test_ds = problem.dataset['test']\n", + " train_ds = problem.dataset[\"train\"]\n", + " test_ds = problem.dataset[\"test\"]\n", " condition_keys = problem.conditions_keys\n", "\n", " rng = np.random.default_rng(seed)\n", @@ -191,7 +193,7 @@ " subset_idx = rng.choice(len(train_ds), size=subset_size, replace=False)\n", "\n", " conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", - " designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", + " designs_np = np.array(train_ds[\"optimal_design\"])[subset_idx].astype(np.float32)\n", " targets_np = designs_np * 2.0 - 1.0\n", "\n", " model = EngiOptCGAN2DGenerator(\n", @@ -226,12 +228,12 @@ "\n", " epoch_avg = epoch_loss / len(dl)\n", " train_losses.append(epoch_avg)\n", - " print(f'bootstrap epoch {epoch + 1:02d}/{epochs} - loss: {epoch_avg:.4f}')\n", + " print(f\"bootstrap epoch {epoch + 1:02d}/{epochs} - loss: {epoch_avg:.4f}\")\n", "\n", " sample_count = min(n_samples, len(test_ds))\n", " selected = rng.choice(len(test_ds), size=sample_count, replace=False)\n", " test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", - " baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", + " baseline_designs = np.array(test_ds[\"optimal_design\"])[selected].astype(np.float32)\n", "\n", " model.eval()\n", " with th.no_grad():\n", @@ -244,37 +246,37 @@ " rec = {}\n", " for j, k in enumerate(condition_keys):\n", " v = test_conds[i, j]\n", - " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", + " rec[k] = bool(v) if k == \"overhang_constraint\" else float(v)\n", " conditions_records.append(rec)\n", "\n", " artifact_dir.mkdir(parents=True, exist_ok=True)\n", - " np.save(artifact_dir / 'generated_designs.npy', gen_designs)\n", - " np.save(artifact_dir / 'baseline_designs.npy', baseline_designs)\n", - " with open(artifact_dir / 'conditions.json', 'w', encoding='utf-8') as f:\n", + " np.save(artifact_dir / \"generated_designs.npy\", gen_designs)\n", + " np.save(artifact_dir / \"baseline_designs.npy\", baseline_designs)\n", + " with open(artifact_dir / \"conditions.json\", \"w\", encoding=\"utf-8\") as f:\n", " json.dump(conditions_records, f, indent=2)\n", "\n", - " pd.DataFrame({'epoch': np.arange(1, len(train_losses) + 1), 'train_loss': train_losses}).to_csv(\n", - " artifact_dir / 'training_history.csv', index=False\n", + " pd.DataFrame({\"epoch\": np.arange(1, len(train_losses) + 1), \"train_loss\": train_losses}).to_csv(\n", + " artifact_dir / \"training_history.csv\", index=False\n", " )\n", "\n", " th.save(\n", " {\n", - " 'model': model.state_dict(),\n", - " 'condition_keys': condition_keys,\n", - " 'latent_dim': latent_dim,\n", - " 'model_family': 'engiopt.cgan_2d.Generator',\n", + " \"model\": model.state_dict(),\n", + " \"condition_keys\": condition_keys,\n", + " \"latent_dim\": latent_dim,\n", + " \"model_family\": \"engiopt.cgan_2d.Generator\",\n", " },\n", - " artifact_dir / 'engiopt_cgan2d_generator_supervised.pt',\n", + " artifact_dir / \"engiopt_cgan2d_generator_supervised.pt\",\n", " )\n", "\n", - " print('Built artifacts at', artifact_dir)\n", + " print(\"Built artifacts at\", artifact_dir)\n", "\n", "\n", "ARTIFACT_DIR = resolve_artifact_dir(create=True)\n", "required = [\n", - " ARTIFACT_DIR / 'generated_designs.npy',\n", - " ARTIFACT_DIR / 'baseline_designs.npy',\n", - " ARTIFACT_DIR / 'conditions.json',\n", + " ARTIFACT_DIR / \"generated_designs.npy\",\n", + " ARTIFACT_DIR / \"baseline_designs.npy\",\n", + " ARTIFACT_DIR / \"conditions.json\",\n", "]\n", "\n", "if not all(p.exists() for p in required):\n", @@ -282,44 +284,44 @@ " try:\n", " import wandb\n", "\n", - " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='artifact-download', reinit=True)\n", + " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type=\"artifact-download\", reinit=True)\n", " if WANDB_ENTITY:\n", " artifact_ref = f\"{WANDB_ENTITY}/{WANDB_PROJECT}/{WANDB_ARTIFACT_NAME}:{WANDB_ARTIFACT_ALIAS}\"\n", " else:\n", " artifact_ref = f\"{WANDB_PROJECT}/{WANDB_ARTIFACT_NAME}:{WANDB_ARTIFACT_ALIAS}\"\n", - " artifact = run.use_artifact(artifact_ref, type='dataset')\n", + " artifact = run.use_artifact(artifact_ref, type=\"dataset\")\n", " artifact.download(root=str(ARTIFACT_DIR))\n", " run.finish()\n", - " print('Downloaded artifacts from W&B to', ARTIFACT_DIR)\n", + " print(\"Downloaded artifacts from W&B to\", ARTIFACT_DIR)\n", " except Exception as exc:\n", " if AUTO_BUILD_ARTIFACTS_IF_MISSING:\n", - " print('W&B download failed; switching to local artifact build:', exc)\n", + " print(\"W&B download failed; switching to local artifact build:\", exc)\n", " build_artifacts_locally(ARTIFACT_DIR)\n", " else:\n", " raise FileNotFoundError(\n", - " 'Artifacts missing locally and W&B download failed. '\n", - " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", - " f'Details: {exc}'\n", + " \"Artifacts missing locally and W&B download failed. \"\n", + " \"Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. \"\n", + " f\"Details: {exc}\"\n", " ) from exc\n", " elif AUTO_BUILD_ARTIFACTS_IF_MISSING:\n", " build_artifacts_locally(ARTIFACT_DIR)\n", " else:\n", - " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", + " missing = \"\\n\".join(f\"- {p}\" for p in required if not p.exists())\n", " raise FileNotFoundError(\n", - " 'Notebook 01 artifacts not found. Run Notebook 01 first (including export cell), '\n", - " 'or enable USE_WANDB_ARTIFACTS to fetch from W&B. Missing files:\\\\n' + missing\n", + " \"Notebook 01 artifacts not found. Run Notebook 01 first (including export cell), \"\n", + " \"or enable USE_WANDB_ARTIFACTS to fetch from W&B. Missing files:\\\\n\" + missing\n", " )\n", "\n", - "print('using artifact dir:', ARTIFACT_DIR)\n", + "print(\"using artifact dir:\", ARTIFACT_DIR)\n", "\n", - "gen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\n", - "baseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\n", - "with open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n", + "gen_designs = np.load(ARTIFACT_DIR / \"generated_designs.npy\")\n", + "baseline_designs = np.load(ARTIFACT_DIR / \"baseline_designs.npy\")\n", + "with open(ARTIFACT_DIR / \"conditions.json\", encoding=\"utf-8\") as f:\n", " conditions = json.load(f)\n", "\n", - "print('generated:', gen_designs.shape)\n", - "print('baseline:', baseline_designs.shape)\n", - "print('conditions:', len(conditions))\n" + "print(\"generated:\", gen_designs.shape)\n", + "print(\"baseline:\", baseline_designs.shape)\n", + "print(\"conditions:\", len(conditions))" ] }, { @@ -370,20 +372,18 @@ "# 'gen_violations': len(g_viol),\n", "# 'base_violations': len(b_viol),\n", "# })\n", - "raise NotImplementedError('Implement per-sample evaluation loop')\n", + "raise NotImplementedError(\"Implement per-sample evaluation loop\")\n", "# END FILL -----------------------------------------------------------------\n", "\n", "results = pd.DataFrame(rows)\n", "results.head()\n", "\n", "# CHECKPOINT\n", - "expected_cols = {\n", - " 'sample', 'gen_obj', 'base_obj', 'gen_minus_base', 'gen_violations', 'base_violations'\n", - "}\n", + "expected_cols = {\"sample\", \"gen_obj\", \"base_obj\", \"gen_minus_base\", \"gen_violations\", \"base_violations\"}\n", "missing_cols = expected_cols.difference(results.columns)\n", - "assert not missing_cols, f'Missing result columns: {missing_cols}'\n", - "assert len(results) == len(gen_designs), 'results must have one row per sample'\n", - "print('Checkpoint passed: per-sample evaluation table is complete.')\n" + "assert not missing_cols, f\"Missing result columns: {missing_cols}\"\n", + "assert len(results) == len(gen_designs), \"results must have one row per sample\"\n", + "print(\"Checkpoint passed: per-sample evaluation table is complete.\")" ] }, { @@ -432,6 +432,7 @@ " nn_dists.append(float(np.min(d)))\n", " return float(np.mean(nn_dists))\n", "\n", + "\n", "# START FILL ---------------------------------------------------------------\n", "# Build one summary dict with at least these keys:\n", "# - n_samples\n", @@ -444,20 +445,27 @@ "# - gen_feasible_rate\n", "# - gen_diversity_l2\n", "# - gen_novelty_to_train_l2\n", - "raise NotImplementedError('Implement summary metric dictionary + summary_df')\n", + "raise NotImplementedError(\"Implement summary metric dictionary + summary_df\")\n", "# END FILL -----------------------------------------------------------------\n", "\n", "# CHECKPOINT\n", - "assert 'summary_df' in locals(), 'Define summary_df'\n", - "assert len(summary_df) == 1, 'summary_df should be one-row summary'\n", + "assert \"summary_df\" in locals(), \"Define summary_df\"\n", + "assert len(summary_df) == 1, \"summary_df should be one-row summary\"\n", "required_summary = {\n", - " 'n_samples', 'gen_obj_mean', 'base_obj_mean', 'objective_gap_mean', 'improvement_rate',\n", - " 'gen_violation_ratio', 'base_violation_ratio', 'gen_feasible_rate',\n", - " 'gen_diversity_l2', 'gen_novelty_to_train_l2'\n", + " \"n_samples\",\n", + " \"gen_obj_mean\",\n", + " \"base_obj_mean\",\n", + " \"objective_gap_mean\",\n", + " \"improvement_rate\",\n", + " \"gen_violation_ratio\",\n", + " \"base_violation_ratio\",\n", + " \"gen_feasible_rate\",\n", + " \"gen_diversity_l2\",\n", + " \"gen_novelty_to_train_l2\",\n", "}\n", "missing = required_summary.difference(summary_df.columns)\n", - "assert not missing, f'Missing summary columns: {missing}'\n", - "print('Checkpoint passed: summary table is ready for export/discussion.')\n" + "assert not missing, f\"Missing summary columns: {missing}\"\n", + "print(\"Checkpoint passed: summary table is ready for export/discussion.\")" ] }, { @@ -478,47 +486,47 @@ "outputs": [], "source": [ "# Export metrics and figures\n", - "results_path = ARTIFACT_DIR / 'per_sample_metrics.csv'\n", - "summary_path = ARTIFACT_DIR / 'metrics_summary.csv'\n", - "hist_path = ARTIFACT_DIR / 'objective_histogram.png'\n", - "grid_path = ARTIFACT_DIR / 'design_grid.png'\n", - "scatter_path = ARTIFACT_DIR / 'objective_scatter.png'\n", + "results_path = ARTIFACT_DIR / \"per_sample_metrics.csv\"\n", + "summary_path = ARTIFACT_DIR / \"metrics_summary.csv\"\n", + "hist_path = ARTIFACT_DIR / \"objective_histogram.png\"\n", + "grid_path = ARTIFACT_DIR / \"design_grid.png\"\n", + "scatter_path = ARTIFACT_DIR / \"objective_scatter.png\"\n", "\n", "results.to_csv(results_path, index=False)\n", "summary_df.to_csv(summary_path, index=False)\n", "\n", "fig, ax = plt.subplots(figsize=(7, 4))\n", - "ax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\n", - "ax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\n", - "ax.set_xlabel('Compliance objective (lower is better)')\n", - "ax.set_ylabel('Count')\n", - "ax.set_title('Generated vs baseline objective distribution')\n", + "ax.hist(results[\"gen_obj\"], bins=10, alpha=0.7, label=\"generated\")\n", + "ax.hist(results[\"base_obj\"], bins=10, alpha=0.7, label=\"baseline\")\n", + "ax.set_xlabel(\"Compliance objective (lower is better)\")\n", + "ax.set_ylabel(\"Count\")\n", + "ax.set_title(\"Generated vs baseline objective distribution\")\n", "ax.legend()\n", "fig.tight_layout()\n", "fig.savefig(hist_path, dpi=150)\n", "plt.show()\n", "\n", "fig2, ax2 = plt.subplots(figsize=(5, 5))\n", - "ax2.scatter(results['base_obj'], results['gen_obj'], alpha=0.8)\n", - "min_v = min(results['base_obj'].min(), results['gen_obj'].min())\n", - "max_v = max(results['base_obj'].max(), results['gen_obj'].max())\n", - "ax2.plot([min_v, max_v], [min_v, max_v], '--', color='black', linewidth=1)\n", - "ax2.set_xlabel('Baseline objective')\n", - "ax2.set_ylabel('Generated objective')\n", - "ax2.set_title('Per-sample objective comparison')\n", + "ax2.scatter(results[\"base_obj\"], results[\"gen_obj\"], alpha=0.8)\n", + "min_v = min(results[\"base_obj\"].min(), results[\"gen_obj\"].min())\n", + "max_v = max(results[\"base_obj\"].max(), results[\"gen_obj\"].max())\n", + "ax2.plot([min_v, max_v], [min_v, max_v], \"--\", color=\"black\", linewidth=1)\n", + "ax2.set_xlabel(\"Baseline objective\")\n", + "ax2.set_ylabel(\"Generated objective\")\n", + "ax2.set_title(\"Per-sample objective comparison\")\n", "fig2.tight_layout()\n", "fig2.savefig(scatter_path, dpi=150)\n", "plt.show()\n", "\n", - "print('Saved:')\n", - "print('-', results_path)\n", - "print('-', summary_path)\n", - "print('-', hist_path)\n", - "print('-', scatter_path)\n", + "print(\"Saved:\")\n", + "print(\"-\", results_path)\n", + "print(\"-\", summary_path)\n", + "print(\"-\", hist_path)\n", + "print(\"-\", scatter_path)\n", "\n", "# Optional advanced extension:\n", "# if USE_WANDB_ARTIFACTS:\n", - "# log summary metrics, tables, and images to W&B.\n" + "# log summary metrics, tables, and images to W&B." ] }, { @@ -545,15 +553,15 @@ " break\n", " pair_idx = i // 2\n", " if i % 2 == 0:\n", - " ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n", - " ax.set_title(f'gen {pair_idx}')\n", + " ax.imshow(gen_designs[pair_idx], cmap=\"gray\", vmin=0, vmax=1)\n", + " ax.set_title(f\"gen {pair_idx}\")\n", " else:\n", - " ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n", - " ax.set_title(f'base {pair_idx}')\n", - " ax.axis('off')\n", + " ax.imshow(baseline_designs[pair_idx], cmap=\"gray\", vmin=0, vmax=1)\n", + " ax.set_title(f\"base {pair_idx}\")\n", + " ax.axis(\"off\")\n", "fig.tight_layout()\n", "fig.savefig(grid_path, dpi=150)\n", - "plt.show()\n" + "plt.show()" ] }, { diff --git a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb index e2bada8..f9af837 100644 --- a/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/participant/03_add_new_problem_scaffold.ipynb @@ -69,30 +69,32 @@ "import subprocess\n", "import sys\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "\n", "def pip_install(packages: list[str]):\n", - " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", - " print('Running:', ' '.join(cmd))\n", + " cmd = [sys.executable, \"-m\", \"pip\", \"install\", *packages]\n", + " print(\"Running:\", \" \".join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium', 'pybullet']\n", - "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", + "\n", + "\n", + "BASE_PACKAGES = [\"engibench[beams2d]\", \"matplotlib\", \"gymnasium\", \"pybullet\"]\n", + "ENGIOPT_GIT = \"git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt\"\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print(\"Installing dependencies...\")\n", " pip_install(BASE_PACKAGES)\n", " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'])\n", + " pip_install([\"torch\", \"torchvision\"])\n", "\n", - " print('Dependency install complete.')\n", + " print(\"Dependency install complete.\")\n", "else:\n", - " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" + " print(\"Skipping install (using current environment). Set FORCE_INSTALL=True to install here.\")" ] }, { @@ -126,7 +128,7 @@ "from engibench.core import OptiStep\n", "from engibench.core import Problem\n", "\n", - "import pybullet as p\n" + "import pybullet as p" ] }, { @@ -206,7 +208,7 @@ " # (1) reachable_workspace(design, target_x, target_y, **_) -> assert reachable radius\n", " # (2) gain_consistency(design, **_) -> assert kd is reasonable for kp\n", " # Then assign: self.design_constraints = [reachable_workspace, gain_consistency]\n", - " raise NotImplementedError('Implement __init__ constraints')\n", + " raise NotImplementedError(\"Implement __init__ constraints\")\n", " # END FILL ---------------------------------------------------------\n", "\n", " def _build_robot(self, l1: float, l2: float, payload_kg: float, damping: float) -> tuple[int, int]:\n", @@ -217,21 +219,21 @@ " # - create a 2-link articulated body\n", " # - apply damping to joints\n", " # - return (robot_id, end_effector_link_index)\n", - " raise NotImplementedError('Implement _build_robot')\n", + " raise NotImplementedError(\"Implement _build_robot\")\n", " # END FILL ---------------------------------------------------------\n", "\n", " def _inverse_kinematics_2link(self, x: float, y: float, l1: float, l2: float) -> tuple[float, float]:\n", " # PUBLIC FILL-IN 03-A3: 2-link IK\n", " # START FILL -------------------------------------------------------\n", " # Implement stable closed-form IK with clipping for numerical robustness.\n", - " raise NotImplementedError('Implement _inverse_kinematics_2link')\n", + " raise NotImplementedError(\"Implement _inverse_kinematics_2link\")\n", " # END FILL ---------------------------------------------------------\n", "\n", " def _forward_kinematics_2link(self, q1: float, q2: float, l1: float, l2: float) -> tuple[float, float]:\n", " # PUBLIC FILL-IN 03-A4: 2-link FK\n", " # START FILL -------------------------------------------------------\n", " # Return end-effector (x, y) from joint angles and link lengths.\n", - " raise NotImplementedError('Implement _forward_kinematics_2link')\n", + " raise NotImplementedError(\"Implement _forward_kinematics_2link\")\n", " # END FILL ---------------------------------------------------------\n", "\n", " def _rollout(self, design: np.ndarray, cfg: dict, return_trace: bool = False):\n", @@ -243,14 +245,14 @@ " # Tips:\n", " # - connect with p.DIRECT, always disconnect in finally\n", " # - apply optional disturbances from cfg['disturbance_scale']\n", - " raise NotImplementedError('Implement _rollout')\n", + " raise NotImplementedError(\"Implement _rollout\")\n", " # END FILL ---------------------------------------------------------\n", "\n", " def simulate(self, design: np.ndarray, config: dict | None = None) -> np.ndarray:\n", " # PUBLIC FILL-IN 03-A6: benchmark simulation wrapper\n", " # START FILL -------------------------------------------------------\n", " # Merge cfg, clip design to bounds, return objective vector from _rollout.\n", - " raise NotImplementedError('Implement simulate')\n", + " raise NotImplementedError(\"Implement simulate\")\n", " # END FILL ---------------------------------------------------------\n", "\n", " def optimize(self, starting_point: np.ndarray, config: dict | None = None):\n", @@ -258,7 +260,7 @@ " # START FILL -------------------------------------------------------\n", " # Implement local search and return (best_design, history[OptiStep]).\n", " # Keep deterministic behavior via self.np_random.\n", - " raise NotImplementedError('Implement optimize')\n", + " raise NotImplementedError(\"Implement optimize\")\n", " # END FILL ---------------------------------------------------------\n", "\n", " def render(self, design: np.ndarray, *, open_window: bool = False):\n", @@ -267,15 +269,15 @@ " # Create 4 panels:\n", " # (1) design vars, (2) task-space path + target,\n", " # (3) error over time, (4) torque over time.\n", - " raise NotImplementedError('Implement render')\n", + " raise NotImplementedError(\"Implement render\")\n", " # END FILL ---------------------------------------------------------\n", "\n", " def random_design(self):\n", " # PUBLIC FILL-IN 03-A9: random design sampler\n", " # START FILL -------------------------------------------------------\n", " # Return a random design in bounds and dummy reward -1.\n", - " raise NotImplementedError('Implement random_design')\n", - " # END FILL ---------------------------------------------------------\n" + " raise NotImplementedError(\"Implement random_design\")\n", + " # END FILL ---------------------------------------------------------" ] }, { @@ -317,38 +319,38 @@ "start, _ = problem.random_design()\n", "\n", "cfg = {\n", - " 'target_x': 0.9,\n", - " 'target_y': 0.45,\n", - " 'payload_kg': 0.8,\n", - " 'disturbance_scale': 0.04,\n", - " 'sim_steps': 220,\n", - " 'dt': 1.0 / 120.0,\n", - " 'torque_limit': 12.0,\n", - " 'max_iter': 40,\n", + " \"target_x\": 0.9,\n", + " \"target_y\": 0.45,\n", + " \"payload_kg\": 0.8,\n", + " \"disturbance_scale\": 0.04,\n", + " \"sim_steps\": 220,\n", + " \"dt\": 1.0 / 120.0,\n", + " \"torque_limit\": 12.0,\n", + " \"max_iter\": 40,\n", "}\n", "\n", - "print('design space:', problem.design_space)\n", - "print('objectives:', problem.objectives)\n", - "print('conditions:', problem.conditions)\n", + "print(\"design space:\", problem.design_space)\n", + "print(\"objectives:\", problem.objectives)\n", + "print(\"conditions:\", problem.conditions)\n", "\n", "viol = problem.check_constraints(start, config=cfg)\n", - "print('constraint violations:', len(viol))\n", + "print(\"constraint violations:\", len(viol))\n", "\n", "obj0 = problem.simulate(start, config=cfg)\n", "opt_design, history = problem.optimize(start, config=cfg)\n", "objf = problem.simulate(opt_design, config=cfg)\n", "\n", - "print('initial objectives [tracking_error_m, energy_J]:', obj0.tolist())\n", - "print('final objectives [tracking_error_m, energy_J]:', objf.tolist())\n", - "print('optimization steps:', len(history))\n", - "print('How to read plots: vars | task-space path | error timeline | torque timeline')\n", + "print(\"initial objectives [tracking_error_m, energy_J]:\", obj0.tolist())\n", + "print(\"final objectives [tracking_error_m, energy_J]:\", objf.tolist())\n", + "print(\"optimization steps:\", len(history))\n", + "print(\"How to read plots: vars | task-space path | error timeline | torque timeline\")\n", "\n", "# CHECKPOINT\n", - "assert len(history) > 0, 'Optimization history should not be empty'\n", - "assert np.all(np.isfinite(obj0)), 'Initial objective contains non-finite values'\n", - "assert np.all(np.isfinite(objf)), 'Final objective contains non-finite values'\n", + "assert len(history) > 0, \"Optimization history should not be empty\"\n", + "assert np.all(np.isfinite(obj0)), \"Initial objective contains non-finite values\"\n", + "assert np.all(np.isfinite(objf)), \"Final objective contains non-finite values\"\n", "\n", - "problem.render(opt_design)\n" + "problem.render(opt_design)" ] }, { @@ -436,20 +438,20 @@ "EPOCHS = 30\n", "BATCH_SIZE = 64\n", "LATENT_DIM = 8\n", - "FAST_SIM_CFG = {'sim_steps': 80, 'dt': 1.0 / 120.0}\n", + "FAST_SIM_CFG = {\"sim_steps\": 80, \"dt\": 1.0 / 120.0}\n", "EVAL_SAMPLES = 40\n", "\n", - "if 'problem' not in globals():\n", + "if \"problem\" not in globals():\n", " problem = PlanarManipulatorCoDesignProblem(seed=7)\n", "\n", - "if 'google.colab' in sys.modules:\n", - " OPTIONAL_ARTIFACT_DIR = Path('/content/dcc26_optional_artifacts')\n", + "if \"google.colab\" in sys.modules:\n", + " OPTIONAL_ARTIFACT_DIR = Path(\"/content/dcc26_optional_artifacts\")\n", "else:\n", - " OPTIONAL_ARTIFACT_DIR = Path('workshops/dcc26/optional_artifacts')\n", + " OPTIONAL_ARTIFACT_DIR = Path(\"workshops/dcc26/optional_artifacts\")\n", "OPTIONAL_ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", "\n", - "print(f'Optional artifacts dir: {OPTIONAL_ARTIFACT_DIR.resolve()}')\n", - "print('Optional section enabled:' if RUN_OPTIONAL_SECTION else 'Optional section disabled:', RUN_OPTIONAL_SECTION)\n" + "print(f\"Optional artifacts dir: {OPTIONAL_ARTIFACT_DIR.resolve()}\")\n", + "print(\"Optional section enabled:\" if RUN_OPTIONAL_SECTION else \"Optional section disabled:\", RUN_OPTIONAL_SECTION)" ] }, { @@ -465,20 +467,23 @@ "\n", "def sample_condition_dict() -> dict:\n", " return {\n", - " 'target_x': float(rng.uniform(0.20, 1.35)),\n", - " 'target_y': float(rng.uniform(0.05, 1.20)),\n", - " 'payload_kg': float(rng.uniform(0.0, 2.0)),\n", - " 'disturbance_scale': float(rng.uniform(0.0, 0.30)),\n", + " \"target_x\": float(rng.uniform(0.20, 1.35)),\n", + " \"target_y\": float(rng.uniform(0.05, 1.20)),\n", + " \"payload_kg\": float(rng.uniform(0.0, 2.0)),\n", + " \"disturbance_scale\": float(rng.uniform(0.0, 0.30)),\n", " }\n", "\n", "\n", "def cond_to_vec(cfg: dict) -> np.ndarray:\n", - " return np.array([\n", - " cfg['target_x'],\n", - " cfg['target_y'],\n", - " cfg['payload_kg'],\n", - " cfg['disturbance_scale'],\n", - " ], dtype=np.float32)\n", + " return np.array(\n", + " [\n", + " cfg[\"target_x\"],\n", + " cfg[\"target_y\"],\n", + " cfg[\"payload_kg\"],\n", + " cfg[\"disturbance_scale\"],\n", + " ],\n", + " dtype=np.float32,\n", + " )\n", "\n", "\n", "def objective_score(obj: np.ndarray) -> float:\n", @@ -504,10 +509,10 @@ " objs.append(obj.astype(np.float32))\n", "\n", " if len(designs) % 40 == 0:\n", - " print(f'Collected feasible samples: {len(designs)}/{n_feasible}')\n", + " print(f\"Collected feasible samples: {len(designs)}/{n_feasible}\")\n", "\n", " if len(designs) < max(48, n_feasible // 3):\n", - " raise RuntimeError(f'Not enough feasible samples ({len(designs)}).')\n", + " raise RuntimeError(f\"Not enough feasible samples ({len(designs)}).\")\n", "\n", " designs = np.stack(designs)\n", " conds = np.stack(conds)\n", @@ -518,26 +523,26 @@ " top_idx = np.argsort(scores)[:keep_n]\n", "\n", " data = {\n", - " 'designs_all': designs,\n", - " 'conditions_all': conds,\n", - " 'objectives_all': objs,\n", - " 'scores_all': scores,\n", - " 'designs_top': designs[top_idx],\n", - " 'conditions_top': conds[top_idx],\n", - " 'objectives_top': objs[top_idx],\n", - " 'scores_top': scores[top_idx],\n", + " \"designs_all\": designs,\n", + " \"conditions_all\": conds,\n", + " \"objectives_all\": objs,\n", + " \"scores_all\": scores,\n", + " \"designs_top\": designs[top_idx],\n", + " \"conditions_top\": conds[top_idx],\n", + " \"objectives_top\": objs[top_idx],\n", + " \"scores_top\": scores[top_idx],\n", " }\n", " return data\n", "\n", "\n", "if RUN_OPTIONAL_SECTION:\n", " dataset = make_dataset(problem, N_FEASIBLE_SAMPLES)\n", - " np.savez(OPTIONAL_ARTIFACT_DIR / 'manipulator_dataset.npz', **dataset)\n", - " print('Saved dataset:', OPTIONAL_ARTIFACT_DIR / 'manipulator_dataset.npz')\n", - " print('All samples:', dataset['designs_all'].shape[0], '| Top samples:', dataset['designs_top'].shape[0])\n", + " np.savez(OPTIONAL_ARTIFACT_DIR / \"manipulator_dataset.npz\", **dataset)\n", + " print(\"Saved dataset:\", OPTIONAL_ARTIFACT_DIR / \"manipulator_dataset.npz\")\n", + " print(\"All samples:\", dataset[\"designs_all\"].shape[0], \"| Top samples:\", dataset[\"designs_top\"].shape[0])\n", "else:\n", " dataset = None\n", - " print('Skipped dataset creation. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + " print(\"Skipped dataset creation. Set RUN_OPTIONAL_SECTION=True to run this block.\")" ] }, { @@ -568,10 +573,10 @@ "if RUN_OPTIONAL_SECTION:\n", " import engiopt.cgan_1d.cgan_1d as cgan1d\n", "\n", - " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", + " device = th.device(\"cuda\" if th.cuda.is_available() else \"cpu\")\n", "\n", - " x_cond = dataset['conditions_top'].astype(np.float32)\n", - " y_design = dataset['designs_top'].astype(np.float32)\n", + " x_cond = dataset[\"conditions_top\"].astype(np.float32)\n", + " y_design = dataset[\"designs_top\"].astype(np.float32)\n", "\n", " cond_t = th.tensor(x_cond, dtype=th.float32, device=device)\n", " design_t = th.tensor(y_design, dtype=th.float32, device=device)\n", @@ -642,25 +647,25 @@ " g_hist.append(g_epoch / max(1, n_steps))\n", " d_hist.append(d_epoch / max(1, n_steps))\n", " if epoch == 1 or epoch % 5 == 0 or epoch == EPOCHS:\n", - " print(f'Epoch {epoch:02d}/{EPOCHS} | g_loss={g_hist[-1]:.6f} | d_loss={d_hist[-1]:.6f}')\n", + " print(f\"Epoch {epoch:02d}/{EPOCHS} | g_loss={g_hist[-1]:.6f} | d_loss={d_hist[-1]:.6f}\")\n", "\n", " th.save(\n", " {\n", - " 'generator': generator.state_dict(),\n", - " 'discriminator': discriminator.state_dict(),\n", - " 'cond_min': cond_min.cpu(),\n", - " 'cond_max': cond_max.cpu(),\n", - " 'design_min': design_min.cpu(),\n", - " 'design_max': design_max.cpu(),\n", + " \"generator\": generator.state_dict(),\n", + " \"discriminator\": discriminator.state_dict(),\n", + " \"cond_min\": cond_min.cpu(),\n", + " \"cond_max\": cond_max.cpu(),\n", + " \"design_min\": design_min.cpu(),\n", + " \"design_max\": design_max.cpu(),\n", " },\n", - " OPTIONAL_ARTIFACT_DIR / 'engiopt_cgan1d_weights.pt',\n", + " OPTIONAL_ARTIFACT_DIR / \"engiopt_cgan1d_weights.pt\",\n", " )\n", - " print('Saved model:', OPTIONAL_ARTIFACT_DIR / 'engiopt_cgan1d_weights.pt')\n", + " print(\"Saved model:\", OPTIONAL_ARTIFACT_DIR / \"engiopt_cgan1d_weights.pt\")\n", "else:\n", " generator, discriminator = None, None\n", " g_hist, d_hist = [], []\n", - " device = th.device('cpu')\n", - " print('Skipped model training. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + " device = th.device(\"cpu\")\n", + " print(\"Skipped model training. Set RUN_OPTIONAL_SECTION=True to run this block.\")" ] }, { @@ -735,19 +740,19 @@ " base_objs = np.stack(base_objs)\n", "\n", " summary = {\n", - " 'generated_error_mean': float(np.mean(gen_objs[:, 0])),\n", - " 'generated_energy_mean': float(np.mean(gen_objs[:, 1])),\n", - " 'baseline_error_mean': float(np.mean(base_objs[:, 0])),\n", - " 'baseline_energy_mean': float(np.mean(base_objs[:, 1])),\n", - " 'generated_feasible_rate': float(feasible_count / EVAL_SAMPLES),\n", + " \"generated_error_mean\": float(np.mean(gen_objs[:, 0])),\n", + " \"generated_energy_mean\": float(np.mean(gen_objs[:, 1])),\n", + " \"baseline_error_mean\": float(np.mean(base_objs[:, 0])),\n", + " \"baseline_energy_mean\": float(np.mean(base_objs[:, 1])),\n", + " \"generated_feasible_rate\": float(feasible_count / EVAL_SAMPLES),\n", " }\n", "\n", - " print('Optional extension summary (EngiOpt cgan_1d):')\n", + " print(\"Optional extension summary (EngiOpt cgan_1d):\")\n", " for k, v in summary.items():\n", - " print(f' {k}: {v:.6f}')\n", + " print(f\" {k}: {v:.6f}\")\n", "\n", " np.savez(\n", - " OPTIONAL_ARTIFACT_DIR / 'optional_eval_summary_engiopt_cgan1d.npz',\n", + " OPTIONAL_ARTIFACT_DIR / \"optional_eval_summary_engiopt_cgan1d.npz\",\n", " gen_objs=gen_objs,\n", " base_objs=base_objs,\n", " g_hist=np.array(g_hist, dtype=np.float32),\n", @@ -757,29 +762,29 @@ "\n", " fig, axes = plt.subplots(1, 3, figsize=(14, 4))\n", "\n", - " axes[0].plot(g_hist, label='g_loss')\n", - " axes[0].plot(d_hist, label='d_loss')\n", - " axes[0].set_title('EngiOpt cgan_1d training losses')\n", - " axes[0].set_xlabel('epoch')\n", + " axes[0].plot(g_hist, label=\"g_loss\")\n", + " axes[0].plot(d_hist, label=\"d_loss\")\n", + " axes[0].set_title(\"EngiOpt cgan_1d training losses\")\n", + " axes[0].set_xlabel(\"epoch\")\n", " axes[0].legend()\n", " axes[0].grid(alpha=0.3)\n", "\n", - " axes[1].hist(base_objs[:, 0], bins=12, alpha=0.6, label='baseline')\n", - " axes[1].hist(gen_objs[:, 0], bins=12, alpha=0.6, label='generated')\n", - " axes[1].set_title('Final tracking error')\n", - " axes[1].set_xlabel('error [m]')\n", + " axes[1].hist(base_objs[:, 0], bins=12, alpha=0.6, label=\"baseline\")\n", + " axes[1].hist(gen_objs[:, 0], bins=12, alpha=0.6, label=\"generated\")\n", + " axes[1].set_title(\"Final tracking error\")\n", + " axes[1].set_xlabel(\"error [m]\")\n", " axes[1].legend()\n", "\n", - " axes[2].hist(base_objs[:, 1], bins=12, alpha=0.6, label='baseline')\n", - " axes[2].hist(gen_objs[:, 1], bins=12, alpha=0.6, label='generated')\n", - " axes[2].set_title('Actuation energy')\n", - " axes[2].set_xlabel('energy [J]')\n", + " axes[2].hist(base_objs[:, 1], bins=12, alpha=0.6, label=\"baseline\")\n", + " axes[2].hist(gen_objs[:, 1], bins=12, alpha=0.6, label=\"generated\")\n", + " axes[2].set_title(\"Actuation energy\")\n", + " axes[2].set_xlabel(\"energy [J]\")\n", " axes[2].legend()\n", "\n", " fig.tight_layout()\n", " plt.show()\n", "else:\n", - " print('Skipped evaluation. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + " print(\"Skipped evaluation. Set RUN_OPTIONAL_SECTION=True to run this block.\")" ] }, { diff --git a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb index 403658e..d3d515e 100644 --- a/workshops/dcc26/solutions/00_setup_api_warmup.ipynb +++ b/workshops/dcc26/solutions/00_setup_api_warmup.ipynb @@ -76,28 +76,30 @@ "import subprocess\n", "import sys\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "\n", "def pip_install(packages: list[str]):\n", - " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", - " print('Running:', ' '.join(cmd))\n", + " cmd = [sys.executable, \"-m\", \"pip\", \"install\", *packages]\n", + " print(\"Running:\", \" \".join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'seaborn']\n", + "\n", + "\n", + "BASE_PACKAGES = [\"engibench[beams2d]\", \"matplotlib\", \"seaborn\"]\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print(\"Installing dependencies...\")\n", " pip_install(BASE_PACKAGES)\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'])\n", + " pip_install([\"torch\", \"torchvision\"])\n", "\n", - " print('Dependency install complete.')\n", + " print(\"Dependency install complete.\")\n", "else:\n", - " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" + " print(\"Skipping install (using current environment). Set FORCE_INSTALL=True to install here.\")" ] }, { @@ -128,8 +130,8 @@ "random.seed(SEED)\n", "np.random.seed(SEED)\n", "\n", - "print('engibench version:', engibench.__version__)\n", - "print('seed:', SEED)" + "print(\"engibench version:\", engibench.__version__)\n", + "print(\"seed:\", SEED)" ] }, { @@ -151,12 +153,12 @@ "source": [ "problem = Beams2D(seed=SEED)\n", "\n", - "print('Problem class:', type(problem).__name__)\n", - "print('Design space:', problem.design_space)\n", - "print('Objectives:', problem.objectives)\n", - "print('Conditions instance:', problem.conditions)\n", - "print('Condition keys:', problem.conditions_keys)\n", - "print('Dataset ID:', problem.dataset_id)" + "print(\"Problem class:\", type(problem).__name__)\n", + "print(\"Design space:\", problem.design_space)\n", + "print(\"Objectives:\", problem.objectives)\n", + "print(\"Conditions instance:\", problem.conditions)\n", + "print(\"Condition keys:\", problem.conditions_keys)\n", + "print(\"Dataset ID:\", problem.dataset_id)" ] }, { @@ -180,11 +182,11 @@ "print(dataset)\n", "\n", "sample_idx = 0\n", - "design = np.array(dataset['train']['optimal_design'][sample_idx])\n", - "config = {k: dataset['train'][k][sample_idx] for k in problem.conditions_keys}\n", + "design = np.array(dataset[\"train\"][\"optimal_design\"][sample_idx])\n", + "config = {k: dataset[\"train\"][k][sample_idx] for k in problem.conditions_keys}\n", "\n", - "print('Sample design shape:', design.shape)\n", - "print('Sample config:', config)" + "print(\"Sample design shape:\", design.shape)\n", + "print(\"Sample config:\", config)" ] }, { @@ -205,7 +207,7 @@ "outputs": [], "source": [ "fig, ax = problem.render(design)\n", - "ax.set_title('Sample Beams2D design from training split')\n", + "ax.set_title(\"Sample Beams2D design from training split\")\n", "plt.show()" ] }, @@ -228,14 +230,14 @@ "source": [ "# One explicit constraint check with intentionally mismatched volume fraction\n", "bad_config = dict(config)\n", - "bad_config['volfrac'] = 0.2\n", + "bad_config[\"volfrac\"] = 0.2\n", "violations = problem.check_constraints(design=design, config=bad_config)\n", "\n", - "print('Violation count:', len(violations))\n", + "print(\"Violation count:\", len(violations))\n", "if violations:\n", " print(violations)\n", "else:\n", - " print('No violations found')" + " print(\"No violations found\")" ] }, { diff --git a/workshops/dcc26/solutions/01_train_generate.ipynb b/workshops/dcc26/solutions/01_train_generate.ipynb index 17240ec..8750768 100644 --- a/workshops/dcc26/solutions/01_train_generate.ipynb +++ b/workshops/dcc26/solutions/01_train_generate.ipynb @@ -55,30 +55,32 @@ "import subprocess\n", "import sys\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "\n", "def pip_install(packages: list[str]):\n", - " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", - " print('Running:', ' '.join(cmd))\n", + " cmd = [sys.executable, \"-m\", \"pip\", \"install\", *packages]\n", + " print(\"Running:\", \" \".join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'matplotlib', 'tqdm', 'tyro', 'wandb']\n", - "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", + "\n", + "\n", + "BASE_PACKAGES = [\"engibench[beams2d]\", \"sqlitedict\", \"matplotlib\", \"tqdm\", \"tyro\", \"wandb\"]\n", + "ENGIOPT_GIT = \"git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt\"\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print(\"Installing dependencies...\")\n", " pip_install(BASE_PACKAGES)\n", " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'])\n", + " pip_install([\"torch\", \"torchvision\"])\n", "\n", - " print('Dependency install complete.')\n", + " print(\"Dependency install complete.\")\n", "else:\n", - " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" + " print(\"Skipping install (using current environment). Set FORCE_INSTALL=True to install here.\")" ] }, { @@ -137,21 +139,21 @@ " from engiopt.cgan_2d.cgan_2d import Generator as EngiOptCGAN2DGenerator\n", "except ModuleNotFoundError as exc:\n", " raise ModuleNotFoundError(\n", - " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", + " \"Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.\"\n", " ) from exc\n", "\n", "# Optional W&B integration\n", "USE_WANDB_ARTIFACTS = False\n", - "WANDB_PROJECT = 'dcc26-workshop'\n", + "WANDB_PROJECT = \"dcc26-workshop\"\n", "WANDB_ENTITY = None\n", - "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", - "WANDB_ARTIFACT_ALIAS = 'latest'\n", + "WANDB_ARTIFACT_NAME = \"dcc26_beams2d_generated_artifacts\"\n", + "WANDB_ARTIFACT_ALIAS = \"latest\"\n", "WANDB_LOG_TRAINING = True\n", "\n", "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", - " in_colab = 'google.colab' in sys.modules\n", - " path = Path('/content/dcc26_artifacts') if in_colab else Path('workshops/dcc26/artifacts')\n", + " in_colab = \"google.colab\" in sys.modules\n", + " path = Path(\"/content/dcc26_artifacts\") if in_colab else Path(\"workshops/dcc26/artifacts\")\n", " if create:\n", " path.mkdir(parents=True, exist_ok=True)\n", " return path\n", @@ -164,16 +166,16 @@ "if th.cuda.is_available():\n", " th.cuda.manual_seed_all(SEED)\n", "\n", - "DEVICE = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", - "print('device:', DEVICE)\n", + "DEVICE = th.device(\"cuda\" if th.cuda.is_available() else \"cpu\")\n", + "print(\"device:\", DEVICE)\n", "\n", "ARTIFACT_DIR = resolve_artifact_dir(create=True)\n", - "print('artifact dir:', ARTIFACT_DIR)\n", + "print(\"artifact dir:\", ARTIFACT_DIR)\n", "\n", - "CKPT_PATH = ARTIFACT_DIR / 'engiopt_cgan2d_generator_supervised.pt'\n", - "HISTORY_PATH = ARTIFACT_DIR / 'training_history.csv'\n", - "TRAIN_CURVE_PATH = ARTIFACT_DIR / 'training_curve.png'\n", - "LATENT_DIM = 32\n" + "CKPT_PATH = ARTIFACT_DIR / \"engiopt_cgan2d_generator_supervised.pt\"\n", + "HISTORY_PATH = ARTIFACT_DIR / \"training_history.csv\"\n", + "TRAIN_CURVE_PATH = ARTIFACT_DIR / \"training_curve.png\"\n", + "LATENT_DIM = 32" ] }, { @@ -215,24 +217,24 @@ "outputs": [], "source": [ "problem = Beams2D(seed=SEED)\n", - "train_ds = problem.dataset['train']\n", - "test_ds = problem.dataset['test']\n", + "train_ds = problem.dataset[\"train\"]\n", + "test_ds = problem.dataset[\"test\"]\n", "\n", "condition_keys = problem.conditions_keys\n", - "print('condition keys:', condition_keys)\n", + "print(\"condition keys:\", condition_keys)\n", "\n", "N_TRAIN = 512\n", "subset_idx = np.random.default_rng(SEED).choice(len(train_ds), size=N_TRAIN, replace=False)\n", "\n", "conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", - "designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", + "designs_np = np.array(train_ds[\"optimal_design\"])[subset_idx].astype(np.float32)\n", "\n", "# EngiOpt CGAN generator emits tanh-scaled outputs in [-1, 1].\n", "targets_np = (designs_np * 2.0) - 1.0\n", "\n", - "print('conditions shape:', conds_np.shape)\n", - "print('designs shape:', designs_np.shape)\n", - "print('target range:', float(targets_np.min()), 'to', float(targets_np.max()))\n" + "print(\"conditions shape:\", conds_np.shape)\n", + "print(\"designs shape:\", designs_np.shape)\n", + "print(\"target range:\", float(targets_np.min()), \"to\", float(targets_np.max()))" ] }, { @@ -263,7 +265,7 @@ "\n", "\n", "def sample_noise(batch_size: int) -> th.Tensor:\n", - " return th.randn((batch_size, LATENT_DIM), device=DEVICE, dtype=th.float32)\n" + " return th.randn((batch_size, LATENT_DIM), device=DEVICE, dtype=th.float32)" ] }, { @@ -301,14 +303,14 @@ " wandb_train_run = wandb.init(\n", " project=WANDB_PROJECT,\n", " entity=WANDB_ENTITY,\n", - " job_type='train',\n", + " job_type=\"train\",\n", " config={\n", - " 'seed': SEED,\n", - " 'epochs': EPOCHS,\n", - " 'batch_size': BATCH_SIZE,\n", - " 'n_train': int(N_TRAIN),\n", - " 'latent_dim': LATENT_DIM,\n", - " 'model_family': 'engiopt.cgan_2d.Generator',\n", + " \"seed\": SEED,\n", + " \"epochs\": EPOCHS,\n", + " \"batch_size\": BATCH_SIZE,\n", + " \"n_train\": int(N_TRAIN),\n", + " \"latent_dim\": LATENT_DIM,\n", + " \"model_family\": \"engiopt.cgan_2d.Generator\",\n", " },\n", " reinit=True,\n", " )\n", @@ -332,30 +334,30 @@ "\n", " epoch_avg = epoch_loss / len(dl)\n", " train_losses.append(epoch_avg)\n", - " print(f'epoch {epoch + 1:02d}/{EPOCHS} - loss: {epoch_avg:.4f}')\n", + " print(f\"epoch {epoch + 1:02d}/{EPOCHS} - loss: {epoch_avg:.4f}\")\n", "\n", " if wandb_train_run is not None:\n", - " wandb_train_run.log({'train/loss': epoch_avg, 'epoch': epoch + 1})\n", + " wandb_train_run.log({\"train/loss\": epoch_avg, \"epoch\": epoch + 1})\n", "\n", " th.save(\n", " {\n", - " 'model': model.state_dict(),\n", - " 'condition_keys': condition_keys,\n", - " 'latent_dim': LATENT_DIM,\n", - " 'model_family': 'engiopt.cgan_2d.Generator',\n", + " \"model\": model.state_dict(),\n", + " \"condition_keys\": condition_keys,\n", + " \"latent_dim\": LATENT_DIM,\n", + " \"model_family\": \"engiopt.cgan_2d.Generator\",\n", " },\n", " CKPT_PATH,\n", " )\n", - " print('saved checkpoint to', CKPT_PATH)\n", + " print(\"saved checkpoint to\", CKPT_PATH)\n", "\n", - " history_df = pd.DataFrame({'epoch': np.arange(1, len(train_losses) + 1), 'train_loss': train_losses})\n", + " history_df = pd.DataFrame({\"epoch\": np.arange(1, len(train_losses) + 1), \"train_loss\": train_losses})\n", " history_df.to_csv(HISTORY_PATH, index=False)\n", "\n", " fig, ax = plt.subplots(figsize=(6, 3.5))\n", - " ax.plot(history_df['epoch'], history_df['train_loss'], marker='o')\n", - " ax.set_xlabel('Epoch')\n", - " ax.set_ylabel('MSE loss')\n", - " ax.set_title('Notebook 01 training curve')\n", + " ax.plot(history_df[\"epoch\"], history_df[\"train_loss\"], marker=\"o\")\n", + " ax.set_xlabel(\"Epoch\")\n", + " ax.set_ylabel(\"MSE loss\")\n", + " ax.set_title(\"Notebook 01 training curve\")\n", " ax.grid(alpha=0.3)\n", " fig.tight_layout()\n", " fig.savefig(TRAIN_CURVE_PATH, dpi=150)\n", @@ -364,20 +366,20 @@ " if wandb_train_run is not None:\n", " import wandb\n", "\n", - " wandb_train_run.log({'train/loss_curve': wandb.Image(str(TRAIN_CURVE_PATH))})\n", + " wandb_train_run.log({\"train/loss_curve\": wandb.Image(str(TRAIN_CURVE_PATH))})\n", " wandb_train_run.finish()\n", "elif CKPT_PATH.exists():\n", " ckpt = th.load(CKPT_PATH, map_location=DEVICE)\n", - " model.load_state_dict(ckpt['model'])\n", + " model.load_state_dict(ckpt[\"model\"])\n", " model.eval()\n", - " print('loaded checkpoint from', CKPT_PATH)\n", + " print(\"loaded checkpoint from\", CKPT_PATH)\n", "\n", " if HISTORY_PATH.exists():\n", " history_df = pd.read_csv(HISTORY_PATH)\n", " else:\n", - " history_df = pd.DataFrame(columns=['epoch', 'train_loss'])\n", + " history_df = pd.DataFrame(columns=[\"epoch\", \"train_loss\"])\n", "else:\n", - " raise FileNotFoundError(f'No checkpoint found at {CKPT_PATH}. Set TRAIN_FROM_SCRATCH=True or provide a checkpoint.')\n" + " raise FileNotFoundError(f\"No checkpoint found at {CKPT_PATH}. Set TRAIN_FROM_SCRATCH=True or provide a checkpoint.\")" ] }, { @@ -423,7 +425,7 @@ "selected = rng.choice(len(test_ds), size=N_SAMPLES, replace=False)\n", "\n", "test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", - "baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", + "baseline_designs = np.array(test_ds[\"optimal_design\"])[selected].astype(np.float32)\n", "\n", "model.eval()\n", "with th.no_grad():\n", @@ -436,17 +438,19 @@ " rec = {}\n", " for j, k in enumerate(condition_keys):\n", " v = test_conds[i, j]\n", - " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", + " rec[k] = bool(v) if k == \"overhang_constraint\" else float(v)\n", " conditions_records.append(rec)\n", "\n", "# Quick checks before full Notebook 02 evaluation\n", - "viol_ratio = np.mean([\n", - " len(problem.check_constraints(design=d, config=cfg)) > 0\n", - " for d, cfg in zip(gen_designs, conditions_records, strict=True)\n", - "])\n", - "print('generated shape:', gen_designs.shape)\n", - "print('baseline shape:', baseline_designs.shape)\n", - "print('generated violation ratio (quick check):', float(viol_ratio))\n" + "viol_ratio = np.mean(\n", + " [\n", + " len(problem.check_constraints(design=d, config=cfg)) > 0\n", + " for d, cfg in zip(gen_designs, conditions_records, strict=True)\n", + " ]\n", + ")\n", + "print(\"generated shape:\", gen_designs.shape)\n", + "print(\"baseline shape:\", baseline_designs.shape)\n", + "print(\"generated violation ratio (quick check):\", float(viol_ratio))" ] }, { @@ -467,46 +471,48 @@ "outputs": [], "source": [ "# Artifact contract consumed by Notebook 02 and optional W&B logging.\n", - "generated_path = ARTIFACT_DIR / 'generated_designs.npy'\n", - "baseline_path = ARTIFACT_DIR / 'baseline_designs.npy'\n", - "conditions_path = ARTIFACT_DIR / 'conditions.json'\n", + "generated_path = ARTIFACT_DIR / \"generated_designs.npy\"\n", + "baseline_path = ARTIFACT_DIR / \"baseline_designs.npy\"\n", + "conditions_path = ARTIFACT_DIR / \"conditions.json\"\n", "\n", "np.save(generated_path, gen_designs)\n", "np.save(baseline_path, baseline_designs)\n", - "with open(conditions_path, 'w', encoding='utf-8') as f:\n", + "with open(conditions_path, \"w\", encoding=\"utf-8\") as f:\n", " json.dump(conditions_records, f, indent=2)\n", "\n", - "print('Saved artifacts to', ARTIFACT_DIR)\n", - "print('-', generated_path)\n", - "print('-', baseline_path)\n", - "print('-', conditions_path)\n", - "print('-', CKPT_PATH)\n", - "print('-', HISTORY_PATH)\n", - "print('-', TRAIN_CURVE_PATH)\n", + "print(\"Saved artifacts to\", ARTIFACT_DIR)\n", + "print(\"-\", generated_path)\n", + "print(\"-\", baseline_path)\n", + "print(\"-\", conditions_path)\n", + "print(\"-\", CKPT_PATH)\n", + "print(\"-\", HISTORY_PATH)\n", + "print(\"-\", TRAIN_CURVE_PATH)\n", "\n", "if USE_WANDB_ARTIFACTS:\n", " try:\n", " import wandb\n", "\n", - " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='artifact-upload', reinit=True)\n", - " run.log({\n", - " 'artifact/generated_mean_density': float(gen_designs.mean()),\n", - " 'artifact/generated_std_density': float(gen_designs.std()),\n", - " })\n", + " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type=\"artifact-upload\", reinit=True)\n", + " run.log(\n", + " {\n", + " \"artifact/generated_mean_density\": float(gen_designs.mean()),\n", + " \"artifact/generated_std_density\": float(gen_designs.std()),\n", + " }\n", + " )\n", "\n", " artifact = wandb.Artifact(\n", " WANDB_ARTIFACT_NAME,\n", - " type='dataset',\n", - " description='DCC26 Notebook 01 artifacts: arrays, conditions, checkpoint, and training diagnostics.',\n", + " type=\"dataset\",\n", + " description=\"DCC26 Notebook 01 artifacts: arrays, conditions, checkpoint, and training diagnostics.\",\n", " )\n", " for path in [generated_path, baseline_path, conditions_path, CKPT_PATH, HISTORY_PATH, TRAIN_CURVE_PATH]:\n", " if path.exists():\n", " artifact.add_file(str(path))\n", " run.log_artifact(artifact, aliases=[WANDB_ARTIFACT_ALIAS])\n", " run.finish()\n", - " print('Uploaded artifacts to W&B:', WANDB_ARTIFACT_NAME)\n", + " print(\"Uploaded artifacts to W&B:\", WANDB_ARTIFACT_NAME)\n", " except Exception as exc:\n", - " print('W&B upload failed (continuing with local artifacts only):', exc)\n" + " print(\"W&B upload failed (continuing with local artifacts only):\", exc)" ] }, { @@ -529,16 +535,16 @@ "# Visual side-by-side snapshot (generated vs baseline)\n", "fig, axes = plt.subplots(2, 6, figsize=(14, 5))\n", "for i in range(6):\n", - " axes[0, i].imshow(gen_designs[i], cmap='gray', vmin=0, vmax=1)\n", - " axes[0, i].set_title(f'gen {i}')\n", - " axes[0, i].axis('off')\n", + " axes[0, i].imshow(gen_designs[i], cmap=\"gray\", vmin=0, vmax=1)\n", + " axes[0, i].set_title(f\"gen {i}\")\n", + " axes[0, i].axis(\"off\")\n", "\n", - " axes[1, i].imshow(baseline_designs[i], cmap='gray', vmin=0, vmax=1)\n", - " axes[1, i].set_title(f'base {i}')\n", - " axes[1, i].axis('off')\n", + " axes[1, i].imshow(baseline_designs[i], cmap=\"gray\", vmin=0, vmax=1)\n", + " axes[1, i].set_title(f\"base {i}\")\n", + " axes[1, i].axis(\"off\")\n", "\n", "fig.tight_layout()\n", - "plt.show()\n" + "plt.show()" ] }, { diff --git a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb index 04322c8..4122708 100644 --- a/workshops/dcc26/solutions/02_evaluate_metrics.ipynb +++ b/workshops/dcc26/solutions/02_evaluate_metrics.ipynb @@ -54,30 +54,32 @@ "import subprocess\n", "import sys\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "\n", "def pip_install(packages: list[str]):\n", - " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", - " print('Running:', ' '.join(cmd))\n", + " cmd = [sys.executable, \"-m\", \"pip\", \"install\", *packages]\n", + " print(\"Running:\", \" \".join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'sqlitedict', 'matplotlib', 'tqdm', 'tyro', 'wandb']\n", - "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", + "\n", + "\n", + "BASE_PACKAGES = [\"engibench[beams2d]\", \"sqlitedict\", \"matplotlib\", \"tqdm\", \"tyro\", \"wandb\"]\n", + "ENGIOPT_GIT = \"git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt\"\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print(\"Installing dependencies...\")\n", " pip_install(BASE_PACKAGES)\n", " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'])\n", + " pip_install([\"torch\", \"torchvision\"])\n", "\n", - " print('Dependency install complete.')\n", + " print(\"Dependency install complete.\")\n", "else:\n", - " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" + " print(\"Skipping install (using current environment). Set FORCE_INSTALL=True to install here.\")" ] }, { @@ -137,22 +139,22 @@ " from engiopt.cgan_2d.cgan_2d import Generator as EngiOptCGAN2DGenerator\n", "except ModuleNotFoundError as exc:\n", " raise ModuleNotFoundError(\n", - " 'Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.'\n", + " \"Could not import engiopt model class. Run the bootstrap cell first; on Colab, restart runtime after install if needed.\"\n", " ) from exc\n", "\n", "USE_WANDB_ARTIFACTS = False\n", - "WANDB_PROJECT = 'dcc26-workshop'\n", + "WANDB_PROJECT = \"dcc26-workshop\"\n", "WANDB_ENTITY = None\n", - "WANDB_ARTIFACT_NAME = 'dcc26_beams2d_generated_artifacts'\n", - "WANDB_ARTIFACT_ALIAS = 'latest'\n", + "WANDB_ARTIFACT_NAME = \"dcc26_beams2d_generated_artifacts\"\n", + "WANDB_ARTIFACT_ALIAS = \"latest\"\n", "\n", "# Self-heal path for workshop robustness\n", "AUTO_BUILD_ARTIFACTS_IF_MISSING = True\n", "\n", "\n", "def resolve_artifact_dir(create: bool = False) -> Path:\n", - " in_colab = 'google.colab' in sys.modules\n", - " path = Path('/content/dcc26_artifacts') if in_colab else Path('workshops/dcc26/artifacts')\n", + " in_colab = \"google.colab\" in sys.modules\n", + " path = Path(\"/content/dcc26_artifacts\") if in_colab else Path(\"workshops/dcc26/artifacts\")\n", " if create:\n", " path.mkdir(parents=True, exist_ok=True)\n", " return path\n", @@ -167,7 +169,7 @@ " batch_size: int = 64,\n", " latent_dim: int = 32,\n", ") -> None:\n", - " print('Building Notebook 01-style artifacts locally with EngiOpt...')\n", + " print(\"Building Notebook 01-style artifacts locally with EngiOpt...\")\n", "\n", " random.seed(seed)\n", " np.random.seed(seed)\n", @@ -175,10 +177,10 @@ " if th.cuda.is_available():\n", " th.cuda.manual_seed_all(seed)\n", "\n", - " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", + " device = th.device(\"cuda\" if th.cuda.is_available() else \"cpu\")\n", " problem = Beams2D(seed=seed)\n", - " train_ds = problem.dataset['train']\n", - " test_ds = problem.dataset['test']\n", + " train_ds = problem.dataset[\"train\"]\n", + " test_ds = problem.dataset[\"test\"]\n", " condition_keys = problem.conditions_keys\n", "\n", " rng = np.random.default_rng(seed)\n", @@ -186,7 +188,7 @@ " subset_idx = rng.choice(len(train_ds), size=subset_size, replace=False)\n", "\n", " conds_np = np.stack([np.array(train_ds[k])[subset_idx].astype(np.float32) for k in condition_keys], axis=1)\n", - " designs_np = np.array(train_ds['optimal_design'])[subset_idx].astype(np.float32)\n", + " designs_np = np.array(train_ds[\"optimal_design\"])[subset_idx].astype(np.float32)\n", " targets_np = designs_np * 2.0 - 1.0\n", "\n", " model = EngiOptCGAN2DGenerator(\n", @@ -221,12 +223,12 @@ "\n", " epoch_avg = epoch_loss / len(dl)\n", " train_losses.append(epoch_avg)\n", - " print(f'bootstrap epoch {epoch + 1:02d}/{epochs} - loss: {epoch_avg:.4f}')\n", + " print(f\"bootstrap epoch {epoch + 1:02d}/{epochs} - loss: {epoch_avg:.4f}\")\n", "\n", " sample_count = min(n_samples, len(test_ds))\n", " selected = rng.choice(len(test_ds), size=sample_count, replace=False)\n", " test_conds = np.stack([np.array(test_ds[k])[selected].astype(np.float32) for k in condition_keys], axis=1)\n", - " baseline_designs = np.array(test_ds['optimal_design'])[selected].astype(np.float32)\n", + " baseline_designs = np.array(test_ds[\"optimal_design\"])[selected].astype(np.float32)\n", "\n", " model.eval()\n", " with th.no_grad():\n", @@ -239,37 +241,37 @@ " rec = {}\n", " for j, k in enumerate(condition_keys):\n", " v = test_conds[i, j]\n", - " rec[k] = bool(v) if k == 'overhang_constraint' else float(v)\n", + " rec[k] = bool(v) if k == \"overhang_constraint\" else float(v)\n", " conditions_records.append(rec)\n", "\n", " artifact_dir.mkdir(parents=True, exist_ok=True)\n", - " np.save(artifact_dir / 'generated_designs.npy', gen_designs)\n", - " np.save(artifact_dir / 'baseline_designs.npy', baseline_designs)\n", - " with open(artifact_dir / 'conditions.json', 'w', encoding='utf-8') as f:\n", + " np.save(artifact_dir / \"generated_designs.npy\", gen_designs)\n", + " np.save(artifact_dir / \"baseline_designs.npy\", baseline_designs)\n", + " with open(artifact_dir / \"conditions.json\", \"w\", encoding=\"utf-8\") as f:\n", " json.dump(conditions_records, f, indent=2)\n", "\n", - " pd.DataFrame({'epoch': np.arange(1, len(train_losses) + 1), 'train_loss': train_losses}).to_csv(\n", - " artifact_dir / 'training_history.csv', index=False\n", + " pd.DataFrame({\"epoch\": np.arange(1, len(train_losses) + 1), \"train_loss\": train_losses}).to_csv(\n", + " artifact_dir / \"training_history.csv\", index=False\n", " )\n", "\n", " th.save(\n", " {\n", - " 'model': model.state_dict(),\n", - " 'condition_keys': condition_keys,\n", - " 'latent_dim': latent_dim,\n", - " 'model_family': 'engiopt.cgan_2d.Generator',\n", + " \"model\": model.state_dict(),\n", + " \"condition_keys\": condition_keys,\n", + " \"latent_dim\": latent_dim,\n", + " \"model_family\": \"engiopt.cgan_2d.Generator\",\n", " },\n", - " artifact_dir / 'engiopt_cgan2d_generator_supervised.pt',\n", + " artifact_dir / \"engiopt_cgan2d_generator_supervised.pt\",\n", " )\n", "\n", - " print('Built artifacts at', artifact_dir)\n", + " print(\"Built artifacts at\", artifact_dir)\n", "\n", "\n", "ARTIFACT_DIR = resolve_artifact_dir(create=True)\n", "required = [\n", - " ARTIFACT_DIR / 'generated_designs.npy',\n", - " ARTIFACT_DIR / 'baseline_designs.npy',\n", - " ARTIFACT_DIR / 'conditions.json',\n", + " ARTIFACT_DIR / \"generated_designs.npy\",\n", + " ARTIFACT_DIR / \"baseline_designs.npy\",\n", + " ARTIFACT_DIR / \"conditions.json\",\n", "]\n", "\n", "if not all(p.exists() for p in required):\n", @@ -277,44 +279,44 @@ " try:\n", " import wandb\n", "\n", - " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='artifact-download', reinit=True)\n", + " run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type=\"artifact-download\", reinit=True)\n", " if WANDB_ENTITY:\n", " artifact_ref = f\"{WANDB_ENTITY}/{WANDB_PROJECT}/{WANDB_ARTIFACT_NAME}:{WANDB_ARTIFACT_ALIAS}\"\n", " else:\n", " artifact_ref = f\"{WANDB_PROJECT}/{WANDB_ARTIFACT_NAME}:{WANDB_ARTIFACT_ALIAS}\"\n", - " artifact = run.use_artifact(artifact_ref, type='dataset')\n", + " artifact = run.use_artifact(artifact_ref, type=\"dataset\")\n", " artifact.download(root=str(ARTIFACT_DIR))\n", " run.finish()\n", - " print('Downloaded artifacts from W&B to', ARTIFACT_DIR)\n", + " print(\"Downloaded artifacts from W&B to\", ARTIFACT_DIR)\n", " except Exception as exc:\n", " if AUTO_BUILD_ARTIFACTS_IF_MISSING:\n", - " print('W&B download failed; switching to local artifact build:', exc)\n", + " print(\"W&B download failed; switching to local artifact build:\", exc)\n", " build_artifacts_locally(ARTIFACT_DIR)\n", " else:\n", " raise FileNotFoundError(\n", - " 'Artifacts missing locally and W&B download failed. '\n", - " 'Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. '\n", - " f'Details: {exc}'\n", + " \"Artifacts missing locally and W&B download failed. \"\n", + " \"Run Notebook 01 first or disable USE_WANDB_ARTIFACTS. \"\n", + " f\"Details: {exc}\"\n", " ) from exc\n", " elif AUTO_BUILD_ARTIFACTS_IF_MISSING:\n", " build_artifacts_locally(ARTIFACT_DIR)\n", " else:\n", - " missing = '\\n'.join(f'- {p}' for p in required if not p.exists())\n", + " missing = \"\\n\".join(f\"- {p}\" for p in required if not p.exists())\n", " raise FileNotFoundError(\n", - " 'Notebook 01 artifacts not found. Run Notebook 01 first (including export cell), '\n", - " 'or enable USE_WANDB_ARTIFACTS to fetch from W&B. Missing files:\\\\n' + missing\n", + " \"Notebook 01 artifacts not found. Run Notebook 01 first (including export cell), \"\n", + " \"or enable USE_WANDB_ARTIFACTS to fetch from W&B. Missing files:\\\\n\" + missing\n", " )\n", "\n", - "print('using artifact dir:', ARTIFACT_DIR)\n", + "print(\"using artifact dir:\", ARTIFACT_DIR)\n", "\n", - "gen_designs = np.load(ARTIFACT_DIR / 'generated_designs.npy')\n", - "baseline_designs = np.load(ARTIFACT_DIR / 'baseline_designs.npy')\n", - "with open(ARTIFACT_DIR / 'conditions.json', encoding='utf-8') as f:\n", + "gen_designs = np.load(ARTIFACT_DIR / \"generated_designs.npy\")\n", + "baseline_designs = np.load(ARTIFACT_DIR / \"baseline_designs.npy\")\n", + "with open(ARTIFACT_DIR / \"conditions.json\", encoding=\"utf-8\") as f:\n", " conditions = json.load(f)\n", "\n", - "print('generated:', gen_designs.shape)\n", - "print('baseline:', baseline_designs.shape)\n", - "print('conditions:', len(conditions))\n" + "print(\"generated:\", gen_designs.shape)\n", + "print(\"baseline:\", baseline_designs.shape)\n", + "print(\"conditions:\", len(conditions))" ] }, { @@ -358,17 +360,19 @@ " problem.reset(seed=7)\n", " b_obj = float(problem.simulate(b, config=cfg)[0])\n", "\n", - " rows.append({\n", - " 'sample': i,\n", - " 'gen_obj': g_obj,\n", - " 'base_obj': b_obj,\n", - " 'gen_minus_base': g_obj - b_obj,\n", - " 'gen_violations': len(g_viol),\n", - " 'base_violations': len(b_viol),\n", - " })\n", + " rows.append(\n", + " {\n", + " \"sample\": i,\n", + " \"gen_obj\": g_obj,\n", + " \"base_obj\": b_obj,\n", + " \"gen_minus_base\": g_obj - b_obj,\n", + " \"gen_violations\": len(g_viol),\n", + " \"base_violations\": len(b_viol),\n", + " }\n", + " )\n", "\n", "results = pd.DataFrame(rows)\n", - "results.head()\n" + "results.head()" ] }, { @@ -410,25 +414,26 @@ " nn_dists.append(float(np.min(d)))\n", " return float(np.mean(nn_dists))\n", "\n", - "train_designs_full = np.array(problem.dataset['train']['optimal_design']).astype(np.float32)\n", + "\n", + "train_designs_full = np.array(problem.dataset[\"train\"][\"optimal_design\"]).astype(np.float32)\n", "ref_idx = np.random.default_rng(7).choice(len(train_designs_full), size=min(1024, len(train_designs_full)), replace=False)\n", "train_reference = train_designs_full[ref_idx]\n", "\n", "summary = {\n", - " 'n_samples': int(len(results)),\n", - " 'gen_obj_mean': float(results['gen_obj'].mean()),\n", - " 'base_obj_mean': float(results['base_obj'].mean()),\n", - " 'objective_gap_mean': float(results['gen_minus_base'].mean()),\n", - " 'improvement_rate': float((results['gen_obj'] < results['base_obj']).mean()),\n", - " 'gen_violation_ratio': float((results['gen_violations'] > 0).mean()),\n", - " 'base_violation_ratio': float((results['base_violations'] > 0).mean()),\n", - " 'gen_feasible_rate': float((results['gen_violations'] == 0).mean()),\n", - " 'gen_diversity_l2': mean_pairwise_l2(gen_designs),\n", - " 'gen_novelty_to_train_l2': mean_nn_distance_to_reference(gen_designs, train_reference),\n", + " \"n_samples\": int(len(results)),\n", + " \"gen_obj_mean\": float(results[\"gen_obj\"].mean()),\n", + " \"base_obj_mean\": float(results[\"base_obj\"].mean()),\n", + " \"objective_gap_mean\": float(results[\"gen_minus_base\"].mean()),\n", + " \"improvement_rate\": float((results[\"gen_obj\"] < results[\"base_obj\"]).mean()),\n", + " \"gen_violation_ratio\": float((results[\"gen_violations\"] > 0).mean()),\n", + " \"base_violation_ratio\": float((results[\"base_violations\"] > 0).mean()),\n", + " \"gen_feasible_rate\": float((results[\"gen_violations\"] == 0).mean()),\n", + " \"gen_diversity_l2\": mean_pairwise_l2(gen_designs),\n", + " \"gen_novelty_to_train_l2\": mean_nn_distance_to_reference(gen_designs, train_reference),\n", "}\n", "\n", "summary_df = pd.DataFrame([summary])\n", - "summary_df\n" + "summary_df" ] }, { @@ -448,64 +453,66 @@ "metadata": {}, "outputs": [], "source": [ - "results_path = ARTIFACT_DIR / 'per_sample_metrics.csv'\n", - "summary_path = ARTIFACT_DIR / 'metrics_summary.csv'\n", - "hist_path = ARTIFACT_DIR / 'objective_histogram.png'\n", - "grid_path = ARTIFACT_DIR / 'design_grid.png'\n", - "scatter_path = ARTIFACT_DIR / 'objective_scatter.png'\n", + "results_path = ARTIFACT_DIR / \"per_sample_metrics.csv\"\n", + "summary_path = ARTIFACT_DIR / \"metrics_summary.csv\"\n", + "hist_path = ARTIFACT_DIR / \"objective_histogram.png\"\n", + "grid_path = ARTIFACT_DIR / \"design_grid.png\"\n", + "scatter_path = ARTIFACT_DIR / \"objective_scatter.png\"\n", "\n", "results.to_csv(results_path, index=False)\n", "summary_df.to_csv(summary_path, index=False)\n", "\n", "fig, ax = plt.subplots(figsize=(7, 4))\n", - "ax.hist(results['gen_obj'], bins=10, alpha=0.7, label='generated')\n", - "ax.hist(results['base_obj'], bins=10, alpha=0.7, label='baseline')\n", - "ax.set_xlabel('Compliance objective (lower is better)')\n", - "ax.set_ylabel('Count')\n", - "ax.set_title('Generated vs baseline objective distribution')\n", + "ax.hist(results[\"gen_obj\"], bins=10, alpha=0.7, label=\"generated\")\n", + "ax.hist(results[\"base_obj\"], bins=10, alpha=0.7, label=\"baseline\")\n", + "ax.set_xlabel(\"Compliance objective (lower is better)\")\n", + "ax.set_ylabel(\"Count\")\n", + "ax.set_title(\"Generated vs baseline objective distribution\")\n", "ax.legend()\n", "fig.tight_layout()\n", "fig.savefig(hist_path, dpi=150)\n", "plt.show()\n", "\n", "fig2, ax2 = plt.subplots(figsize=(5, 5))\n", - "ax2.scatter(results['base_obj'], results['gen_obj'], alpha=0.8)\n", - "min_v = min(results['base_obj'].min(), results['gen_obj'].min())\n", - "max_v = max(results['base_obj'].max(), results['gen_obj'].max())\n", - "ax2.plot([min_v, max_v], [min_v, max_v], '--', color='black', linewidth=1)\n", - "ax2.set_xlabel('Baseline objective')\n", - "ax2.set_ylabel('Generated objective')\n", - "ax2.set_title('Per-sample objective comparison')\n", + "ax2.scatter(results[\"base_obj\"], results[\"gen_obj\"], alpha=0.8)\n", + "min_v = min(results[\"base_obj\"].min(), results[\"gen_obj\"].min())\n", + "max_v = max(results[\"base_obj\"].max(), results[\"gen_obj\"].max())\n", + "ax2.plot([min_v, max_v], [min_v, max_v], \"--\", color=\"black\", linewidth=1)\n", + "ax2.set_xlabel(\"Baseline objective\")\n", + "ax2.set_ylabel(\"Generated objective\")\n", + "ax2.set_title(\"Per-sample objective comparison\")\n", "fig2.tight_layout()\n", "fig2.savefig(scatter_path, dpi=150)\n", "plt.show()\n", "\n", - "print('Saved:')\n", - "print('-', results_path)\n", - "print('-', summary_path)\n", - "print('-', hist_path)\n", - "print('-', scatter_path)\n", + "print(\"Saved:\")\n", + "print(\"-\", results_path)\n", + "print(\"-\", summary_path)\n", + "print(\"-\", hist_path)\n", + "print(\"-\", scatter_path)\n", "\n", "if USE_WANDB_ARTIFACTS:\n", " try:\n", " import wandb\n", "\n", - " eval_run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type='evaluation', reinit=True)\n", - " eval_run.log({f'eval/{k}': v for k, v in summary.items()})\n", - " eval_run.log({\n", - " 'eval/objective_histogram': wandb.Image(str(hist_path)),\n", - " 'eval/objective_scatter': wandb.Image(str(scatter_path)),\n", - " 'eval/per_sample_table': wandb.Table(dataframe=results),\n", - " })\n", + " eval_run = wandb.init(project=WANDB_PROJECT, entity=WANDB_ENTITY, job_type=\"evaluation\", reinit=True)\n", + " eval_run.log({f\"eval/{k}\": v for k, v in summary.items()})\n", + " eval_run.log(\n", + " {\n", + " \"eval/objective_histogram\": wandb.Image(str(hist_path)),\n", + " \"eval/objective_scatter\": wandb.Image(str(scatter_path)),\n", + " \"eval/per_sample_table\": wandb.Table(dataframe=results),\n", + " }\n", + " )\n", "\n", - " eval_artifact = wandb.Artifact('dcc26_beams2d_eval_report', type='evaluation')\n", + " eval_artifact = wandb.Artifact(\"dcc26_beams2d_eval_report\", type=\"evaluation\")\n", " for p in [results_path, summary_path, hist_path, scatter_path]:\n", " eval_artifact.add_file(str(p))\n", " eval_run.log_artifact(eval_artifact, aliases=[WANDB_ARTIFACT_ALIAS])\n", " eval_run.finish()\n", - " print('Logged evaluation outputs to W&B.')\n", + " print(\"Logged evaluation outputs to W&B.\")\n", " except Exception as exc:\n", - " print('W&B evaluation logging failed (continuing locally):', exc)\n" + " print(\"W&B evaluation logging failed (continuing locally):\", exc)" ] }, { @@ -532,15 +539,15 @@ " break\n", " pair_idx = i // 2\n", " if i % 2 == 0:\n", - " ax.imshow(gen_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n", - " ax.set_title(f'gen {pair_idx}')\n", + " ax.imshow(gen_designs[pair_idx], cmap=\"gray\", vmin=0, vmax=1)\n", + " ax.set_title(f\"gen {pair_idx}\")\n", " else:\n", - " ax.imshow(baseline_designs[pair_idx], cmap='gray', vmin=0, vmax=1)\n", - " ax.set_title(f'base {pair_idx}')\n", - " ax.axis('off')\n", + " ax.imshow(baseline_designs[pair_idx], cmap=\"gray\", vmin=0, vmax=1)\n", + " ax.set_title(f\"base {pair_idx}\")\n", + " ax.axis(\"off\")\n", "fig.tight_layout()\n", "fig.savefig(grid_path, dpi=150)\n", - "plt.show()\n" + "plt.show()" ] }, { diff --git a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb index 372e495..50de285 100644 --- a/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb +++ b/workshops/dcc26/solutions/03_add_new_problem_scaffold.ipynb @@ -64,30 +64,32 @@ "import subprocess\n", "import sys\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "FORCE_INSTALL = False # Set True to force install outside Colab\n", "\n", "\n", "def pip_install(packages: list[str]):\n", - " cmd = [sys.executable, '-m', 'pip', 'install', *packages]\n", - " print('Running:', ' '.join(cmd))\n", + " cmd = [sys.executable, \"-m\", \"pip\", \"install\", *packages]\n", + " print(\"Running:\", \" \".join(cmd))\n", " subprocess.check_call(cmd)\n", - "BASE_PACKAGES = ['engibench[beams2d]', 'matplotlib', 'gymnasium', 'pybullet']\n", - "ENGIOPT_GIT = 'git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt'\n", + "\n", + "\n", + "BASE_PACKAGES = [\"engibench[beams2d]\", \"matplotlib\", \"gymnasium\", \"pybullet\"]\n", + "ENGIOPT_GIT = \"git+https://github.com/IDEALLab/EngiOpt.git@codex/dcc26-workshop-notebooks#egg=engiopt\"\n", "\n", "if IN_COLAB or FORCE_INSTALL:\n", - " print('Installing dependencies...')\n", + " print(\"Installing dependencies...\")\n", " pip_install(BASE_PACKAGES)\n", " pip_install([ENGIOPT_GIT])\n", "\n", " try:\n", " import torch # noqa: F401\n", " except Exception:\n", - " pip_install(['torch', 'torchvision'])\n", + " pip_install([\"torch\", \"torchvision\"])\n", "\n", - " print('Dependency install complete.')\n", + " print(\"Dependency install complete.\")\n", "else:\n", - " print('Skipping install (using current environment). Set FORCE_INSTALL=True to install here.')\n" + " print(\"Skipping install (using current environment). Set FORCE_INSTALL=True to install here.\")" ] }, { @@ -121,7 +123,7 @@ "from engibench.core import OptiStep\n", "from engibench.core import Problem\n", "\n", - "import pybullet as p\n" + "import pybullet as p" ] }, { @@ -188,7 +190,7 @@ " def reachable_workspace(design: np.ndarray, target_x: float, target_y: float, **_) -> None:\n", " l1, l2 = float(design[0]), float(design[1])\n", " r = float(np.sqrt(target_x**2 + target_y**2))\n", - " assert l1 + l2 >= r + 0.03, f\"target radius {r:.3f} exceeds reach {l1+l2:.3f}\"\n", + " assert l1 + l2 >= r + 0.03, f\"target radius {r:.3f} exceeds reach {l1 + l2:.3f}\"\n", "\n", " @constraint\n", " def gain_consistency(design: np.ndarray, **_) -> None:\n", @@ -357,29 +359,29 @@ " fig, axes = plt.subplots(1, 4, figsize=(17, 4.2))\n", "\n", " labels = [\"link1\", \"link2\", \"motor\", \"kp\", \"kd\", \"damping\"]\n", - " axes[0].bar(labels, x, color=['#4c78a8', '#4c78a8', '#f58518', '#54a24b', '#e45756', '#72b7b2'])\n", + " axes[0].bar(labels, x, color=[\"#4c78a8\", \"#4c78a8\", \"#f58518\", \"#54a24b\", \"#e45756\", \"#72b7b2\"])\n", " axes[0].set_title(\"Design variables\")\n", - " axes[0].tick_params(axis='x', rotation=35)\n", + " axes[0].tick_params(axis=\"x\", rotation=35)\n", "\n", " axes[1].plot(ee[:, 0], ee[:, 1], lw=2, label=\"end-effector path\")\n", - " axes[1].scatter([target[0]], [target[1]], c='red', marker='x', s=70, label='target')\n", + " axes[1].scatter([target[0]], [target[1]], c=\"red\", marker=\"x\", s=70, label=\"target\")\n", " r = x[0] + x[1]\n", - " circle = plt.Circle((0, 0), r, color='gray', fill=False, linestyle='--', alpha=0.5)\n", + " circle = plt.Circle((0, 0), r, color=\"gray\", fill=False, linestyle=\"--\", alpha=0.5)\n", " axes[1].add_patch(circle)\n", - " axes[1].set_aspect('equal', 'box')\n", + " axes[1].set_aspect(\"equal\", \"box\")\n", " axes[1].set_title(\"Task-space trajectory\")\n", - " axes[1].set_xlabel('x [m]')\n", - " axes[1].set_ylabel('y [m]')\n", + " axes[1].set_xlabel(\"x [m]\")\n", + " axes[1].set_ylabel(\"y [m]\")\n", " axes[1].legend(fontsize=8)\n", "\n", - " axes[2].plot(err, color='#e45756')\n", + " axes[2].plot(err, color=\"#e45756\")\n", " axes[2].set_title(\"Tracking error over time\")\n", " axes[2].set_xlabel(\"step\")\n", " axes[2].set_ylabel(\"error [m]\")\n", " axes[2].grid(alpha=0.3)\n", "\n", - " axes[3].plot(np.abs(tau[:, 0]), label='|tau1|')\n", - " axes[3].plot(np.abs(tau[:, 1]), label='|tau2|')\n", + " axes[3].plot(np.abs(tau[:, 0]), label=\"|tau1|\")\n", + " axes[3].plot(np.abs(tau[:, 1]), label=\"|tau2|\")\n", " axes[3].set_title(\"Actuation effort\")\n", " axes[3].set_xlabel(\"step\")\n", " axes[3].set_ylabel(\"torque [Nm]\")\n", @@ -398,7 +400,7 @@ "\n", " def random_design(self):\n", " d = self.np_random.uniform(self.design_space.low, self.design_space.high).astype(np.float32)\n", - " return d, -1\n" + " return d, -1" ] }, { @@ -436,33 +438,33 @@ "start, _ = problem.random_design()\n", "\n", "cfg = {\n", - " 'target_x': 0.9,\n", - " 'target_y': 0.45,\n", - " 'payload_kg': 0.8,\n", - " 'disturbance_scale': 0.04,\n", - " 'sim_steps': 220,\n", - " 'dt': 1.0 / 120.0,\n", - " 'torque_limit': 12.0,\n", - " 'max_iter': 40,\n", + " \"target_x\": 0.9,\n", + " \"target_y\": 0.45,\n", + " \"payload_kg\": 0.8,\n", + " \"disturbance_scale\": 0.04,\n", + " \"sim_steps\": 220,\n", + " \"dt\": 1.0 / 120.0,\n", + " \"torque_limit\": 12.0,\n", + " \"max_iter\": 40,\n", "}\n", "\n", - "print('design space:', problem.design_space)\n", - "print('objectives:', problem.objectives)\n", - "print('conditions:', problem.conditions)\n", + "print(\"design space:\", problem.design_space)\n", + "print(\"objectives:\", problem.objectives)\n", + "print(\"conditions:\", problem.conditions)\n", "\n", "viol = problem.check_constraints(start, config=cfg)\n", - "print('constraint violations:', len(viol))\n", + "print(\"constraint violations:\", len(viol))\n", "\n", "obj0 = problem.simulate(start, config=cfg)\n", "opt_design, history = problem.optimize(start, config=cfg)\n", "objf = problem.simulate(opt_design, config=cfg)\n", "\n", - "print('initial objectives [tracking_error_m, energy_J]:', obj0.tolist())\n", - "print('final objectives [tracking_error_m, energy_J]:', objf.tolist())\n", - "print('optimization steps:', len(history))\n", - "print('How to read plots: vars | task-space path | error timeline | torque timeline')\n", + "print(\"initial objectives [tracking_error_m, energy_J]:\", obj0.tolist())\n", + "print(\"final objectives [tracking_error_m, energy_J]:\", objf.tolist())\n", + "print(\"optimization steps:\", len(history))\n", + "print(\"How to read plots: vars | task-space path | error timeline | torque timeline\")\n", "\n", - "problem.render(opt_design)\n" + "problem.render(opt_design)" ] }, { @@ -550,20 +552,20 @@ "EPOCHS = 30\n", "BATCH_SIZE = 64\n", "LATENT_DIM = 8\n", - "FAST_SIM_CFG = {'sim_steps': 80, 'dt': 1.0 / 120.0}\n", + "FAST_SIM_CFG = {\"sim_steps\": 80, \"dt\": 1.0 / 120.0}\n", "EVAL_SAMPLES = 40\n", "\n", - "if 'problem' not in globals():\n", + "if \"problem\" not in globals():\n", " problem = PlanarManipulatorCoDesignProblem(seed=7)\n", "\n", - "if 'google.colab' in sys.modules:\n", - " OPTIONAL_ARTIFACT_DIR = Path('/content/dcc26_optional_artifacts')\n", + "if \"google.colab\" in sys.modules:\n", + " OPTIONAL_ARTIFACT_DIR = Path(\"/content/dcc26_optional_artifacts\")\n", "else:\n", - " OPTIONAL_ARTIFACT_DIR = Path('workshops/dcc26/optional_artifacts')\n", + " OPTIONAL_ARTIFACT_DIR = Path(\"workshops/dcc26/optional_artifacts\")\n", "OPTIONAL_ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n", "\n", - "print(f'Optional artifacts dir: {OPTIONAL_ARTIFACT_DIR.resolve()}')\n", - "print('Optional section enabled:' if RUN_OPTIONAL_SECTION else 'Optional section disabled:', RUN_OPTIONAL_SECTION)\n" + "print(f\"Optional artifacts dir: {OPTIONAL_ARTIFACT_DIR.resolve()}\")\n", + "print(\"Optional section enabled:\" if RUN_OPTIONAL_SECTION else \"Optional section disabled:\", RUN_OPTIONAL_SECTION)" ] }, { @@ -579,20 +581,23 @@ "\n", "def sample_condition_dict() -> dict:\n", " return {\n", - " 'target_x': float(rng.uniform(0.20, 1.35)),\n", - " 'target_y': float(rng.uniform(0.05, 1.20)),\n", - " 'payload_kg': float(rng.uniform(0.0, 2.0)),\n", - " 'disturbance_scale': float(rng.uniform(0.0, 0.30)),\n", + " \"target_x\": float(rng.uniform(0.20, 1.35)),\n", + " \"target_y\": float(rng.uniform(0.05, 1.20)),\n", + " \"payload_kg\": float(rng.uniform(0.0, 2.0)),\n", + " \"disturbance_scale\": float(rng.uniform(0.0, 0.30)),\n", " }\n", "\n", "\n", "def cond_to_vec(cfg: dict) -> np.ndarray:\n", - " return np.array([\n", - " cfg['target_x'],\n", - " cfg['target_y'],\n", - " cfg['payload_kg'],\n", - " cfg['disturbance_scale'],\n", - " ], dtype=np.float32)\n", + " return np.array(\n", + " [\n", + " cfg[\"target_x\"],\n", + " cfg[\"target_y\"],\n", + " cfg[\"payload_kg\"],\n", + " cfg[\"disturbance_scale\"],\n", + " ],\n", + " dtype=np.float32,\n", + " )\n", "\n", "\n", "def objective_score(obj: np.ndarray) -> float:\n", @@ -618,10 +623,10 @@ " objs.append(obj.astype(np.float32))\n", "\n", " if len(designs) % 40 == 0:\n", - " print(f'Collected feasible samples: {len(designs)}/{n_feasible}')\n", + " print(f\"Collected feasible samples: {len(designs)}/{n_feasible}\")\n", "\n", " if len(designs) < max(48, n_feasible // 3):\n", - " raise RuntimeError(f'Not enough feasible samples ({len(designs)}).')\n", + " raise RuntimeError(f\"Not enough feasible samples ({len(designs)}).\")\n", "\n", " designs = np.stack(designs)\n", " conds = np.stack(conds)\n", @@ -632,26 +637,26 @@ " top_idx = np.argsort(scores)[:keep_n]\n", "\n", " data = {\n", - " 'designs_all': designs,\n", - " 'conditions_all': conds,\n", - " 'objectives_all': objs,\n", - " 'scores_all': scores,\n", - " 'designs_top': designs[top_idx],\n", - " 'conditions_top': conds[top_idx],\n", - " 'objectives_top': objs[top_idx],\n", - " 'scores_top': scores[top_idx],\n", + " \"designs_all\": designs,\n", + " \"conditions_all\": conds,\n", + " \"objectives_all\": objs,\n", + " \"scores_all\": scores,\n", + " \"designs_top\": designs[top_idx],\n", + " \"conditions_top\": conds[top_idx],\n", + " \"objectives_top\": objs[top_idx],\n", + " \"scores_top\": scores[top_idx],\n", " }\n", " return data\n", "\n", "\n", "if RUN_OPTIONAL_SECTION:\n", " dataset = make_dataset(problem, N_FEASIBLE_SAMPLES)\n", - " np.savez(OPTIONAL_ARTIFACT_DIR / 'manipulator_dataset.npz', **dataset)\n", - " print('Saved dataset:', OPTIONAL_ARTIFACT_DIR / 'manipulator_dataset.npz')\n", - " print('All samples:', dataset['designs_all'].shape[0], '| Top samples:', dataset['designs_top'].shape[0])\n", + " np.savez(OPTIONAL_ARTIFACT_DIR / \"manipulator_dataset.npz\", **dataset)\n", + " print(\"Saved dataset:\", OPTIONAL_ARTIFACT_DIR / \"manipulator_dataset.npz\")\n", + " print(\"All samples:\", dataset[\"designs_all\"].shape[0], \"| Top samples:\", dataset[\"designs_top\"].shape[0])\n", "else:\n", " dataset = None\n", - " print('Skipped dataset creation. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + " print(\"Skipped dataset creation. Set RUN_OPTIONAL_SECTION=True to run this block.\")" ] }, { @@ -682,10 +687,10 @@ "if RUN_OPTIONAL_SECTION:\n", " import engiopt.cgan_1d.cgan_1d as cgan1d\n", "\n", - " device = th.device('cuda' if th.cuda.is_available() else 'cpu')\n", + " device = th.device(\"cuda\" if th.cuda.is_available() else \"cpu\")\n", "\n", - " x_cond = dataset['conditions_top'].astype(np.float32)\n", - " y_design = dataset['designs_top'].astype(np.float32)\n", + " x_cond = dataset[\"conditions_top\"].astype(np.float32)\n", + " y_design = dataset[\"designs_top\"].astype(np.float32)\n", "\n", " cond_t = th.tensor(x_cond, dtype=th.float32, device=device)\n", " design_t = th.tensor(y_design, dtype=th.float32, device=device)\n", @@ -756,25 +761,25 @@ " g_hist.append(g_epoch / max(1, n_steps))\n", " d_hist.append(d_epoch / max(1, n_steps))\n", " if epoch == 1 or epoch % 5 == 0 or epoch == EPOCHS:\n", - " print(f'Epoch {epoch:02d}/{EPOCHS} | g_loss={g_hist[-1]:.6f} | d_loss={d_hist[-1]:.6f}')\n", + " print(f\"Epoch {epoch:02d}/{EPOCHS} | g_loss={g_hist[-1]:.6f} | d_loss={d_hist[-1]:.6f}\")\n", "\n", " th.save(\n", " {\n", - " 'generator': generator.state_dict(),\n", - " 'discriminator': discriminator.state_dict(),\n", - " 'cond_min': cond_min.cpu(),\n", - " 'cond_max': cond_max.cpu(),\n", - " 'design_min': design_min.cpu(),\n", - " 'design_max': design_max.cpu(),\n", + " \"generator\": generator.state_dict(),\n", + " \"discriminator\": discriminator.state_dict(),\n", + " \"cond_min\": cond_min.cpu(),\n", + " \"cond_max\": cond_max.cpu(),\n", + " \"design_min\": design_min.cpu(),\n", + " \"design_max\": design_max.cpu(),\n", " },\n", - " OPTIONAL_ARTIFACT_DIR / 'engiopt_cgan1d_weights.pt',\n", + " OPTIONAL_ARTIFACT_DIR / \"engiopt_cgan1d_weights.pt\",\n", " )\n", - " print('Saved model:', OPTIONAL_ARTIFACT_DIR / 'engiopt_cgan1d_weights.pt')\n", + " print(\"Saved model:\", OPTIONAL_ARTIFACT_DIR / \"engiopt_cgan1d_weights.pt\")\n", "else:\n", " generator, discriminator = None, None\n", " g_hist, d_hist = [], []\n", - " device = th.device('cpu')\n", - " print('Skipped model training. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + " device = th.device(\"cpu\")\n", + " print(\"Skipped model training. Set RUN_OPTIONAL_SECTION=True to run this block.\")" ] }, { @@ -849,19 +854,19 @@ " base_objs = np.stack(base_objs)\n", "\n", " summary = {\n", - " 'generated_error_mean': float(np.mean(gen_objs[:, 0])),\n", - " 'generated_energy_mean': float(np.mean(gen_objs[:, 1])),\n", - " 'baseline_error_mean': float(np.mean(base_objs[:, 0])),\n", - " 'baseline_energy_mean': float(np.mean(base_objs[:, 1])),\n", - " 'generated_feasible_rate': float(feasible_count / EVAL_SAMPLES),\n", + " \"generated_error_mean\": float(np.mean(gen_objs[:, 0])),\n", + " \"generated_energy_mean\": float(np.mean(gen_objs[:, 1])),\n", + " \"baseline_error_mean\": float(np.mean(base_objs[:, 0])),\n", + " \"baseline_energy_mean\": float(np.mean(base_objs[:, 1])),\n", + " \"generated_feasible_rate\": float(feasible_count / EVAL_SAMPLES),\n", " }\n", "\n", - " print('Optional extension summary (EngiOpt cgan_1d):')\n", + " print(\"Optional extension summary (EngiOpt cgan_1d):\")\n", " for k, v in summary.items():\n", - " print(f' {k}: {v:.6f}')\n", + " print(f\" {k}: {v:.6f}\")\n", "\n", " np.savez(\n", - " OPTIONAL_ARTIFACT_DIR / 'optional_eval_summary_engiopt_cgan1d.npz',\n", + " OPTIONAL_ARTIFACT_DIR / \"optional_eval_summary_engiopt_cgan1d.npz\",\n", " gen_objs=gen_objs,\n", " base_objs=base_objs,\n", " g_hist=np.array(g_hist, dtype=np.float32),\n", @@ -871,29 +876,29 @@ "\n", " fig, axes = plt.subplots(1, 3, figsize=(14, 4))\n", "\n", - " axes[0].plot(g_hist, label='g_loss')\n", - " axes[0].plot(d_hist, label='d_loss')\n", - " axes[0].set_title('EngiOpt cgan_1d training losses')\n", - " axes[0].set_xlabel('epoch')\n", + " axes[0].plot(g_hist, label=\"g_loss\")\n", + " axes[0].plot(d_hist, label=\"d_loss\")\n", + " axes[0].set_title(\"EngiOpt cgan_1d training losses\")\n", + " axes[0].set_xlabel(\"epoch\")\n", " axes[0].legend()\n", " axes[0].grid(alpha=0.3)\n", "\n", - " axes[1].hist(base_objs[:, 0], bins=12, alpha=0.6, label='baseline')\n", - " axes[1].hist(gen_objs[:, 0], bins=12, alpha=0.6, label='generated')\n", - " axes[1].set_title('Final tracking error')\n", - " axes[1].set_xlabel('error [m]')\n", + " axes[1].hist(base_objs[:, 0], bins=12, alpha=0.6, label=\"baseline\")\n", + " axes[1].hist(gen_objs[:, 0], bins=12, alpha=0.6, label=\"generated\")\n", + " axes[1].set_title(\"Final tracking error\")\n", + " axes[1].set_xlabel(\"error [m]\")\n", " axes[1].legend()\n", "\n", - " axes[2].hist(base_objs[:, 1], bins=12, alpha=0.6, label='baseline')\n", - " axes[2].hist(gen_objs[:, 1], bins=12, alpha=0.6, label='generated')\n", - " axes[2].set_title('Actuation energy')\n", - " axes[2].set_xlabel('energy [J]')\n", + " axes[2].hist(base_objs[:, 1], bins=12, alpha=0.6, label=\"baseline\")\n", + " axes[2].hist(gen_objs[:, 1], bins=12, alpha=0.6, label=\"generated\")\n", + " axes[2].set_title(\"Actuation energy\")\n", + " axes[2].set_xlabel(\"energy [J]\")\n", " axes[2].legend()\n", "\n", " fig.tight_layout()\n", " plt.show()\n", "else:\n", - " print('Skipped evaluation. Set RUN_OPTIONAL_SECTION=True to run this block.')\n" + " print(\"Skipped evaluation. Set RUN_OPTIONAL_SECTION=True to run this block.\")" ] }, { From 3895c93b83e820fb65b172f007cc0edf4b9f2cbd Mon Sep 17 00:00:00 2001 From: Soheyl Date: Mon, 16 Mar 2026 14:58:20 +0100 Subject: [PATCH 37/37] Exclude DCC26 workshop assets from ruff lint checks --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 36881c4..f267845 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,6 +113,10 @@ target-version = "py39" ######################################## LINTING ######################################## [tool.ruff.lint] select = ["ALL"] +exclude = [ + "workshops/dcc26/**/*.ipynb", + "workshops/dcc26/utils/**/*.py", +] ignore = [ "ANN", # flake8-annotations (mypy's job) "COM812", # missing-trailing-comma (conflicts with formatter)