From e9e163f5cb608732c655b585e774525326751c13 Mon Sep 17 00:00:00 2001 From: Soheyl Date: Wed, 4 Mar 2026 08:33:03 +0100 Subject: [PATCH 01/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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/44] 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) From 65c0ac287e1bba177c97de5e6c0d63b580f0f473 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Tue, 7 Apr 2026 17:19:43 +0200 Subject: [PATCH 38/44] Add workshop notebooks, utils, and assets for DCC26 Includes participant and solution notebooks (00-03), expanded notebook_helpers with training/evaluation/visualization utilities, and asset images for notebook display. Notebooks have Colab bootstrap cells that install dependencies from this branch. Co-Authored-By: Claude Opus 4.6 (1M context) --- workshops/dcc26/assets/engibench_logo.png | Bin 0 -> 606361 bytes workshops/dcc26/assets/engibench_problems.png | Bin 0 -> 1001496 bytes .../participant/00_setup_api_warmup.ipynb | 466 +++--- .../dcc26/participant/01_train_generate.ipynb | 483 ++---- .../participant/02_evaluate_metrics.ipynb | 1451 ++++++++++++----- .../03_add_new_problem_scaffold.ipynb | 996 ++++++----- .../dcc26/solutions/00_setup_api_warmup.ipynb | 539 ++++-- .../dcc26/solutions/01_train_generate.ipynb | 551 ++----- .../dcc26/solutions/02_evaluate_metrics.ipynb | 1433 +++++++++++----- .../03_add_new_problem_scaffold.ipynb | 779 ++++----- workshops/dcc26/utils/__init__.py | 2 + workshops/dcc26/utils/notebook_helpers.py | 1015 +++++++++++- 12 files changed, 4819 insertions(+), 2896 deletions(-) create mode 100644 workshops/dcc26/assets/engibench_logo.png create mode 100644 workshops/dcc26/assets/engibench_problems.png diff --git a/workshops/dcc26/assets/engibench_logo.png b/workshops/dcc26/assets/engibench_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6c1f17c3a3957ecf638e04a259099b477c6cca33 GIT binary patch literal 606361 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

`irP`V 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

6-HcSqbvj&K2yd zpz030S;eGWBm_o|bSAgWq6*IY>z%>+Rp(h9;6VO{WLT`x7Yl%6!d)%>9R!h>N1GCG ziwA5~xjG4sd|jShGGiNBK1>xZUA7ie!iHaO3heS;hruA-uKk2h?24G{0jpzOEd4+t ztaNSbLrh4DnGprmlx8JFb2-$;s=d|Ncl?6pql@O$EJ;ZgM9XBDd+CNQ;L-!Srx~M9 za%m|lfTTbgFZECzV+I8{Wug^H<9bEt51R+>nxMqd$G=VGImxZBLoYd~Svctp*o5~A ztaiv8YI6tgCAi{A>RFZ0mUO{*I+Wh0URwj6q4T-7nQ?r9{sBM8)*Q=V!@q$%$e6OX zd!HI7ayQj@VKigLpkNdgLE?Ss+V}*FoYh+EGs2>D74+L$u58%g!lTc~Ve0bi%`xjgnP*760^9pkH%JQK;pH_Is|AJFi0mR|`ytUXJFs-5M*ZJ!wU9uUHPt1wVJRrsmx% zb0RONw{(uCNz7hzN2R3*ib1?Tur#EV?u^dq#VSm{9d{goI8Bre!$jxdd{sO#%d3b-0#f(Q}JDkG@^%bb#eQ9uWmi-NKk{< z7bMu{I|!V#GLAqN4K}&=Cio9?yld6pg`_Wq^&hXVq7}~(nfXs*gh?840H1GdrkHIgSi~2F{CfjM`gL=!DiY1z~&8sCK&;EP@MK!IcV$A z_k_3kO|k|1YGa3CuNpIl9azOKn1HZ_`VcxKWbxPYft-!QtF49TpQ%ZN&CWQi#O?cT zVEGX=3$C(Npqeu)I5&O_o9UriSjL_#jVVdLwO>bVoSkvDO~hHAEss{ouXw^gm3$tL zLXJ4`v<1{C-e)Az^9T-NscJ?Ts?9GnJe$!3oB59oKY6tLbjGu2wtOnH?Sl9NOz!KC zC1qPL9yaqs@o4uC&(`U@5&l+W-rx~Qveo_cHCL(L(Nf8{%4&a~YtL5NS-PpHp!4Kx znrFvEVJ+-beGI*SQW;kF3S7?2OPl$nxg zi^>h`aLna|7V3Kf1cy1ugRq2u_Q61N$Q>yVnD#AV#tuLa_35&9Q61x`g+ zM*f(tYMKS_p<-xZ7a8Wf~v&QyAf9=2rSwo$^1>@CP z)HM1*A?jl_Ft}&%SFOlPSDn>;GEZ=N&G)837Fmo>81H+0cyGi9;#02j)caY_gxlnF z85PA+!#lBUOyV%|*&4Fn`d>!V%IIcU+(L-L?kwIRpRLI}YhK+0PE!w(NcR}=+1;rF z?KW@fX#_iCpQvosok0W$&8I!v`OprH7OLTmp!Tz@qMBVkXK+$qM%;uSoVz;3gh{jvDj)TxZ3!oKkXYO#G zB!nBOlSK_1-}VId`Tttwc$(?@S)s?&@Ias2+|Lk07{&3ALcMwUXR-oWm;gnLKtb}2e&loV)Xd%}XqK{Z_QH(ZodR29y7VFp53%l}#Y;A+A~W5|d}Uuf z%`x4H{ujJfu2cgxf^#mo+Jaz{NQol(KGksy~w zk8yo@2pLi0Y8~rYOpfrLoD*m)7-1}27tl4tt&Hh>m>ZmCQ7NLgB+ny$xL_CJh_EQ} zUa)ss*tz|sM6ZhZ-4|tunXhhFN`Qr>|FqcLG=5+{89 z+ncH5KWMmXGo3Kwy#}>~am!zAbsk89-x;Esq{h}R!#}{`n&xV4m5%@%UtSo*>IfVFF(L9LO?l!LHEyJBX}PAid0b8YcEh|T zDyXKE#sE0chv}a7wIPym{@XmTR`{Y&tkW9Q@{tig{i6Tco`?TFkc;@zcG=fcQ=GA@ zt15RXPNK;MM-m0y83}XMBVsYp^Y!}?I03yGK+Uf6x6z6{=}Ro;@F#Oo^2%ys zb%gPK*4Mz_NyL?~+#uPhg~ zU=0U9fwLrwmg-G>7=hc?ltDpt#sIw`Qsb6KOK_MJsy1`FcL9s8PyqudUB=c@g~a=` zFZ?yeO|_?t)C%bR1rEn7lrM+lMBs(`JGEFJ7A+VmQ}XF=JI)%A>O%tKrYHk*tzNt! zLMHrs)2xLTJE0OaE5MJp_}D}~*D!^0AQ5V|IrV12FVl3_r>bTZpm{7)@Qh_igrK-ysjK5)Ku!=0e9p8EdCm-Xc= z=O)unbYdYvz*SXg(p-;R6?j2KWajj(js}QZeko9YK)Hf;WB#r={(B4n)3Z8RDUqtK zmtAC(Lw)Ymr^gb4kwTL4Jp~ZgCpp8q-=7FzlEWW3Q5j;Oog;CxD>EdWFUxxTEWp^~*U&bJP%$WrC#&|#LfyOo+OP0PX z|FSKpJ(EqK{$VqwC9D#Qa%`z$;PSg? zbKX$~RC3W-=I{(~iezhvDptjd&?d$B-p?**5j3bM4cCA9c6LD?Fu4jbK{4N$yTTXBeG(m3g(3HVg8Za z`7wv@E^hV=1CV4*K)!4{_i5heRKhwzTR-+8rjMEyDRIfyH$U0})T-@Pl%`O>ORg+c z*DXK$EsM;_>-#A=g7fln_UW#$MZi9{dj_H4rjU2|mV8c>3b#(=eS<0iZsY>AC?2YApMXm6m>EN+ zqXwg*Pj|)+&zHgFWqz=hF>vT!TIU7=*PbYk9!Lw&OI$~!+EkX!XMh0{cdNbJJgJn+ zuRby{FuckQfQXR@kVDb8@xm3khm#v zkTbGaAxs2@LE{CpbHXH8QNHS+{Y$g`L{K%P?*|Yh#>|@%zAFo3k#i(5w{CGqG5CrK zMD&*<_lKK1aYC_jwq6ibqh8CsHORbL*TGMvq&8^{;UiiSd2NME^H{X_&tBpL5s*I> z_H7>0A6sz~Adt7%{|~H0H#9qDsj^lgp`1pXbe*#9_hivrGdp3ft(DM5FIW|v)#l=K zVMXB6>(VqsF|m&&Zw>AIdMoiIQ0vMGw5_(t=@Ftv(G5t(Qvag<=6SL#gEn6B@zUF6pCo}Z`C?8cpW zy3INbFNKV#w9L*^8xqBIhfs%fhw;Vu3bNn(5wMmYon3>j;~cY% zUj*aqZ#Yp*bfCw+Tut)6tp*TFbMuWAPnc!J*2{~0>ESIw&Xk$TjPhG7%0_bpc>AWn^k{{$8rke?9T+p!vhmI}> zQm1;R$o%MTum5zF&rY#~D8pskU;DM>?s!R8u3uk8hBV(*Kq z@96svT>dXs_vcyhC^^r{*P4py70LpH#)PTqAsT zAIjRIUDV7<3CH`AD}I_q9EQ4)GvVhJmPk zx7hKisg}9UUUn%|GI<8z);FhR$vn9;OO0|g?tDC~;8QX6kmZ!p>|&xGM~kPmL%v~1dqLfH#^ zcLlmw*v~5?ZL{%dhsVhiCKh+YjykdoL= zQAZC)rSbVthRW&v-8bb4b|-opToItgsa3}!Tk(Ssyr>T82mXk`n<9% ziqspmHHu%1fvP*Q^fB*2yYF5DUU}!Id=Z3oSpFU3;}@ylxfSUF3W!;vPba{%s%byF zz68$t>#nHi8JSIbzHOmi3q}$+A8l+FQFKL2etI*u!p_>Zh}j)U0%Ut|?8k@=Ub!%fXmEg!kK;`G3_ZL9)x%;IGCP!$u*=GL$Mi5^cK8(lJSox=-a=J>5KwgJ5 zMdpKkA#;8vO0Tl%)`is!8TZBvgZwM85Al&8X+Zq#ikmSTwltP47=Ng>+<8cIetff3 z6{E65KDX>Q%$YwXU&)NyBA*K&^OAu*!wAkT-&@|$^)6olAv*iZy=A zJ|U~Boblgz;W&K$^}tIDOd9$h16BT;hF3r_cJ1=tHXA;&S>?Gbg_CTC7G;38CAH-Uy1BREvw83|@?WAO1&Vl%Z)q+w52f4l&u>$7 z<>+Xjefw_cd9oPng{dqyNoW| zvh=Ov02bpug?x#F!(F7?ikuqSdJ7J+pF_5C#a_& z@-6W5Pa=f=N5+pr3G2#Z2&oaz!{}Qm< z3dtR&wi&tZ-1g}nxM&6HmIi=7jEcyMlv!-8?|qBOIa04%L>-Kvs72}UtYsEu^;^xR zUdsYxdyYjA84(|}55)BMOAAcZEyvA=3zpY~jN!F;LJQ53}eU_!xF6leRpOpozHUX$3Rk=)Oxu^@Mh^j&#jQfm-pU`iAE~Y znyv-g=p?DgoXlG$;sD3vK7Y05SX4Hm3pK6}EoMs1eEr7{W&b}|*DGYzZH-~Kmc{OW zw0}#5uOp54Z)FnZ7CL-x_n21&e{gMfgG?1?4jbya=5=S6`p{}sQENKWi7euI}td(76{@{*|fUD(hdxbO}S^)n2Jh9npg3ildgASS-FE6GiQA zh+^LDr2DI8Ka~4&)6jtaM-+P-Ds(hAr;Eg*B;4>Pg~^{b~TZZOYu{M)!d`%(JVvC{h=Uj&8riXDZ#)IMic z1S3_Q$6`otfMGpa2s7TdoQf8dNR2#`*ZClN!6kZ8rwu#f#{;dZz1Fh@Rlr+*8_l8X z1a7VZ?i>YJxdrjkFM(od(#8kq_w%omyPSrqW%X=Nn@&*>a*=j*g(}@ZSl`;j0EpkqO zF={Rl*(f_>KIr?PPp+tDK&=`VVv>j;)ru+{`eFT6iiGp%JLGdgApsovksBxappgX- z#_clR+4I!Tcnv`}k%UoW06m#ZcNkx!Pp10wo`Ug7DipUJ@1Y1G0u2*i-)?YIr zY^Qg1m-UzL_NnAtcAo3mVci(^Kn$$2T6QmD5weqHKUdWR8phew^4jbERnh(f;EaTU z9~O0+tDO4ycRL*UN`Y9waam`i2)qZD;uk;lkFWeq&O)y+y)+MBxFSnkV-XmNSs3q+ z7#Q!C;SMy!U<%urmHfsvj0;Nf2Fokoc2K8~ImLdA@&s4BX~mT2bNpzIDeAqmkfA_Y z@J%q8Ct%xK>)x@|KY$^j*mW%Up_EK)vZ*t7fcg2vA2Xe8VG|5E zC&cW-Vt9^aH+|jjYe6N4>wL1;ISz8EtWUca?xZ#zHyr;>r2fs$b1VS~ah}`8|Bq68 zL_vrCP|*KhZxTd){}Sq*UT8}sIM8G95Jxh^_e2N$G&A%yOMFPbpOy|qr@YFD0TQT$<~wQ*|xydobwSXr+~X6Yu-voMR1Q>}Ua z9K2kRFx;xEeOgKWfiY1qFkQK-);uu7do8^vRVF;psP`5tqBj|_pHb(3pdddK%a)laam&`{8>y&Z1hVBF!V)=?P6;YrHK&e%ChWF7 zC~~jSzd&QV@ghz7uv%2^H_F0qzBJfsUktHo<+h#4bUb;+Js~c>TEA_}jwKbb{LmPfeWt|Or^-Z>kLdzO zx0qEUpX;qEHTKcrfrioD)l=fi4=-|BbVZNxdJX+FXuQYh+L%p~9 z`z5<`R13Zkyvx_WiiU)LZsT}QWV$gLfa^ZJ@4<0g*sC&3ye(0^Ks9{qy>9uyua1pR z4m(Ndx>P(tZcrj^x+>9Ao5}J?_aO9u5VDobRuh0)NJkZCn#e@D3L^|74bBa@8i0q4 z2S)_cB_+{G`E$RlMOf|6yfzK@TAa=wVLbBPANJqg?TbAHjgfy59o`Y9Vg+_DI4F4r zvC7I<&^qL9fkvH#DoZg_g?vs8iyc1oI~)mS@Jp>BWC ze-=xapDCb+I+_~SziDqp-Tv_juXE$;yRDni=m*We)xBsJ==;khjC$PyuCm(iZ+jhp z{M9G7?!+Kk9`}Rxmef|Vm3t2|O@BV$rJ_X)JRg)1hEUJBr*C$I443fj3{A6Jv-#vWNb(H@=7%vY|2&_L3-5>;uD+um z)HR=XE0I{0Sch!ys!unJM5K5-SXct{%@RgG8#CumG|qocm0zE2od6Hj}Dwgm;;z-%HGw#SIwKfriSZPMt<>xVQ8EYd8$B0R|%ebb&1sU(UB zh(?#R>117IS2qyw>%Vr**Ix5?HPic4IchwoPybX`@UNIC(C)9lR6s$SRgR^-*faWbtS}#kRcg9cx`wxpL!|pPk z2wPG-z^~w=?0R(k?mst6y*+A8zuDzrKQRs2GQ6Y%=2KmSRcsum&YcP*bspxSCC9t_ zk_q!~mz~!3b#JhLoA$Oud1qyyne=l6zuowy``yl2lU*KB^-7$K=(wt{h~O0;U%Ukw zqu|=)7ebOQFt?Y~rrbod*C@+xGNT?oeV51OkKJ2h_h0#zxS=Puw?=0A{Yg7N!_S0T zQ^b@tSNovT@R3~q?_Bd=&tk6tEu*L9MyyBwz17f>LPw(Q2|f`oetvZ-lPa1OtIKE?3$#%1{r&&gd(U{d zqi$_j4Ix1al8BP3lY|g`LPAKC=v|uLjS{1dkd8!6^xhd!W7H54b@b7RHp6H!dVTgx z&NrBYJpG{YPJ9Qq8{q z2QnoCQdjV85;$1u7vLFRVyKf4?H_!yvVA-k(JOo5_P8JO^jh_-rn?z8=omeB??udz zuE2tBbgC%(dh`bSl+R8S>RgCl{mFU;-1=^Q8q7?>q&2E6H_ul6C>1g?tWIH$_r8lT zM&__fZ=-cK`4=?TV@H3b!=BHaKj866$P@&C3H*E`_D8MBj<#$|`BKi(DF4Y<#vT#8 z{q%SJzgJH9g==~oR$S?%4|sW&>~_#w*Z~RHew~~1ZYX_Nd@f_b$IIEyY>ctPuS7K2 zv=+4?Zg07{76xm+O3^uGr4%U_rqDaBo7)Ylx=whhitzUDJt<-|+uevHfz2d=N@7;l zWMyEWCfcR5B>pMQhRhSkb^0E@^zTK_SHF{N<vmt!whj`3Q%30tQXHkuVT!*B1Q z7bR{cg{tCB-nO+-N$d|8l(V4Y>;+((Skv{^R&GS^>dF8wY$j85y+xk%SCS1C&+g!~B7p3`sb3 z?{>r2ON+g?0nISFy(iPjUX7P%5^3OpsoZ`Vm~E@&n88QiM3eqdEFvZnUzX$)@PYRT zt+;sse+7GEW@f^OKZ<>a3w^Rmd9H-EPbB*;H>G8vNcY|n{FC8rk3EvD@4jmnS^{_H z7lT%BHN+IzIj9sz{?N+J%ImBrn|nTdJy(2tVeJ)zh|hQG)|@bTL@dipqV>e!{O)bn zom&y~gQG<>^N-ibj?oW_|4c6%ZL7mR+n+msZ`~Ch;PrTA`n&~`gmS@H@yFxBY(b|z zKd?XJG0OBv=C5zD%&$sAzNLAZxri>F76A=ZUGr}WnOB$VMOSF!_wDIwzl+}oZnz}I zz#7we($)9sK{P%HlClKb&-j&aov36Ad_W;z!~3NO>RK~Sc(3Am;@txkFy;t6f#2OW z=4D>{tFsh)lfC;%^mbkf?8fdo6SH}}B0{UZu}xO@4U#36%QRM)QdYTr*`5ldZa)}N z;GUZUtpZEhpWP8;&Mc+2*%E^|?&onlQli|3WY|zDThM-a%hC|dldW_?gw(kh%y=HidL5+-{G(beKy|###6|wF zDcZpKHj6tI9SlvTPUQPKydPW+dW&rqvU-K?v>(~Vz_u&xXJ!M}H7yP5^)=6yGVbVp zLu(g^4bPdd`pHr^-2g{Ty*&pb`WKSE4!=Sw-`RM`{#%8Pfq=lJEnli;G5DM@|Ixj> z2wKh_d6zO<s$yW=zO)6TcA%8RULpDX{}5M+kkxzR?f%5zyD`1z>gzD zAvA-~=GCw8VeLS_lmDKKq9f9=n=ZL^v}{`fx%Jy)YiwX$I7g-$M{(K=N>rUKk;P@I#4kQMDE?n%n7C)2^k_ zU*aeyz8gK|Kz?Hp9jWiZi4X*AOk~&3$5tIR+Bh$!WMT+*!8u>ni zQZe&dJb7zlN6U7e8B)Xc7d{7Jd_HB#U1WA z@Bf$46XF67AugnLq#=kq4GI9{#25dcZG9a1zF^Cl9n^wS757o)KC0})&+ogvLZ2Q| z(V&#d7VZr4L%7wI8ZDJ%+OQ7ZXa@^yPEY6mYMp3Qj}4sY@kv&q#T<>zz^)a`S=R40E5S88-(XRI_z;m8QmLN1a`8m&S{wIQmYmt_%F z$};Y?L+AW!u0LBB#qubwciMej#&UZsCdmz`_7Bs~vhCBcvhCNivaC(@dOd$Ey;ki@ zFGd-UvdW=f<_>24YPI$*ja#qdOEZEb9JN!`h}m7W+>Q1^DPtx5IU>c}M>TIF{EXgT zNV&e$#(RVtTILj~SbX}v;y3Q~fGvG=>w1aH%nmJVJ^&3!(#X~6viSIPMVoR6*#*1i z^5Qp`2C|!xX@8+_qH5G|1AOM)MD=Nb2*HOq9sJpEU;2aA!R0wXPg)!kwTSBRuU~SI zkoKSozGn>uJY=I4t1Pg^x>D!W3AdtTToamSs`5dyRYvab2%hj+#A;E=_4YLf-kw(~ z;MTM!X-e&_PaE$GTmnvD{JgNQv39tu4cWdCNhRIw5d%mJ$=1V|XznF@W~2NXcbkQ5 z-9@%0UNG_@%}J9tDA|cozZo4~Z@BtZyE1&H6;qv)E)tJUeTSOuDfF4)ifWV&*7F^G z({QZq*TGvp1|fL+@86Y(D@rHKFX29W4@o@E3ucm#VW~==C?8Ba-13Gqyjr$%*rA$! z&3SbxaDD&dNcr#dflTS%{2}dJb?>p$c>UyS>Stdz(a_$ZO4TjWYEhv?L^Hs6yyl(A zu&3l52+%0=*aa;KaO#2Ih(u~G+gOEiCHapzx225A_TUS2 zG9R~Y=dla--m8P%7btCN4-W%}dLaXT*s9|R3beB&tL^O78VD{|34=}#`|roDFMp17i%@b?|EXz9b-dvC z>NvzZCYzlKwpDwja;+1$^@Ve9dV=l4DzlpJsQaNDJ}HIt6~KZk68H3f+=xK}H$C~9 zXG)V#Bh(Uio@{h$BYq|}xC%jUSCO5&74%jaQ49@O2wdR$`XxpGtqat@huOq} z%Oa;@l#9V(81$;~`BAZLo5Tw~6!;?Fib^}iO52$QxAo7)_)~%$?Y8)(OjBmg*M%lN zVNwc@bMrS#6*tR;kx;XN1nT0^B??Pv&{(=o z)wiCmbHUfn5wCevO1>viTT5%kkw(@7!4TyOIH=E8`z8ZByIq!D*qeZUpE$MQy~FB-id`xRu*@GZOXauvJA6 zPBwHNKYN_W)qIlkRb8E~d8$EN;}oC@t70bPvky*`$GXTa&`k}KL}>^t&2I>&6J` zy4I8U)d|ce5if_u?N`P9`*Y1H^R_5%W4rLSTr!wZHeWM-qsM)@rBD+eK71bdhyykPmEUl`hhoxlw44_i2#LZkPHv>?@x)agx6O z%YimJQY)M%vAK49rgv=(p0H!b=h5c7Q`9J(yZ`u!r_aDh`In;Y1zw8Y4gns!_4qxj z+jyts$XC;TM_RAYRrnRsa_CZ59!>FB)YLK6FV%V0RB;-obX~Jalk1)NWLDhlLrtkL zk!2y+?@XOla?qS*Z@0H|Tf9b`maAqljq?2Ir~`o^2^kb1!}xc>fkZ8f0PP7|A;-_O z5^HRq2t~%7mvf{C#~CIq4lZzrl;zz3WZ-6cO?k3UYAM1~!QN|we|V%UaYR0^6;B>u zlac#N>iDatjhIk@lmm8bWLGs=QNnmf?IWVDuL=mP_@nHG@kn|lmM z(;`ltr8q#MGkPAyvNsS9~o~2GC*_NeM ztRPfWW{m_ks|y{>wTSz%zvnveNv|7>NRs|>`c^3y>{_8f(P)q*f=if-rk+7~Xh2=; zeC8-`k=QNHaBH5@6v;IgfU0K#nWYgr@jHG3v-cHdo7(72I4tfPThEZ4;X7EZd}}NL zui!l(AGSx-vj>E>znwpgTw(xP@QO}4A9UuQ1Rj3MBB`QIgyO5yu*n-bpNSs`EEsl% zosbYM(ir~u)^^kQ11dMoU5$~4k~VrfgKs?x$79-{kTJ6#1X_P7SOc49^5$#i>)vc1-KKWksv|kO17X!}>T!lkY9 zCB&0UeHBj|&Ad9)B{;j8Zrn|uyI&lBD_B*7bAFstgk5Tv4L$vv1LvK>lXw}8 zOpDvQ@GnM?FAc(HS<}5;wXHYZryP~DoXW(}g{so&%mS4e2&0|UaB&}1IrB|c=sEe1 z(*Fp;YxYqiu{Dhk(VP1|Ixg!ilNXO(hJWyPI6^@wRj=%NnF%9cc?*8!iY}n*)G^%7#on4CD z4-_AlpDt=FM1+T8+!T$?bUWcBNL*soH*b-ZA0^6ekYE*(;Ry3Y(4~bT^U&qAN7PFs z&@1(>qn+wh*5zKcpQ0Idc@`uTs&-q+T^dlreVDp^Rq{r=3{4X+z)$@Pcg8#^lO#vzvjDAl}6iyT% zNMNU!gENW5hvroXN#zaEAlZNA1c_b{-g|<&hf{Svw~#}7oMO+u=Mv20H%@V{*QYt(P=d6CB=h_S0D#tM@aY3hX!#uo`@z$C5Zn`?+hc>;hDwr@$-t`MQg z0LT&%xq`_D5=Ficp`#QuwQQSgMjg?aR?$(ocx}l9(GhROX7N!c(v?Z9n%C`8GQsp3 zq=$j1J#A968$C`vRt-UW3{$!)*HG_Ja;5Jb=-%UF-kS@EK(RRwy%ZX9CaL_*+Wrn& z1X8GsGZ0nt8_S+Jj(C&3F|Lw{eEz=YBt9B92fIh@d3@P(gtCGYFq?I5~RrPKB6It%LPOPV^J#=V$5Y= z1X18&CI$XKx{dxNn!>q``YSbS7=)#LHWx{3$xc&)hKESkGgMD0_d3I>idi@UeA zAV%#Zzz|38fFZ{u(sFFm7>Sg%GxirQ%!Ip;q7=~c)xS!WDO;%6#Wa1rLYp?37VLQ2 zuH<)nB~G}Th{xLREzsZjGA~)*Hqon4wZ9>&@3Rym%9t=Zxg+vL&WCf2<_L|EApY5} zd)YqMedFj82jA9ls9}*69?Oy@M+JYy&7a`nEvB5kzpmKtKd^IMX;Pkh&Q{}wgnn{G zt8g79%B|BlR9gSrF|ib5vQ0ZoG9KX~a&Ynjj)fivTo&{pQN-UBeMisz$Q;Naw)WNl z@nU2T)+$m8rBl>(MUugJ*i_5A9*9u^?+5n$URsNJFy-4;f#M>xjA_W8;ZpU=O0RQ0 zB>@%JQq?v&$^t%6A){XH8a3j-;h&IHGANa%1`GSLO-oqNm)-@JAlG67ATUR1Fi^&l3<+`@P2T(4 zlfx6(4(D!)pJBo^^HPp`P3s}c75;uXVXbSH-B|wuud|e1wzEr|R^u5nQ6iQR(#SgV z=nclM0S)@@ORB8BT~)=*O|$2pI1jz_R5Fm zWxFt#^&U->@N3t=>W4GGzy_Fz1%D-nNpDFGX%CVhi`Uq0;nhf2Yy>d17$#Zz90Lyv zl}#CsI8OAfihRa>yQLW0czrt-99wlJKB_$ZSt9xF7P!edm;d8Ep2(x8pUHAR;F>Ar z{%oOBdtdQ!hU=)#VwALd8J+U5r^I~4eB6LrXM?DV)m~K_Sy}%5qbR$}>MxLwfVvU~RTd zYbt+}bZ&h!vt@?_0vD@g->h`zDpcvR9GcRoiHEu?gf_x}Lkfp{%DcjEf4-aq8PLXW z6G!`Ej|55kX?xS8tkjHTxE6C;I&Oh8wMJ0#-+YsUT^g8wCk zfNw2eq_jx}KR@P{Q5fs*y&o}NeK~^N$%gH!OgUKwC^h4u%csN|!8{k0#}+2c5($In za>-$P{kE4Dd>8a8y!vuWSliLcALx5Xho`lVC6wD?1L&~1;8M0ozCMF7$!dL%wK`9e zEt{)qJRNzONpekkqR!kuw?G3d)B6s(P}d$;gZY%o^cKrJW(X}RVttZHK;Zr%JMoeb z7tTm=5`DjgFNg$35}-=NAn^n2p?!{w`GJQqN#NSd`5Sf!$v~^sfRgU5IAiZf?Ul;R zaB#PrV93rf22L06-Lh5&Yyms$$M%}@0{1u6U)EK_{MGA7d*S&vAJoa z&e5gK9T<#^CW%oTh78-!k-kcfjl(E@s~LC)WtB89RXtw%l<(!1{gXBYVB?4LQ>e{XRgghP{I?dQ7u(=G}!LH}UO4g()_ z=hPxss6lqSD(}ixm_|HV>J>pXte!G|Qm!mRWuMfL7bm-?izW73@Xk2CI!0hYbfaH` zVXN2P<4meU!o>5VL2{HeqTae(C=?ZH_?WF%$@^MyBk95Qa9TeXa;)8(I6gz^TQ9BcJ;X@qS=bN8G&efd)jju-)ipFlOEZTqeX954Gu3{ACtue@hHD4knAP}1J$XfH>e+BjVUwAV($!N>Y=e*pVYXjJ7TBIqE4|4m@spW+XY;1JnHZP84e9UI(smU0 zy}nj5GVTXIg&MAXM!preP^#92Ogx##zIr^4#=xK8o&9$Csv zY0AtZzi09@`8uETRmB8(Wq$E~UJh?un~of>#cYxknP0%}L15Q?*?wyDQUdroOxeya zIRY9HhL3+@?*-r7mU_@4QFNU~gUj z?vYIfSesJsNM4nVcA2xh;uT?@$KUX-?v^HxgApsU5Ci+FT-T^C(R6*k3LjS47`WO! z?&Ei7-DHu-CrarruMKiaVNgVEIR-GvL6*olCfJ`H*c; z`{GOK?lI}{l!pyYZq2!*k3OZ3+_J6Q`9a%sWh)Ht5&jTCCW9lx!kuU;--orFCTp#y z!}h8VA?*#$42#EU&`F_opb8I08A$^kU19sp}D%t6JmD$bD{!MyR zD^EmMz6CamJ`7qGPhhUa)CK8LuWj4wYEb-VFTkevJd+BYQ3HPGl6@3=oy$G9m*m5! zd21XbzGPVs1rWMe#AmlMSztH8!FG)V70uFTU7s* zzj!gnr&l|Q^-Ffa&Gb`QI&lNr_UGH9vY#DXT?Olh5~5!l@;Y&JKfDP@LudNOLt^+> z2F*H^Z{9U@AM7PLYhNT z34AoRUn8Hvc#=bLZ;i1ZM)qysE08$z27U8|@e*SaF9#8?*uvAOUuAZaC5-X8Ps?oU z+otO4Ud}=mIFAIB$}zVtus8TE0&xXSRS9?^&OlHvndSs?pR>e`zT|DG^m#q*M~kXA zr4zsFNH4PzPCfVxWoAK&_iUHkcX`B$qGcVxC*u8zB4w;^pwn9Y-YmmTHLXmYHuDUe zKZCF|XzNxHa_iV`IIlHO+n4O5xnmQ<+ z)Jg%$30S!D8q->FO<_P|uEg`}r|o=9e}Mt}oLlj7Q{>pHEkpL-@Z9$6l}mx1C7!QM z?902|_O@4P66hCv?Jz|$=6;6tq${3J*K5xix;d=}?$p&z-mSacY?7|y`AO}DO(gRM z`9f7|!&U)C4jV^HKwP7OQgc4ft(r@Czg)~*o8t;RTl_ig!GQjtP2~$Sm4Uk&#heJ= zbJP_2yn;5o{%zI{WG@dRCm1r`0Go@-zd@X54Y>naofV3%Js`s71XlvcG;jWSa6;h= z@uF~|>E<*^#zDo~Zh(7nMk*B+1&lKohwm3%Ez|FR7B5Tt(PQoBJ!9=e4+??v#>Zrs z$+3yFN!z=u?Umn1S8jiElGp^DMU^IwqFlblbG~-Cf*`COaNN&hZTd*P)WCHFsM_rp zb(r1`&qvP8%8TYplL1HFflc~v^8d8|1=CvaR1sg#x$h@eI)c z5t>u&M+UA#4^BrRkK{;cMba60fVW=p%`+W1r*bDJEdKL(Kk-ZckJn0e!A9R)NY$^4> zq)XUdXx}No4^QBM$L+7Gwj+@mZLP2f19&b&a#UFwTw0p93_pHBa3y>t1mqh#3ChOp zSM}ECIi{-2S=-l#9odzmZz&H3ZTqD1!M&6i9E{q+L(;)0j;WJ$rp$*di3I){EIQcY z@h9T=XNWQZ5%`DSCife-te7Ny4FzAddzu`(0(nFiO z=R`*CB{h>g4XGKG)B!0tm>ryzM1+uk(O}e=4F({p_ zHD2X3*;IX*E*0LB(-|!>vQ7hCS{@$TmZ*--<}ALF>W}a@XYC;8jtngj+k_+Td{B!P z4?8xP>a0{|Y@!(08xgIFb`^5#q)I<;qSz>AMoo&0f$AL;Y#7Mn> zQg34WOjQqJM_TW^`~S>;FmpZ#2`#}k;z zF(Uj1A8lXM&EsBmyo#=({EcV*WxEHA#~I=_jKW`7+1bJySGfqSpIoMCnDor#Y~fm} z^=!a%gvEyrglU$K=DE6Ln49X)pG$VMJyksA5BLgqgWbAbb_6E1BLveFU_I$!>nvv9 zI9qVgWK2Su0;~c6aYmw!ID+yQTe?-DHc{O;K}!bhA@xIs*Xh14qjYuBnnVQn3~(txDEc{lIIO6_I5TqmZ95T4%Z~BtBa4Di1t;r#aBFOsbmmv z3k{=fVHRC4o)_i1vnVexr{T$F<(=p`l@nctb7K4Ija@U`8KA|JItxCy_gJ|%bi7B}tf;g9eW#QqrDbNTHQK-9)J^w7{+o$Up~~^X8XoUuDzr`QW*o^^f9iPb zoqKvH2z*@(grFN4Ur!Li7r>UYm4_5QBB~6~q(9Ax2e}-%qy7R-*6@`M5-q*4)SIeS zQv+A@3R4Oa7Dov|VD8U?M9qgSUw;7-QUesPPM_Q5fJHC7?K6s?4??JT`fb_T*O z&0&}e#;71|>$hOECx@k|}6 z(@?K6M;*;I>dN(xR5wVHd#YfYrg*BPySq`SXq2fzrz|8_#xTLoy?_q$rq_gz_t0Sc zPqnu8NkTBH@O?l~4M>6~Fr=wyM`DTeKT1{qCqc#PjQ{?TMPSn=$>)gr3cbM;B? zoMDcZb!2vSxrS7KF^vLM(`J9IrK~%X@VehlAsJ=ju2B{52J(XQcHBFQ`;RSIvv_BG z?S>n-gQ1Skx9uMll5CARf*T^o*;wWQf6_z;EZ}ksN{^yT|)59y{d{{dqbYBRS?0>Nez-1+fD1&hoy`Fs|5nAhFK0tehbz2>N;g8oXuo#nkyGH6f zij05dL|E+9)G}*(sP`-Jkf*}Pr!nFK4OU^I^4l!~I$gUj%8()#7%Xxn(NZI1m>uX* zfipyXJ>Uy`85n;grhbIDcdh(2_L|Y<-Yvuf_X&hUPK*hT)Q9APzBLSFO!rV+$Ap$E zuTuv?#`+m)yTfNR%`sLkc}BS-BX(1MMUzy^ry^?G6q~qL_?+3eG~1*MsE3y^iBs*O zx2jzpqI{qZ)yvn4t`8lzrd61f2&5k|O!HcrKJJ;^x9d3^gcV7zm3;e@vX`-P|8zit z=N@V#VF}7*0uL!LA60hec)k#@R_Xsfsdcd(TjF?5#tf<)bxX@YWkg>?9@VZLB}KN` zD=kc2KIzVMOZCJp`+4<3;+QUi3jYiDnjir58&}(+NPOqwMF1x*20S`P6wE52N+x67 zc1ULm^>^hOaW`=2+*uLvbp_$NZ^)}7h<>ZpTm+k^aaqrOD2`6f**Xmq<39USRVj|L z3$TBfFnj?GUxJ+v9XaJ~j%~qX(1o(xT;)V&9%|@jHL9WBw9IKcwTziUU_)vsqIqi4 z-FUx_RkzP3$N7WH?dK>w{$Os|ob z=^=26r$tE-A(t#6(uFN)E!usrx8T06_)2STV3vxhJi6KC7HY8IP_}?WO&KUbPxmuE zg23Pzpcowt3;4uNgrGeDo$JJgksh4q0r86PJo;uqn`%B~&8KZjQ_;H`rtOy79Tlgg zUuIUOookdh;#^vAd%)!medYUFO_5Rl;dQ?L`M64dskvu~H_9GJd)i%JTp<7eAZ!KZUGc&%JH{s3^a2p%P_ngXh9^^isi=&U`q>XLG7e78U1V9pc z37H3f%LY*y9ujavs?{|jOobx}86B+<=V6U=41KR5XUtB+^z;mDMlXdK&(<%Oj&SyL zp;H$|if89>+N$=!u)<;ej=}l zY#;>PD9+GIDET*nJ|j$>Qp@qnyUMNu_&^%otvI0X`Qe8pZ6hDo{f`C^Q-hc_KDZ~ zB-Bb%{BbsRtmFNEnf!lwfY*eUd@ijQWkf99tq&HR8*r8w=r{;tDlX}qyn2}Jb6k7x zyE+|FKvQQU{RuRm7UYGS_RC}lV!H?OifL+N3i{t~b$V?+f(azs2xJ?c;aciumuH`) zXna;sjVlZ2;P++dg5EZ2yYpaT|J27C;sf3EGhE?`a+uU@Cr!PkM|CD z=oAvR$l%D7_Q>><$rHkes%w`HRgiyX#gc(X5Ji&3{DBAy;6LM3+KwLTA7*(zXfT-3 zuy!3yLx)SJanjZbpgV?;HG@KjqE(RAfWFE~#_Xmj;Sp1G~~f0EV!VPKi5Prp1H zK!c5G(|T9{5isO!6#vXQ>CWxGd|x_(_0Z>Jupl%>)<;E@Zz_nO zrJ7=T#|{gV5Jt+{YrLpt?qkppS$8(p?(Cl|{fT5Qiiw=4!iA}$?n?5mc@K^e{@8yh zeo0^fxMVmFRSgR(xLTr92yKRe=xU6okBtT* zqleS}*Rp4WlZWotA=&924Y)Q!0(Y1ZGh|eO|2G?b<08@JJ5z4tIyqa{wfyl1m%&!Z z=N~)pvL7NStPv+m%t=NDbW5SC=nQpS$Z(g+ANVCmuwPy0Q(KtYhsHKt}P2F3;ZjA zjTzt@OA$U z$N&A*|19u-%lcn6>E9aiZw)z&Nc^KK|JIOyYskMf=)S|m`a$~(H9;=`4@2x2|cQNYbsE5#NpRzj|zO7L;;EU zkO*y}zT2d2Nx!^Cx{3=u?MzW|aebiQ>P1lhTdP`~%|=*#^P@!0W@Rhs*OgYS@_UV+ zif><;>0&uqi<_D07~NYFWjw_)QW zYBzLCIZC)uYHqh7I%okdPBsg^7sXw}Yli=T|zMybYcsP{LxD6P$b)wsFetTieXmshSr z#qQ}aYw zYpV>$;XTPZ$?dn1bMvJes)Ef7+j7YFuFhwXEF`3{3 zj8(8K<0s;}Ax2;|=jj)~&t*Ad9Ax`rQD~ETpqa&wzLBvy)Q~QFRhzw=o1N_hDxZPyUd_HKp>T;R zPRgT1dc6!alkkGBv&gc`%BugTi<2Eef8LVuYUi}qGn>pwn=1{^9TCtmbPTwv8fQ3r zVeY^?)c)!V^Z9rhe(~C$CcG_>MIjKP<_BcAN+$^FINcVT?T()gFOb;2C(r`j+r%`B zUn#uc3lXMZ;@3|>cuw&m7pu0jZX}{Sr-cOkOMA2pv`i}!k8I*h_GgjBGC&q!Nt#Eh z<_eluGF82l25kzDi!Q!adiV*B0{}X-)AjcoPO;9NrS2@7x3eT%tSB!KdZXF zj_?f-VfWc_WcA+o)e)up-CWM?=WFKIGxaJ7>FElh!_J{?GW$DOdBb+XkB6U1h>K^5 z=N&hLmi!j{I_8%Z%(_a7lEgni=X#gVmn5^my5H6BfT9&Qr5{H-c;v)j@0Daz)V|C< zUdCneqXN(82N7O;R0qNvwfo@Vi@Qy2h{lrvxR_x5yPxQZuAr4*JTadD5Aj#dZF=C#%gj*g@5Ain75KZ36fyP4F6I#!?2^|?$TE+MfRXzwYfW8|Wxg%NUF zFk}uZ3Ciyov1tQ7BsH&UYu1W>`1Hhl>8Lky0f{RD!Kx>v&QpWqnj(0h1bH&o(98xl zj~2?t=3(%@03kN0@r3iNtF0Z#L?omE4{hDlJVxA&6X4O=zW7~ZB`$6NFfEA6>*fbA z6-_7ZrFRkht@Dqw=3!9XIpA>7My9`Fq}GQ*Nnmq`BP2;2?eD?G9lScTA@>D3R?#Nx zl(jEgzSUxSDM#$4Ki<21?go_#fjTZqf`~--0JoR)G)OFMZfe+d&{ljVB@|Sm_!~$M ziuJ`{WTUb5;~&Ho{R>$Oj@!eGK8YRu+l#9eKuk>H`JOqg^_xd}9+Oa_I`7~+jfzLG zy7rOMf%>*D#NHk;-5Q-P=rwa+E4G#3q=2m|gOI7kiauiSKt&fcoq-9E?F_h4(i}xI05wL1UE$&QJNX=fD1-mgPtS`nv0-C z>Nh_faQu|x1Oze1lm7t+BGpaD@$XGX#y~SeMixXMvJ2(CCPa+=Q8%r#ZmfNEoLH$L zr^k-HppRJG);bGa;-F}!63&rZ?9nl4RhG)~4!_O;C@U=>Awymi@^2gl?*)P#8CW%S zAxa$w+$H8M@p)e#4q?6nrH?d8;MPdP=9MovC3fGr_;8QpJfKEzo_LOlb#wj|z)S!v zT4(oHx>P|AqDdYIa|JvTxiZwGD=v5E(%3NZK2<-0*-cK_A0d#MaZSbE0LEmf8gf2i& z(6j%_ME<;jYXhV<@>a`KnYe;FpoEx#|9-dSPeS$w&{b(ZI}JWIGuKKm?&n19Vrb>` z*YUu`{eF8xih=<+2iS5Wr^|>tW(I7y0PQmbn218as_0G0aHVh;n=*-05O)1JI2Bv_ zuUG7C+qPxQr?&|Z>IK+fsMrA>6!-~XGuk;k*2Dv@XTSssar620t z3S0{TGi8FM(&Xr+G~e90IG+6s2Gm6qe(l)mbTqhzNMAf!(Vp zO(mA6eDh9jsS-K1(5$r+I4?nuylJz?cWD;~>!exU2FJy*xwyGyXJ*&)|c6*Lcr!Y7?^?eJ7x;w4uH?&SAe=3ubLHuZGa3> zTJ0m4#syloh~{|_^dU((U3o0~my$6+!C$;yO`4L1Gg)c7j8cMmymoR*1-Dh=eG*}+{Wbsd=>OAQMtYY{ejIX zwfX(!206wFNvAPK8UUF5yzN$`D^C(51WocNX_>G)cogERYowvm8wqa!fEa_n!3nz- z1O9G9*%l!N{HvW=7=eAtH89X728H}Y!jTXf!l&SU1g~$@sQ33}+;_lz5U}sG-&a6H zH>K3S>1#(>XooEwc8zWm!^FIub^Cro0SnR>2cy|PRm>(Z8OylptinNNa|aB=fUQ`6 zKSo?=7Eq{4Sj2l`0^5EWtTZ}B?ZMwO=AMAsu9QfsSt8Kg>u_zl1eVp1EBBwpw%8+Q z(h8;kkZ~J!Y+!}$Z;uM1CeX8{yS}u-piOw+S&d{nHg*N4*s7 zByjxyO%PJPY!vV~*eti#a%jBs;}xe8Le#0+Ug#nqx90U-CpuH&kzG=4tx^R^xr#v(0g40c>t=&Sr3imCLY~+@dloSL{k$}Z>5Ng5`-jw;o zQQZG*JP9{}TuRN1^!`lDX$PMyyFpyX6M|E8y<-0DFVQkM0%JQNsP-7f6lYXy6+Y*_ zv+>)?-rhdwjZkyn(Y~7l{r#EF;`4buK|w(;S#+0%YFhwD(EkBiHW_lCfMqSRb4#*t zh7$~sQrxfVJzTZ0d0R|@EuwruJl1{sbH!*lKQn?HSzbQu(U$QrSs0Kgn^o+BB--z4 zt`VM4_@5;GA1nG_E8Q@eo&$D&Z*ji}B+$d&+w=N`X3G1|WOjbh<5EIZIu&HL9@aKm-Qvh3Gb5L9-+F-kZ%PYl1z-)qpqf^Qc(# zT7U1B=+YTyr&c!XpuliOb$`cjuVUq;t8g)5-`ULPn96+aoE8p~8=ADUdA8vJ*x|M*3BrWfA-aY4z`zaUcWi#UZM}yt4&2 z7~quT^( z7?$Y@k~Spe&$+bYAdSogxy^XUPF}^b(s~q&kl6#p?O^BhuN3X^-dmQ;)W!wR6!o3~ zZn4w_~%nR+9&Xum~XRnT-*I4W;5(!EvaK|XS)fo5cywmFueB8Q4v~5WT@Cn0U zv)ks)7ncP^8{|5@e-3!CY3$ES!QI9l^Qo+ELmfT&9}T3)v1`?wBVOx9{mLfBUaMM_ zJIl?xGEeuWMXR~Dk6f-R3uF;hdBq0HDF5ca`=AVPcI*2OEJdI3tgnvz+;SlBQh!0| ze>(Bh$_-+y816?7G2+StLfebt`S}BG&gg~!bvv%6a4RKX)Eh*T^0v@a3K3PQRZS!B z&8g_QA)8@+{uK+O9s>122)CDFEM4eiKnmcp^o`0I00QV^&`?(jdv%{S01%u80tu_z zqx;76l1sjP2|d_Wvh4JDZ>03bH+p2k*8nZAQFd4Tsh5K}&0mMFeUptS9^uQ_s@~tO z&f7+cMHci&lk!OOuwBJ(`B>KhOJ$c(8jt-F7I$5XezIwq zz2Cu?7(cKYzR*w+yGtM{Jr;}eb1K*6y$Ttshkw@Q1uEMzX%?gLfLki*$gix_(fzs! zn7nR;&#v`)|3+l>j;Tf+dS`JS-!9F*-fzzCM$TOR1|l>foM6bcUDoZb{RP&C83&Al zMo+s>89vk5XIrvzsg~9Sa8l*bw8G<79;6?L)0lF>)Jurv;=}EKeBP|8aXj2Zvk+Ih`(u0%r#zv z9BSqDX+O(XAd{`mNW(|XNN0n{5$%4yPgaj}pVWMbGHSIpUHKjC^M5#d??9^i z_kY}MP+C@muJpWK~q=sf_IGYz>roGP9LEPqGh(@AW+0 z_xrBgzt11P|L(iv@OnO<*Yg^W$K$$Q-L$V40TyZxcO_B8xX`TC=#_n?`YXYXIlq9& z`|zXYj$|uE5Br?`V^1w(v28Q6BaQ7J`Ql36*d(VZsdh_)8>Yt?Z0X8Ka4ayiE37p3 znJCW;msL++EMTa=eq!rH6>EWn}DpG&vS?w@$H_O#LSL?w#>O(J&un(3IJwraQUq(;- zdQLj1kB;`ez>XORMA$a|qUhky%&Nsepb+1wwM})u4P`b*IA^NdJK{D=B_aA3jQ|qJ z5=xw!Z%+XtXUqrYQ`Y4&VB0qJHBjXGo2&E+271mDY^$lJq?(?kiofO)?EWtLo=5f> zRlH4*7y+d9Sxa&u3DP7v2$u!IJvfw9EFPeL6bk|3QtO-RWdJ2R*NImDxsX?Wxt?uN zfpn8=6~|9im%v;hBiS`k@wTgNue^ZL2vnOayaiDvk8%Jkwvd^_pMWf@dnKYrupia0 z6Y+N9SF5I8z6fqgQY2mR`gW5mZQ0{Wx@Q_QIL2`Qr}n_I$S)5*%#+diqbe`%QpW4A zbft@>gmgp6Icr+HZ8W;fkk>~=5=*CHO)0?&M5|JW!CI$Io%$n#o|mk1`!(k6KZlfH z-|MBwL>xJ1pZ|Bsbj{Pfm(7tBHz! z;o_J#zt3YbOL%VS1sCaqe^YIT;4RB%-<}?`jBwKLSP?cU(KlWD{@D0id31eIf;Z$M zX=jo;rQqR=Ul`l$P*}n-0HX7uba*O{1PFeoF`tIq%%9Sa^_r(_BrRUyW`QC!EWl4* zQ>lF{(%xRa=29OenJHLJ@Lnt?KK5B=!S*2P1v1D>?o)Z~HEcX7vIKwurf=^Ba_ZHj zc(Y(s&;G%gYA)Bj8_PqFqb_yw#Ey{Dsu9_~PrxebK5lPTp&qT~@+m>we_=XPNk(=T9AEyBfrpI|JJb8jnlQ;Vfv(S zmwmaHPBn-1t1ucokOow0v2s|*k*;7R|4dnao^pfA!!#$V;c8YTv(CQoXB&@$lExb8 zY}?-6Cv%-U4?>O6iA52);6}eZ7yc559^#7muk^B4; zMW3>X?==!77m>*YemZCcE{j_1}_=ADFUK|s31y85%t3gi?HO713IZf%eUXqm1P+b4Rr#=y&Z z9?$RaSvrAmj<7AyTz_!a4m}ecn`j#37meE8JiflaD{jH+ak_m12LA7*9DMuV?StzI69 z%gpqevg!1kt*@1Rb?Mk48M5E)!Q%@AcSoMsh2eT>b9fWpsW3X$XU)yneJsHp za72NkJ5y|hASK9!=q(|(#VT`3ku;_uGm(}v*X`7(lN+HcKME1Q?Qz-M!5}WhGP^T> z!0P4~y6iI%Ou9a&vEG6YZeaFZgHjV-N2GwYX-nhxq7?n?1ngcB2s-9>tbf}f?h(i1 zYtrj~QBnfQHpIM$bKe;*+HZ`GFg^}Z)c=fvvO;fG+{EJBOx}!IfuuP~e+{Z2hwT${L~tWTb`{JE z7F}qn1?3A}I$WknX(w$X7sNAEw0F<>zY3FASm|gSuQ{CA@`zvSq*#N>4wI4_Srg}A zt0hC6Uq;BSt*sE+0l+sY5T1wAD83&oGk(a}sa`z$h&zu&vER*7zR%nD#d@AwE*3C|%v{)lbcZhk)ia1=!&b zm)u*Prm@`V(nXUP_wi(&ef$amKu)}L$$>7sbDdpXsSOS4*#7R}azH-h=UcQQCGAaG zh0Q(GkmSOJUSfy1gNJP&K%z23kF-+qGj!SE@-s9omMzLpfvCp?+j&N$^p9d`O0(gn zLKtXh*7@nhsmI>u!WWDHqbi#TvL6LePe#+7bjkZJw+kv}bq#HG9#-zch{UxP#q|O0 zH0>k1RA%~vRok%QbBGBL=NqOWf3gd;BRw-e!t|qzK{>)d115g;y{b%BLlndd)btPZ zKUgkIc0f;i!uJ+0MXJ&8Fr=m-eq||tDQZ!rErIbrg*|0Lzlf!HoGu1`i&{N=6W*4l zc+8J>Xgmx1dDWlAvq|=tsCzj{B01S3=L8P z+k)7{t#9$z60gbN>^rSU@=)=M^f=ZEbwUy_e(Z(F)m!EDN1!RW1$?z1aNu32K?<^1b7ul>n5U|8m@fa7diBv87H0! z#zzwOAsmFY=*-M#`WT}$Ror`ZsbG(h|oplI~a zt_yP4;)tps_9PENX?trzSe$Zree8d%->(hTvqbUB{^a#iLzF0FBX86H1)us!(c;q? z5x;@z7VPUk?is$2!D&1_svNgAmt-8U1i{V(K&(Qz7Oib<`3)9cIY+jIf4q2+o6m!D z6?nhwQ$V8Aw{JQdbK8id9!DoFXC8DB8$H2Ban8UEuA%J}{;ju{*oN)$PxsH4v z%X#xH=aJ6kaR(*)g zaWz!s^W9r;JFNC#Oc!fb-m;YSA5RUMma$Q?oEW#|h)&Hznlj=PIy&qd!M9Oyz3-mAF8FE8oP0RW6#Rs%0ru zE?!!ZD+De%MVFoZr3s<1@-$0vT->#{TXjksCPySzq$};jEEIDX71^gcOk@}KO1F%> zD7~G+aUeg=Yf?P_`jkOI6%dlzEG9S1lwESzrJ_O6W4FDvmu)Xb5u(&Zf(InsFFZ5~ z76ZO|7|mFoY3%wIcXqqChT*!4mS;UUy=^bTtXQ_@*(01XU6mbS0UrHnsk{f{0a zE2L5o(o~7~(b@E)R`^VQ_cZR8jN}*mGxsk{H`OTftNd&2Kpgg}L}bQNg6b=2!YOVg znqJlrQ-9oLzEiNIwWmjD;sFuWco2Wuz_Cd_Z{`ABSVq@4vbX0Bs?wj2pWh)r7aA-( zy>3ZR?j6G48_b-I)=>+Y79IU_eB*SO-= zQ0f}#8A}Lm6^pZtVtmOi?RfDj03lg#=?=e#D`=py#r8V2iqacKRkZ-hZisuLSDNCB zhPBj2F8gzvdd^g;_zeS6kP~U<)pYqoD7PrwVHLuuPw;ZXf^WQ0S05B6k!F;iy?@?r z-bP}dw@X?9G}tT)u)1pS`J872v;O2}3bt`L{iXu}s56<6_IJpd+Fv_X4{DFQ9hUlG zddH|oQFSs|G?CHqOt4lk=d9=XijtRwi4@b2g)sSm!gT=P6y^IIvJxhc+7xlRf^yCB z%M@dqFj4hcKfs}~vfq+=52q^`1S;L!bSWS^KGbb*CY(_9Ud!lRfDyV+e?hTt=3(6G zAF_|0f6~jL9tD-ez4ZJ}ft8>b??$m%)c`3JzQYvwng}^_PqiEKzSkVC*3mO&LPeR! zoNRKe^e9Z@rlg|TvU5M?@BgsUqoka>Zj7Bh0RQ4AvKt@}5%e)`fQAMyYxhwNRI|T) z;ZPwai5B1N*bRCjD7Fm6#LwwYNm!kvkk@fid-Gwt#AlpXc)!-QQPZ^{)2~YdAtA|H zNxHT3h1Wp^rWH@&@FF$FdqIhR@R5Lc(mB)g_E%=_z5)h(Uq=_4!1$(x@uWuhC#?%B zK%ZD!4Rva;Z^gOVY{9u2##6Yvua^U(HfQxgth9??%24BmsF%bfKtCPao8QbB%L;L@t7uGK-Ou`w{WhCs^N-@FK>Rb$+ zXdYMw#FNEiDo;;Wx+DU;RYMJxyE09K_1?y$m3S8wr8(^|@Ar3h&G$|_k!bP7z}YI6 zmugbn00GWK)_x230sw{MZp?i;kiBK zdIMhiUR`#oWzKq}m8yKM>5Ky?U8(_6FtUAIex(4aiOZW61@l@o#pl~$detw+4R+58 z_rct#CmelJ!w*N%;36tOK8ka26d0od-hZynKX<5QKl~K6fuHd`I$Y9DwUk%b2?BR1{Z^*Im)}@KipnsChx*_*Y}~7@r?l;FEPSsO2?0#&AP8j231M{umYTKXC&>A9** zi01PKd%66xRsZ^( zL+Y@bQiiT*H~qdN!?stu4=_Fz4IMM+!B{^}ZTtE!r2EI!o+w zY~mPb5Jvg+iAC^T6dSk;m@tIPjF>o5glZfXpgzxMet;*G#=3fh$S-_29$(K;FFBa7 zI-#aXu^zX+9G7#i3gKfJhLQuN8RGr2#dqTy2ef-`+}tj4$m`DawtZ_W6R!IE!p3s-cz%zyyIE_pV^pKiUr9B)i)(0Dcsk9gFz@2Ini(goGIk}3pMqv!jzf$hdeDMUowiFrECF##~`QcCmj=0uo)@h=nEUV&oMW? z-USfi)T6Sd>7-9uTS!R#VF8{5dndIwb+1yZSQU$@rznH;U}*Q7W$qKV-vczo!*TiC z+(BOj%P))=4MHu7?Q@zJ7d5_g%-NZ`4TTrRxz-95ya#Di`*-fji=hnBDt!;#GPR}U zOBI!A#o6~_Ou>~60MHdO7lqTyG;vGnw3zDiStnif;e-^XusW2X_cT*W`SFypMwTbEmEXl^}Z9parsP_$^34;G+P*Xz)##yE#3|0jFF~= za=cLZB*IPed(HC>0bOBmrbw4aGA%ppas9deaZ;FI_oAIcK*WUWi$Kd<=7!>{>wjUMQK7QwbQ;Lx0QJvaYe1 zGSIS^Cxq*!IMK-Wk-(eZhX^OCc}Ri~E>Mv~i18*7ho%g4R!%~G@MZTEQ(FL|KfzT^ zi8YQmIfe-Z>RT{%IJzwGyOFZbLL%WV;FL#XZ|%2F7<+SbGjh@UOjkPFzn@Lkwnaxi zxj3|4vOCnqDA}5roD|Cj^?`ndcdMA)McO$O$eUzM1}QhS)+I+saCv@Hx_<3l2;^Gb zJpntoV$&M|7m6*;WcYngDXA*Az;459iP0zL)WO|5W(1z!g$~6JZUgPtBJmADVUwRa z^6jTnCJlzaorM|$*dsF|=)u7zEM{c1t?(qHv|td3#r=l8hyCNVDr=BN*{!%CkCgVU z5C81M0Pug&HY*yEpReZBXNxaG7focUDV8m1e-~-7kBc0I`HR|8M|TdUIVAgygO8P===}lyfDNBVjzJF1Ln374n5G zR&G)Firl-~+XJD%y)hj$f;R^t9Cl}(&S5r0*ayD|mJx6jA6X--Zw=mLW#SjlJT<<|QuRTZWQLC=uDl z3NG#PNcuFpHc4t4=|nvvpP}L&0Ghvc{xy_V!n|jn?1Y`2O0G*o6E=tcOp7n z7JY(QrZFJcuRLnR-$7Xst`ag(!>ez$K{w1VLE6g0PaR51HnBtW5=dO*Pvgl}c$$p~ zqY=)j)!Gj#pro*OHl3y8TW=-`#0*pg1zvh=od0D&jmd-y(6=R!fIKRGYZLZn>9&Tm zgk-dWmwU7~aSY4UVju$7K$;1k+NC_!BMo>peLBXlu~^=^<)uo1rq~Nr4;S%CTmeCH|9GW1A6=ZOWV zJqoL@H>*ir+FT6v7oTfiq07sW#ah5;SRvtYtzEl(T7t`a$;=4sp+oMw(PL9@H{DNV zclvCC!2Sp%D8Qwd^)L9zX&u(-vW)YRfaZigP^PG6I3&E&XktqT__C0ABgMaJr_TU%a+gV0|CuH4CtQ-k`s%Dh1?A@Vuk{Zr=oIcpIfz-%?gw!m$^rftT@Gxk>mw> zYPpG2(8@B}H zHYhm*&Ax$(c>QgS8w`@s61E1q{*tttW{r}+dW}C^%lXs}J~Wgz>^8W(m9{I!n&Hj5 z3<`;-%a@0vP~K~)Gi+0MkD8GzbrRi2>dg#wt(g&3Ff%GO z0Y3>&)dz@^DlIsdyZM{IT%)}5plZL3v07d%H>j!Ow zuLN!~wrNacfow#tRl)wnNgDk67p^sKc8kM$iW+wI<)rJ;plu0bpssO3QLHNt5s1%G zd@r)zP|fZ?7YcT+^vaI>NdhC-`A{}1BiYVTE+3!avBA#2({I6zNFL5URZ2Mr;pT`i zNDcK}T6F^VUDle8`Ktjs4>Wb_l!&!cCY0*tArD@Fe~8t z8`n;A*P<#&vvNQY9Q4d*ix3^-nSzLsB!w#t?M6;&o=C&{2aSHCt692}b%A%XrXyWn zqH75xQxwEoTlZvBZ$gE@f!po59^O!m&EQY$s04h5bqp6Fr$;9Tj6s<8KIt?IkE7gGWae+|gs+m@@q;G7QMH3+z)%0J= zljx%6uG&SH6*O5^K=u$b@rksu7y>~T}hX_r$l2=bI ztBz7qIj|| z!_qIA@%H4n^(0qy!h9hZ&%mp947JDN(`@N%?V)b;zh&vPBe8*@QE}aq`E5Ds0_p?k zH!7`-g4k)38AB^k4zT{kgMGN|Bf==!F5PpCW6KOsx4a8L zQQaVpB{|6}(8e}|Jgh+e~Hw_cy$N& zhWPrEcgG~tu&qd%sEUi<{PA<9@S&aMhD5dIAl#I)`PWc74*)@vapeL+Zno_``Ui1t zOiOG1sEcZAI3&)KtROzzzRtYWwCW~NLu*s)pVGSz60 zY*Gx>XpRRL@D6*S<=cD}n2S4E7S&ZCJ)A9(@oARe5D5D#MVW?R<5gf2dzStBS;BST zyYku${BM;^K<5k~4p^P>1F33~Hm9{HNC2+XQq^fFEMLs1P8@F>+Zk|xA=wQC9$!IY zbMN#+-XXT;lkB#PKoVm;r)*4XTQiy1kb-CiFuf{-l*0^Lm@v35_^N`+zl0}F9Bf?y zkU>@j&l`1;eV*wR^3|f=pyBK2?S?<@*f<3D=?+3SP(J&g1+7&Hc%A{Zo-kzBHHTpf zd-Fu0JsvO|6)1K0>lYmS{cu_i!B6oRDfNse7!M14K7e)`m;KS zodxloPV(OU6@Cs7iYr~qS9)&C#)n$dT}%*|b<1FsD*ket{VSllCWbV4t(ViV_4h%9 z#Ixf>n=Z2VJyfG{z5Drb?w^!dhP;y$y+S~6b`U&v@olc`^Ap7}E3{Sv49ewQ5Pq7K zcu#SNr<`-XrcNvYoJOtdtLqs$9ID4euYP_dFu0cgF}0{u zfD-aMu#MblcB&|N9)wa=Wb7>~GueUvlq&w>JFMY#?3v2xuN7bk1ppa6IbwEPuzdA< z<@Q&1UEzh52d+1q0=e_Qez7@Gt+uszG;}=eO?*n%|6*d5-i^&3=shgOP8;f=YnQZxc>UE?`l%*-zwqa_Dy5An9Rn&| z(%z+=Z`p%Ugj|Y8$IaOJlX9(iGTSMjvw4gj7Lh=}9P;c$Kou(WChjT+Xj{S?f#ut? z#qwrAMZp9emYB0oj~-lf3X}mEto)*;XU2(7gzM=*Ia6m%!{arrL?qx)&` z6MdO6*8<$BM#p@gIt)}*9<3E@X4VF5A?rTxh6hxk(kdz{k~sO}0~uww1X{YCatPMx z-vbdK>Q_5JXDsGlPl@{+gSjxz;+o(1}bD!JJ@t&G#H zQf%-_k1vrewg3`|LY?s%6hBV#S!8pC`(qH1D3PHp0L6*Mdk6?jFoU9=GzkHmPXeS^ zw{>%vF^#3$;wH|`6&u@7cs*2;xC@z|R2i`~vrX~igKi~?(m&|We>Vf8;R64?z)@a7 zMgPb%qIJ6lMX3zm#^nhCyxB_?c<~Ne16~S_s0v%O2i?eAbXNlfADJOUN5SK#NE&t`~GW&TC}<;R>hl@%ST8R`3Hp(D+aZ&5Nug;{4%d z!BqBG3U?QE1C)dR{Gc=j7%)p(=8rqcXXgO*e^8DyAG)8ww86L#?4fsm{Pq=@lkgRz zGsYSeqzFU1yNtQ8k*xgp*8xnIEGlAKsdmV_t-X00>rTn;YbQLRQ_se#bvU-W6dGJZ zK!GA%70eN3!}^oemd-$`!G&VgKF@D&3gf&WXQ&2AS110$#dN~;={E%q0nhuU45&we zOSHJpE-4&=z5F!)6AQ*ZDnZcs0P@_=w!aJ55rP0)yr{&)Fy!hWC+o^z_UaLIYTTF= z`~l~)Gp*}yBno8(HPWYEAPR_*$_Eb5?!*Qo|2r#qnMXxbTpLiV0u2OHsmG#ld%X3& zAi7cUx8liNz=Cl=v#xctL)G5GSbb*|m$nNucqgGV&FqcQ(#NI&kebBbFKG`u#km*3 ztb*Ae4gf}yUkbXJM-YSZHsKc`{Z(m=ZdTeq<+v4BP^-!;8v+go@InWa3Jrt!$$WE1 zd@(&va%-o>hPzk^w~fVqax1dca|hrAcMIKqhSq{{f($fjV3L2sO|*t`Ej8R)z1Up< z*9Xu=7L-l}YW}vAbHkH`25}FlMq%`bLA-T~TgLMfDLGXKd}A$jBKz_+liwr#5*h6P zp1eWa*X-TxOPjd_p-rQZSf8Br@1g=fi_F`!x^AYNBd*FMxI%z9obmbq>hc_gYSr*s zD?3IbAR)JwQo}Y2x51M-XTvq)%PcE|(gjBiPb+)vliE`_sTg6nnPajQtG_|ze%Z*m z*eF90-F^aW)nhj21+=OAc=g^G2x66YcRm{}CE8x!2JYiE$eq5j_br{w@V;cTige^z zkSkXMBpPXBg|wp4AkE2Y(L6IWE}2vX=cA=F1=tsN8K{%<>c4PpKVkbV2>y-|EuOuk zuygJ@7$bn9-#lg|=1sqFMPR}srnCOj>^YrOTy-rxQhE)RCz z(1Ub87C8_{Kx{{7nxoNw8<>Jam~Y2#`)zT43&5s|w7b*cTr;5`uyA4662;|pqO&^% z381rk(DE#12dS;m6GSH7=TcNCTOAS^K|4(~y8VUVEx>2L+D zZ4w(EEe9Ee2dN%C8V96Fy$)yW-jc6Cvi7D^?{EI`+mpdV*KmUi2zGfQPDeR6J&&HV zcXiSVcHT5jeFtuROvC3M0K9TA|7Y0f2^2Rj$Yyl)&HRl>!!7z0rDl)(dsHC7ge3Eo zZJVwnGlpQIPC1v~*~$Amm*AWa_G_70-hu4*7h0JHKZWARFTMs-2H#BBI7J|pRegDxX%HfzZU4R*o{mYoixy4mXvc*c18Rw!{CzlqHzTj?!tL(_HfFj zx1qhaJT6L<8nC@f&?!AB)%bf9*dnHUVIU(TgI~?+z_{B{FK{P@DIZR8{+64Loz zBYO>=-$KEbIyLAWvO8W+S`@&!dVo)IB@B5*Qn;IBH5Ra7Sl|I!TpHkci`xw-N}U$G z1=(1j$qDtLC=hNGCFf~Gdd8LOu;`6%{Qx-WWt3gA6@&{>gDwQ1 z|M!vR+hJv@VC3z-qZd`sh1kTf?@4N3G+W~nQ6Bm{j{?igM-9y0aHV;$Lb>?kqLA{4 z*Of;ik-*__QC|f}nkGq4#ja?I*Djxq3XCC76d0k!qSM7Ax&^89~}%y>WoK zN+GY5`wZ^A795>zm)p+Ytr2$)PT{y)5BZ*}qH^u6vM71QhfHvq$#Sf_ZU*{1;QPgZ z(SiMB7_is9mw=roqZBzQ^=_GTZ?k8RkxPv9uV*9jM$e4{G)aNA@&+DOD7?n zU0;Sfw&R+qN0CGUT>}SL^N7IRFpx)v{WE z%HuAg&oc^#BLN$(;ahhHY$_1?w2o;i6O~zv#Y7CJ7vQ-r!gB$)FyR0_F7JVVd|+eS z4qz)l5{J#hb#ylU9%1n7H>L|m6!uO7p^>vVEZeuN{v|*P04~CA z%DJQEqE#n<&91iWd}(k&72JE3PC;8Jd1G?@z{uGJpYx+V!+68?u3mQEuz&ZE=Tyzl zUjg4)4Cfk4b(a570l-ruCjE@J)RpQX7au_-fdH<(Tp)%M;{%C-d7Rh6UFZuu#B!PL zS}ar!q8BQBk-Pd_Omd3}Q6L`TgfdVH^dSSFh>aC8HFyU)&uY*$w!fNRdX8);^^vIW z8n~LvD;HoMUg5$UgcW&ZG62`0SDr25y-r%p>0gFEUl!nh8?FXCW%#5*XgE$Ur2SbN zZuu2jJbl+?yAANWRU$05q;5G1v{My!?kn$x zZ9Mc0Hmi^sB*Gr@HSnr-vkdJH=KzlDA4(li5#ir74A-OEtM)V0IBPm%gB0#58Sy%j#-`v#3Wnw8j%Au#X3G!}d0^8Fq@9%+K}epG9I4sFJ{cvxLn(KNIMQBox^z0?-C z$@BG169c~DSN{s1a-jS@qY}m3kfV^3SuMa+x%-Gn2RXw-o7sIofv1l1hixn%@zhIv zNIlBW>IUpnB?vwu*uTTLzB?xvr2in!PdqLrOv=>Jy-T8 zkMQ@GKu$Q4qHa2884X#;w15SLJjwgC**L<#7i~fXaD>+b!Klj|HouA6D-mghJD#z* zWk3V8!W#5@mW39Am(9FI;T~(8EVRKgav_u?7_(8M`0Do@!bji(7gJ;*hw@-gk{?(1 z|F#8w42Z&N{|o)nQ+r`uPnMx>sQjB}t;=)MwDN+<0;1RYD-gmB`N~>2vIqBakhij< z(RX$q4ri7eMGe*{1)naie0D6oW>FJEFhH$yr%7p9g4)D1uYTW89f61 zI`adlJ_qgIL5+fxE^yac;I7w@x&X+Xor=UyAqV`kW{p5Cq=qs`a)LXio06YdMzp@q zDtZ6~DWd_IZxz%7gAX-=5o-=SYBQXeA>j3q951$Su$FEr+pg1FLSglVLJ(_I(4UIk+U_07Z6=DvvKQk(A*jF1UNY14=`6yCKxK&dpuW@zw`5yJ)IC=fD8 zzy*##mnq0@yd}BA;~T*Kdc|qjVuZZ`JHQ1PYp_YF`_Q;yL={;@3d~Nu*UKt|`yg^a zFzfy9?Q!=g+$EDK+}{;EW+z+B&r6O(C8D$A4g_NSn$x#T?jv^+y^4b*A^{}`r}z{83%wPfWBLTNl^ACyeVj(rC^HC zKw^|#O^!=;KCrUR$eAH}5aRu92B00TP6FMAWm=MJ2WpfJ1t}!vb1TwmMSwRLO^VP9 zGPC{Dv14#`RD9k>6kcbaZnnBgwg}u7Smpuk9(Yw0lyJu(n~PuPyXJgmTigEM%4td} zTg0MdR6_1<5(1B@g_{5XJrLv)$%*w3-{v6b1+I2^;@YJ#=s#LlbvFMIumxxcKud{J za{8>!V8g$IPKn@hDQygt7&`;=)~9!Y$h&qD~+{Zn+tHdyLWWK7J7>MMMN@D!XWe3bQ(HwqEsv)2hT3#cn_VNDTsu&^o{ z)Hd&70$olTo#zXrYd8<@p&$q^Zm40zjKL7b205Xfi<0WnPVlSm1X!t&5zXC!tF(9D zt{n>>-pN1u{VI`B+&M?vP5<1bfp_8qlI6>4lMoQYs4U$w8IKkt#SjF-dytLs*)KNb zqLPRoG)=K@L$wmm-h}}2GSQXqZXX1UEC8&OK}$Gp>f6};tk+dm=aL3tp^wzl$8@v| z=xnUKQ2vXTrsOV|!;{6QwnE2nHF93S5$QM)?5> z8JD$dh1}B`VVLADq#Q*l659#7^f-91Ea158IL~f zfu2YW+&}gebZTY@NNbhLcD6;GT%Uol`U=?R2l4z|1*9NJEBRi7#3{(BrlbKoapwe` zIckG0y!kojGFc#rUEDft73A#426f_jW~vDADlR#9}io(gsKN{^qt`7 zk3j^>yCn7lH2i799_ivzTh-G9)8qZfl802lXbL*-?S*4-l6NQ?UgGl&N2JOt2&+7e|4PM4}iRWcPncF=D zsXrv;15nM~0q*J#dS1kYxIBXnCV^n-;U~xY9x`g4B`hE{HyeVIS)ujk!ZuDhDUX7c zMS^KNDYCUNcE4@PSw*wAxAg4_hv@cZ{C(H1+(Lc%sG|dWaMnEo6_ZFzq7Utv=Kf5& zhg?|YIbMY^R71vH(hhwqNa6S+0g!1&<<*~5qfR}-Y(Kjpa(SdG1m0wPNK^`irb9oe ziv7ORYYT*L<}tsve9HRPPg_d<6@Ckd4QoeRpLyyj^=Qz`??h!;dr+v}LA~HvK-X3y z&700Skb(K^P@+=RV~Y&p>7KL)+Jo3Fgt=>N^te z4tj+uKHDT2I*CuZ z<#iwO*MYG@$%1m8kY5g!Dbr0Q)wGV~q~19LHkVgNIhPv(I@}CaDC^(lzwxD{Hiy3z z3!jF+ELmedGu)-$-0pl^QnV76Ce%HgDM#>e_98H=Li+#KaVl36%h-f5j$2|aPy zN9@AP-u$D2Re3f9LJhTG06ZQze9{BpjSgr{YuZAMP@lZyts{H7ZfmcZxK zFm%2gDT26e0n>u`2QPq3bUGa1WXs3n8z8Jv2fsavYqRm9@(Og+Ke8@)L?2O)a-K%E zKeP+Vnq;C|%3sqP{Kn2vZa+Z!<;4a$Jth%*kNR7Hfs}&!MDGL4geb<=asqMp097Yx zj#q?qH0V1pLe~ICnO08W1CHuU51zXhP<5u#f1xIP z*$j&l&w`<`O!)|0r8ioDm&Csr;q0wa^YZ-E7cigp4(4HUBIz~ab(49O<Oke zAn(Nxp~+vzN!CmZm5PkZ5T@HZAM? zUfk7>NzX_WnG14EUk6mhoCxPm(1VTUW`m8YkcG6x4xlv`PrGjEy!T&VP5;9!C~&cW zS#+5H;M;$8Iac>Ddgs?)IBA_EaFhQXr{HJ4Hkel?I!3NgX(9yxl+>i1)85)c7BT<) z<^6bSxOK~)Hg9KRL_n_Rgkhic4ZJ$4+zq&4wJ-&V)Q2?U??E&>YoEw4$qfi5QLAjn zKU#oM0bC=ARv%Rupf|h+LB8}$T!E7Z5xS+=kxT*U*qm%SXVw*HuK+^aO?KWnkV+=n z`cn`gb7Vww7f^f942Ypw@NcoZzzP8aaK{3lVr`vaOt?jL^S zvfPJbypJ#g;@tY^_v+e?= zROZyCoUwzxz1y1wDCZcnJ)pha9I*dA*{NS`;h|w_Yuw81LL}xuL*u~sT&IKByAPCh zl>>A8wOVn_K@KLwY%lZh=n>X&;QU*t;;#ng8I{jOHF_m?OcG5*W`*=d=&EV%co5KrDv%xr8(n`$ZqU#`fk~Srr=#S$ z0fF8!=L=5Nc9dM=L8D0oWbvJLN1{yNejQ1Nki(KnawJ7nq0NxJLesfWQ?D zz}V`K3{ws+^dQFP1jf*Kptbq`|(<$7|y6S>9B=XAiSG!wV#S=yYUMrH9c_-2d$^hVG&f=mN1rntJn8{MR0LBLUsGtj;G*ic`qQ$V6rz1%bJ=$ z=8lfQDijK@(_dy8kN?MY6T|NJ*Fs$%pWz6iRtMX>jm|USwJ^glaQ4Grm)tS{J@_*v zvGCJL_B=J{%4F1F(%Ke9lZ*&{ zdorS`RLbbf2~=sUop@f1sKv?rQ^C+!8A&WKOhU3nXjHD7{C4L+DpmWy0u&(JNYpU3 zOOK3NAkfDg@OS=bo(?W&yb_tZaBl-e0~=JfcTowUSAJB|8(Of;!8(Fb-rj!XR0t;r z&&m6vS?@TS>>C(k6=CR!W2QyM-H6vHSB)X%Vb+ZhJ>1NrOFjoIybrGPrtk$MzLw_i7ZR2tk`Y|i3Ew9SX^!sS2B{Pqa>@>`+; zq^+;{H>cs(z^r1{fm&#>v}ojBc8;iHoj0UHcVRTmd!QcUUbJ^dC0=VoO-|LIiV$C{ zin9(oUYb0xpa2$?9ejoz zs5_Q#(ACCPr3lHP1q7l%kUeC!osx|2Ll6W2zV`8XZlFYAcWl&s{Sj48Ff8ai#FE$i z0axSumev>S*O^hCBs?yhX5O5dmsm6WV#ww6#DC7pAN{Dm9~1C&Ac*``|0Y zQ8RW*KrxKHs6>6%$pW5xGXSkRyf$yTF3t=3Ff=W2@9mZs7FN2@{ibs&c;8-_nQ+#&xvP%-wV ztcblZOR-yrd`OfN5;?)o^YBUqv4N}vkCQo6WO!_WA;!7sbm2D5*Wx-@JKa|jL9@Ws z)y8)4>&a-Fql=5C!yzc2`MY%I*;;L^q?ay<*j^qCT0vfTcTIO)wd6eZ=BDj6WpM~| zkSx7--BCS!B!Gc~4H8Wwomq7yG%-f3>2=qbYIzq5`GWeUQ{gFgNw~%KvV}bBVzKld zP%!R9a3xvod`7tNk9JBqHTH`fJ5(o}QO7ty@x605C18kESo#WjzsvkK27vCCKzhLj zoURqxg)eXypK^2`xjy+hB_N97^yz>OMu&dw)|>brZ0b)*C;sW*2&bibPuh>$Q{YQc z)bqmyDjYm_dWgXY#rt1>q+Fl#vUj9*L|9tkoVak|v~k!zOOJ}U)2 zfgtCR)D)j9H0IZTOm)4F*j8Ym@L{>5F!5X`_^N&22UfaSvwaYoY^jvW_+4N&{OwJ$ z)*HI%^$>85ttgAk6P}Gu;V^`Q%A*l$-iO|n@)?E-MjAXrr1i(+4xrrd&bu$vEbPC~ z;hq_p=nyXkbOd-sVoRa;k-=H&Y6sZVesgWCJ3!4Vw~gTCYJiAh`z2 z*L#q}hU=uLGfk$-U&9I9;g#rwL~|hY1jAi>7zm6oT3&@^d#-t?CoIbe1Xy)puo-lI zIkf8NRU-HInb2Lx@Eo^kY<3yVR7Q!ZDEO#Cg@Zcs8tdifWdswzA2``&ZeT}!F#|L( z&_7Oc+HRVBqi78DDp<@XGGf9|f`Fl<3RfL^bBEQaBE4q=tAWAM`7s{X2shM9C`WUH z?{UU7|KWdGOZ)>2>_5N^%I&v&v46E+5kiCeP*=+EQi-;Mq<=IbJKk2e?%8nzK>Qr* z@Z|C7=nTe4)$n*EAAABFgS+`lam8aCGNHZYV(s84gde7MomrKhyAbkK(F-WH(v`mE<9lHQ@IQ?*F3LO znNDFehgRlft=bkX=opgvUXIO@VBDV07{d(&POX9&+@D*EFV(x<$CI;AgCwNTXE3;L ztXYW`T)asAipi8ha=vjabwQJT4U$#^%yw>SDsKa>zSHWy@AycNgEI>`rK5(#Lqh*g zEp-3~Tey96gP2X9t^2U^2@AG(J+Ha?V*qD$a z+Rz|=r%ejo3Vi1CJ#MT#$){;0a_w{gv)HUL33a%qsap3^*ur3wWIFdwNhHSBVRD9{ z&`pBZy0ikM<6YF1YUesuz{sT^I1tO{Tf!AKYuz~N7C&A(rl?421dq!;76T?^AU4((Ye5E)h zb8b15L7)&A)dREviXI6ksMQrob9K-84!5%uZgL`epCzahN#_O^E!^!jG160c2@wVY zvhgqPS3YgB{D6Es86JnMc&YnA{7qhUIL6ObFM_fg{jQfad8?FXHb2{DI=ABfZoA}t zfbjTJJxy*?({#EC)dl^QHSRyc4uW;Vo8*69)xl8o=>9mdxLxOHdm?^5y}!Ttt$>RE zPON|rYLmrEiTi)i`oQo1QATL#>G2aeDO(z|Rg^wn{4~wInhm!8caIzPwP<5&9h7={ zbkCA-9Y81!fKc}7)HzfkBOVu!??a8wxd$=00Gi7jpF;)AlQ8Z?g|mH|sW)vKeSNR; z;{h*TzQ8$+Y!!x&C+3GaJ8=iSuF^d(pQ&Q+;O0YU_Cn0#g^%WeLTwIa6LTy3NoBDv z{kk%*xJhNxe+E9V(|h+jEGMIlOTEu0cYW00i3E!6bEwUwG1G4sIS2Siqp)`7sKo(u z=a089No|ZS@Gno&F)4Df(B0B$O?RXoUILbo=G*z<;MH&nVw!Z)O?srsSpqk)XMHJT zoi#*p?FOPNurBBYI%2wjrXqfx=Wh2#IPHJAJ3;Fo4TZGj}ke7`Q*~ zZw704B4I;UH~IUKheK2DfMq-+x7Ucb8|^_>&PR=$0oi?nRxQokOV%VEL~_;QN66an z|2S}P(ZyR{diM$+zQT$20e=YY`^nQqWMNBgII8CW_WAQa>N$8!Xc|CeayMX?4*h5A6g2%}xYxQ<6-G}He^rQKr z41-~E?)vL@t45wEn=ZZvZt-$s4&4a!#Y9aU36c7GM+ySA1(eHCHeI^bfZwX7UP}AV z(?vV+D>>$V9QL2DMgDJI6~iAqOgWi>G@uOxr?Dyn6DBXxaGykdFdn z(&|;ydyb-X^6*7?qtw4E{AK6;+IEhPk3Yz=2}(GsN_Mi(eMPEy%{|BspScmf0;B19 zCMP$b)Ue@uP+_sUP*5HORJ|;`Psm{Y5h{`ebJ36k-JYaL5dZwIiT6LBz-lX4ouT>D zZDg~H1X8E>szW1L!V~owt^TjU3FwLy(vTA@VVHd)HTmkD<2*ay3P=W;1z;x#n$1`h+b`occ8~t=dhg%5 zC9Z2T`MEAI0Ir(9nOtN%34}kIzvcIW1AXn%E|`RI66sA&2Jd@nqk!?XM{Oygf{;%JXItB_*y9{hL$_(hA;$EY96xjGw*~g z|EGiGf6x5mKb(CrDZbud+||2I?F;kBhf-VPDo`yPTx1CdLKCy9*lY$RS~m1T{d3r0 zt3oul8oSK*s*&&SFTF;#G}=ng10uGId)i@p>q|FFZsQ;BeMci z2Z0o*`t$whLS%TyKfCwy{l%+N{WIVIbsCItB1)A5^`Xs8w2Qz1oa@k|Ao&$asz7>? zk|XWk(3z2^U_htge~vcz-?H({R4H)j3qpFRP+HY zTG(HsQ1st`XB!XQ=M7o3Wq3cl31rOZL>G+5LZfQ#p^-v*iW^aI@>L>RsKMWAGa*HK zG7JgC2DlS@s_awf>@Xf%Jy%)StN*X#6rh4^aeXgJPXNb@ALd3w8IVzrfE2VN5rQaWg0%`_PrK=$gy08r zAktPR44HznY8{Xu<#ZCo0CfZu2_lul3fX<0H-UsCO4j|^Ir*2o=Y5}#d+&3T-60uZZCSUxPYEi*@o2#vzKJPZe}Mrm=R0YK!nE2&tcW0Q|rszv~3{3O$Nr` zb!Z;F1ms+jaM7tyejw^vBS$UA+`SStsu)#vab`1-a_aMAm%{KlNF}qygb4EvXHfqZ zbTngbQ4i)1T}CLlh5X>A&R@iEDxyAYK;72B!*2~%X4TL@c>}o>RjO(37oJGhfF2VS zpPq?g>%6NF8#h;On29#*0qO-sCvyk|yTZmQxZYU>OfyU(TOF6_ncDD0LR}B+b$hs6DqNaPQ@S_1xqP{M!1?W;|N*)=DlM_?put41B+&M;B7l6i2 zxeP|!SX%(@RX}F~hC^(-y|w6!pNC7J6z~vi)dQvPG{7ZnebC5iZHKh<_0OSG={I1c zCZS&A;F)Mzzj)kQ%gPmd;qjMR1&+9Kg&5lxQMl{j1E2Ni18J&~z6U?b4pi*kQ__2e zL|Qqpi5V12kfQRWID0}E34D6aeUNbfk7?*`vax&ldlRzWui%cm>u?+8Flrjoopa6! zci_>4C3JK?{==dah&FH@w3X>P_&S%o!x{^Gn}STmxmDFh zNIW50a!X!q0e`wvGv#FVgsG8>3i@*sw!O(bCF4#;E5w;-4Pf0VXpaSfbq8{nXjrp3 zgMuJ+jcR2lD>SO+iJ)p!OsYYQkDHBt`eB&^5N~ll_Q786PTMw6SqVx{2P)lzNimgd09= ziNr11ZXu2P@i+}<Q^cYE95W5R1 ztA}xAwXSt(rRelEcG*2)hf`leU*Q6S;Y$~{V?`z1Mc|OL!lT;tIdVvC!f}RKx!l14skNu&N~RLeguz2? zZ+Q|F`v^v__tk{n3!Vs=M%CrVQfL;1cop9{04*FY4a9&HoHEL@JefDr|5BgHG4{a& z`k&olXFGP!zO)@-7{o2W$6jfgsKRV(*j?b2v!|}^j@}^tnDVuVyF3Kx03%8jAQGrT z{Jwo8;p%POgvtUANe5SVb)?(qeLT*D+7-6ujgH=rtZ>+sU|yks1_x1Ia%~ZjQSTig zVRdy-Zm*~s!n=Dh8=EDV6gS@1xVC6(znwK4AEGMU6ZP}{>(W$W7i#=;2)yX8Trs0KtoCCI?)z z)}K+#MIA4hyzuG3IQuzH^EvRIJAx8{3>QiMs>DNc<0C3apXWf$=kpg#j^#A!Efh6q z$q5ue_jPkJDrKc8Bx3W=A1K>)7b7nrI!hlyR#K1patn+WN$sm#e*QrI=6-@V>i)Dr z`!xD+v0ykSRxBJi(dRQwMDqdM72kK3tMB-~8M88fLC!(f{{agX0=R__2lW&cS~g%{ zOS(_~>;W{erTIt#16$JM9hy%JY-t`s!oZdcY{{T4%?co0)L_t-=A#4~w554y(*`5% q#m5aC;|3#c9;%>$Eg9I7=jdK>k8fY@U2vL_Hibqp&joM$-G2c0pjILP literal 0 HcmV?d00001 diff --git a/workshops/dcc26/assets/engibench_problems.png b/workshops/dcc26/assets/engibench_problems.png new file mode 100644 index 0000000000000000000000000000000000000000..4417381830e813b82bbdedc87cc199bd56daf7b8 GIT binary patch literal 1001496 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⁣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

yfE*>Y6P!);r!(ej$I8 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