build_all, build_images, build_handbook revised so that the html is not ready to use.
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,301 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import nazca as nd
|
||||
import time
|
||||
|
||||
import mxpic as mx
|
||||
|
||||
RUNS_DIR = ROOT / "runs"
|
||||
OUTPUT_GDS = RUNS_DIR / "build_all_primitives.gds"
|
||||
PRIMITIVE_EXPORT_PACKAGES = (
|
||||
"mxpic.components.primitives.active",
|
||||
"mxpic.components.primitives.passive",
|
||||
"mxpic.components.primitives.pic",
|
||||
)
|
||||
SKIP_CLASSES = {"Route"}
|
||||
|
||||
|
||||
def bootstrap_technology() -> None:
|
||||
"""Register broad layer and xsection defaults used by primitive examples."""
|
||||
mx.technologies.foundry["AMF"]["AMF_Si220_Active"]()
|
||||
|
||||
|
||||
def discover_primitive_classes() -> list[type[Any]]:
|
||||
classes: list[type[Any]] = []
|
||||
seen_class_paths: set[str] = set()
|
||||
|
||||
for package_name in PRIMITIVE_EXPORT_PACKAGES:
|
||||
package = importlib.import_module(package_name)
|
||||
for _, cls in package.__dict__.items():
|
||||
if not inspect.isclass(cls):
|
||||
continue
|
||||
if cls.__name__ in SKIP_CLASSES:
|
||||
continue
|
||||
if "__init__" not in cls.__dict__:
|
||||
continue
|
||||
if not (
|
||||
cls.__module__ == package_name
|
||||
or cls.__module__.startswith(package_name + ".")
|
||||
):
|
||||
continue
|
||||
|
||||
class_path = f"{cls.__module__}.{cls.__name__}"
|
||||
if class_path in seen_class_paths:
|
||||
continue
|
||||
seen_class_paths.add(class_path)
|
||||
classes.append(cls)
|
||||
|
||||
return classes
|
||||
|
||||
|
||||
def safe_name(prefix: str, index: int, class_name: str) -> str:
|
||||
cleaned = re.sub(r"[^A-Za-z0-9_]", "_", class_name)
|
||||
return f"{prefix}{index:03d}_{cleaned}"[:32]
|
||||
|
||||
|
||||
def get_cell(device: Any) -> nd.Cell:
|
||||
if isinstance(device, nd.Cell):
|
||||
return device
|
||||
|
||||
cell = getattr(device, "cell", None)
|
||||
if isinstance(cell, nd.Cell):
|
||||
return cell
|
||||
|
||||
raise TypeError(f"{type(device).__name__} did not expose a nazca Cell through .cell")
|
||||
|
||||
|
||||
def build_fiber_coupler_seed() -> nd.Cell:
|
||||
with nd.Cell(name="_seed_fiber_coupler", instantiate=False) as cell:
|
||||
straight = nd.strt(length=10, width=0.45, xs="strip").put(0, 0, 0)
|
||||
nd.Pin(name="g1", pin=straight.pin["a0"]).put()
|
||||
nd.Pin(name="a0", pin=straight.pin["a0"]).put()
|
||||
nd.Pin(name="b0", pin=straight.pin["b0"]).put()
|
||||
return cell
|
||||
|
||||
|
||||
class BuildContext(dict[str, Any]):
|
||||
def __missing__(self, key: str) -> Any:
|
||||
try:
|
||||
builder = CONTEXT_BUILDERS[key]
|
||||
except KeyError:
|
||||
raise KeyError(key) from None
|
||||
|
||||
value = builder(self)
|
||||
self[key] = value
|
||||
return value
|
||||
|
||||
|
||||
def build_context() -> BuildContext:
|
||||
return BuildContext()
|
||||
|
||||
|
||||
def build_via_i2m(_: BuildContext) -> Any:
|
||||
from mxpic.components.electronics import Vias
|
||||
|
||||
return Vias(
|
||||
xs="via_s2m",
|
||||
area=[1.0, 1.0],
|
||||
sz=[0.25, 0.25],
|
||||
spacing=[0.35, 0.35],
|
||||
xs_l1="p",
|
||||
xs_l2="metal",
|
||||
)
|
||||
|
||||
|
||||
def build_via_h2m(_: BuildContext) -> Any:
|
||||
from mxpic.components.electronics import Vias
|
||||
|
||||
return Vias(
|
||||
xs="via_h2m",
|
||||
area=[1.0, 1.0],
|
||||
sz=[0.25, 0.25],
|
||||
spacing=[0.35, 0.35],
|
||||
xs_l1="heater",
|
||||
xs_l2="metal",
|
||||
)
|
||||
|
||||
|
||||
def build_grating_unit(_: BuildContext) -> Any:
|
||||
from mxpic.components.primitives.pic.gratings import Grating_2D_Hole
|
||||
|
||||
return Grating_2D_Hole()
|
||||
|
||||
|
||||
def build_psr(_: BuildContext) -> Any:
|
||||
from mxpic.components.primitives.pic.taper import PSR
|
||||
|
||||
return PSR(name="_seed_psr")
|
||||
|
||||
|
||||
def build_mdm(_: BuildContext) -> Any:
|
||||
from mxpic.components.primitives.pic.couplers import MDM
|
||||
|
||||
return MDM(name="_seed_mdm")
|
||||
|
||||
|
||||
def build_bragg(_: BuildContext) -> Any:
|
||||
from mxpic.components.primitives.pic.bragg import Bragg
|
||||
|
||||
return Bragg()
|
||||
|
||||
|
||||
def build_crow_ring_device(_: BuildContext) -> Any:
|
||||
from mxpic.components.primitives.pic.rings import AED_ring
|
||||
|
||||
return AED_ring(name="_seed_aed_ring")
|
||||
|
||||
|
||||
def build_crow_bus_device(_: BuildContext) -> Any:
|
||||
from mxpic.components.primitives.pic.couplers import ring_bus_wg
|
||||
|
||||
return ring_bus_wg()
|
||||
|
||||
|
||||
CONTEXT_BUILDERS = {
|
||||
"fiber_coupler": lambda context: build_fiber_coupler_seed(),
|
||||
"grating_unit": build_grating_unit,
|
||||
"psr": build_psr,
|
||||
"mdm": build_mdm,
|
||||
"bragg": build_bragg,
|
||||
"_crow_ring_device": build_crow_ring_device,
|
||||
"crow_ring": lambda context: context["_crow_ring_device"].cell,
|
||||
"crow_w_ring": lambda context: [0.45, 0.65],
|
||||
"crow_sz_ring": lambda context: [20, 20],
|
||||
"_crow_bus_device": build_crow_bus_device,
|
||||
"crow_bus": lambda context: context["_crow_bus_device"].cell,
|
||||
"crow_w_bus": lambda context: context["_crow_bus_device"].w,
|
||||
"crow_sz_bus": lambda context: context["_crow_bus_device"].sz,
|
||||
"via_h2m": build_via_h2m,
|
||||
"via_i2m": build_via_i2m,
|
||||
}
|
||||
|
||||
|
||||
REQUIRED_VALUES: dict[str, Any] = {
|
||||
"A_cp": 10,
|
||||
"Brag": lambda context: context["bragg"],
|
||||
"MDM": lambda context: context["mdm"],
|
||||
"PSR": lambda context: context["psr"],
|
||||
"R0": 10,
|
||||
"R1": 6,
|
||||
"bus": lambda context: context["crow_bus"],
|
||||
"dLx": 0,
|
||||
"dLy": 10,
|
||||
"fiber_coupler": lambda context: context["fiber_coupler"],
|
||||
"gap": 0.2,
|
||||
"grating_unit": lambda context: context["grating_unit"],
|
||||
"number": 4,
|
||||
"pitch": 127,
|
||||
"r0_rck": 6,
|
||||
"r1_rck": 10,
|
||||
"r_ring": 10,
|
||||
"r_rck": 10,
|
||||
"ring": lambda context: context["crow_ring"],
|
||||
"sz_bus": lambda context: context["crow_sz_bus"],
|
||||
"sz_ring": lambda context: context["crow_sz_ring"],
|
||||
"w0": 0.45,
|
||||
"w0_ring": 0.35,
|
||||
"w0_rck": 0.45,
|
||||
"w1": 0.45,
|
||||
"w1_ring": 0.55,
|
||||
"w1_rck": 0.45,
|
||||
"w_bus": 0.45,
|
||||
"w_ring": 0.45,
|
||||
"w_rck": 0.45,
|
||||
}
|
||||
|
||||
|
||||
def class_overrides(
|
||||
class_path: str,
|
||||
_context: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
overrides = {
|
||||
"mxpic.components.primitives.pic.couplers.ADC_STD_2x2": {"Rd1": 20},
|
||||
}
|
||||
|
||||
return dict(overrides.get(class_path, {}))
|
||||
|
||||
|
||||
def required_value(parameter: str, context: dict[str, Any]) -> Any:
|
||||
value = REQUIRED_VALUES[parameter]
|
||||
if callable(value):
|
||||
return value(context)
|
||||
return value
|
||||
|
||||
|
||||
def build_kwargs(cls: type[Any], device_name: str, context: dict[str, Any]) -> dict[str, Any]:
|
||||
signature = inspect.signature(cls)
|
||||
kwargs: dict[str, Any] = {}
|
||||
class_path = f"{cls.__module__}.{cls.__name__}"
|
||||
|
||||
for parameter_name, parameter in signature.parameters.items():
|
||||
if parameter_name == "self":
|
||||
continue
|
||||
|
||||
if parameter_name == "name":
|
||||
kwargs[parameter_name] = device_name
|
||||
continue
|
||||
|
||||
if parameter_name == "cell_name":
|
||||
kwargs[parameter_name] = device_name
|
||||
continue
|
||||
|
||||
if parameter_name == "via_i2m":
|
||||
kwargs[parameter_name] = context["via_i2m"]
|
||||
continue
|
||||
|
||||
if parameter.default is inspect.Parameter.empty:
|
||||
kwargs[parameter_name] = required_value(parameter_name, context)
|
||||
|
||||
kwargs.update(class_overrides(class_path, context))
|
||||
return kwargs
|
||||
|
||||
|
||||
def build_top_cell(cell: nd.Cell, top_name: str) -> nd.Cell:
|
||||
with nd.Cell(name=top_name, instantiate=False) as top_cell:
|
||||
cell.put(0, 0, 0)
|
||||
return top_cell
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bootstrap_technology()
|
||||
context = build_context()
|
||||
topcells = []
|
||||
failures = []
|
||||
|
||||
for index, cls in enumerate(discover_primitive_classes(), start=1):
|
||||
device_name = safe_name("DEV", index, cls.__name__)
|
||||
top_name = safe_name("TOP", index, cls.__name__)
|
||||
class_path = f"{cls.__module__}.{cls.__name__}"
|
||||
|
||||
try:
|
||||
kwargs = build_kwargs(cls, device_name, context)
|
||||
device = cls(**kwargs)
|
||||
topcells.append(build_top_cell(get_cell(device), top_name))
|
||||
print(f"built {top_name}: {class_path}")
|
||||
except Exception as exc:
|
||||
failures.append((class_path, exc))
|
||||
print(f"failed {class_path}: {exc}")
|
||||
|
||||
if failures:
|
||||
print("\nPrimitive build failures:")
|
||||
for class_path, exc in failures:
|
||||
print(f"- {class_path}: {exc}")
|
||||
raise RuntimeError(f"{len(failures)} primitive(s) failed to build")
|
||||
|
||||
nd.export_gds(topcells=topcells, filename=str(OUTPUT_GDS))
|
||||
print(f"\nWrote {len(topcells)} top cells to {OUTPUT_GDS}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,198 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any, Optional
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import nazca as nd
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RUNS_DIR = Path(__file__).resolve().parent
|
||||
OUTPUT_IMAGE_ROOT = ROOT / "docs" / "source" / "images"
|
||||
PACKAGE_ROOT = "mxpic.components.primitives"
|
||||
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
_PRIMITIVE_BUILDER_PATH = RUNS_DIR / "build_all_primitives.py"
|
||||
_PRIMITIVE_BUILDER_SPEC = importlib.util.spec_from_file_location(
|
||||
"mxpic_build_all_primitives",
|
||||
_PRIMITIVE_BUILDER_PATH,
|
||||
)
|
||||
if _PRIMITIVE_BUILDER_SPEC is None or _PRIMITIVE_BUILDER_SPEC.loader is None:
|
||||
raise ImportError(f"Could not load primitive builder: {_PRIMITIVE_BUILDER_PATH}")
|
||||
|
||||
_primitive_builder = importlib.util.module_from_spec(_PRIMITIVE_BUILDER_SPEC)
|
||||
_PRIMITIVE_BUILDER_SPEC.loader.exec_module(_primitive_builder)
|
||||
|
||||
bootstrap_technology = _primitive_builder.bootstrap_technology
|
||||
build_context = _primitive_builder.build_context
|
||||
build_kwargs = _primitive_builder.build_kwargs
|
||||
build_top_cell = _primitive_builder.build_top_cell
|
||||
discover_primitive_classes = _primitive_builder.discover_primitive_classes
|
||||
get_cell = _primitive_builder.get_cell
|
||||
safe_name = _primitive_builder.safe_name
|
||||
|
||||
|
||||
PALETTE = {
|
||||
"WG": "blue",
|
||||
"SLAB": "cyan",
|
||||
"M1": "#FFD700",
|
||||
"M2": "silver",
|
||||
"DEEP_TRENCH": "black",
|
||||
(1, 0): "darkred",
|
||||
(2, 0): "green",
|
||||
(1111, 0): "green",
|
||||
(63, 30): "#FFD700",
|
||||
}
|
||||
|
||||
|
||||
USE_COLOR = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
||||
COLORS = {
|
||||
"bold": "\033[1m",
|
||||
"cyan": "\033[36m",
|
||||
"green": "\033[32m",
|
||||
"red": "\033[31m",
|
||||
"yellow": "\033[33m",
|
||||
"reset": "\033[0m",
|
||||
}
|
||||
STATUS_STYLES = {
|
||||
"info": (">", "cyan", "Info"),
|
||||
"generated": ("+", "green", "Generated"),
|
||||
"skipped": ("-", "yellow", "Skipped"),
|
||||
"failed": ("x", "red", "Failed"),
|
||||
}
|
||||
|
||||
|
||||
def colorize(text: str, color: str) -> str:
|
||||
if not USE_COLOR:
|
||||
return text
|
||||
return f"{COLORS[color]}{text}{COLORS['reset']}"
|
||||
|
||||
|
||||
def status_item(status: str, count: Optional[int] = None) -> str:
|
||||
symbol, color, label = STATUS_STYLES[status]
|
||||
suffix = f": {count}" if count is not None else ""
|
||||
return colorize(f"{symbol} {label}{suffix}", color)
|
||||
|
||||
|
||||
def print_status(status: str, message: str) -> None:
|
||||
print(f"{status_item(status)} {message}")
|
||||
|
||||
|
||||
def apply_mxpic_colors() -> None:
|
||||
"""Apply the mxPIC color palette to Nazca when the layers are available."""
|
||||
print_status("info", "Applying mxPIC layer colors...")
|
||||
for layer_id, color in PALETTE.items():
|
||||
try:
|
||||
nd.set_layercolor(layer=layer_id, color=color)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
def remove_generated_pngs(img_root: Path) -> None:
|
||||
"""Remove previously generated image artifacts."""
|
||||
if not img_root.exists():
|
||||
return
|
||||
|
||||
for png_file in img_root.rglob("*.png"):
|
||||
png_file.unlink()
|
||||
|
||||
|
||||
def image_subdir_for_module(module_name: str, package_root: str) -> Path:
|
||||
module_suffix = module_name.removeprefix(f"{package_root}.")
|
||||
parts = module_suffix.split(".")[:-1]
|
||||
if not parts:
|
||||
return Path()
|
||||
return Path(*parts)
|
||||
|
||||
|
||||
def generate_image_for_class(
|
||||
target_dir: Path,
|
||||
class_name: str,
|
||||
component_class: type,
|
||||
device_name: str,
|
||||
top_name: str,
|
||||
context: dict[str, Any],
|
||||
) -> str:
|
||||
"""Instantiate one component class and save its cell image."""
|
||||
kwargs = build_kwargs(component_class, device_name, context)
|
||||
instance = component_class(**kwargs)
|
||||
top_cell = build_top_cell(get_cell(instance), top_name)
|
||||
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
image_path = target_dir / f"{class_name}.png"
|
||||
nd.export_plt(path="", title=top_name, topcells=[top_cell])
|
||||
plt.savefig(image_path, bbox_inches="tight", dpi=300)
|
||||
plt.close()
|
||||
return "generated"
|
||||
|
||||
|
||||
def generate_component_images(
|
||||
img_root: Path = OUTPUT_IMAGE_ROOT,
|
||||
package_root: str = PACKAGE_ROOT,
|
||||
) -> dict[str, int]:
|
||||
print(colorize("mxPIC primitive image generation", "bold"))
|
||||
|
||||
img_root = Path(img_root)
|
||||
counts = {"generated": 0, "skipped": 0, "failed": 0}
|
||||
|
||||
img_root.mkdir(parents=True, exist_ok=True)
|
||||
remove_generated_pngs(img_root)
|
||||
nd.clear_layout()
|
||||
bootstrap_technology()
|
||||
apply_mxpic_colors()
|
||||
context = build_context()
|
||||
|
||||
for index, component_class in enumerate(discover_primitive_classes(), start=1):
|
||||
class_name = component_class.__name__
|
||||
module_name = component_class.__module__
|
||||
target_dir = img_root / image_subdir_for_module(module_name, package_root)
|
||||
|
||||
try:
|
||||
device_name = safe_name("IMG", index, class_name)
|
||||
top_name = safe_name("IMGTOP", index, class_name)
|
||||
result = generate_image_for_class(
|
||||
target_dir,
|
||||
class_name,
|
||||
component_class,
|
||||
device_name=device_name,
|
||||
top_name=top_name,
|
||||
context=context,
|
||||
)
|
||||
except Exception as error:
|
||||
plt.close()
|
||||
print_status("failed", f"{module_name}.{class_name}: {error}")
|
||||
counts["failed"] += 1
|
||||
continue
|
||||
|
||||
counts[result] += 1
|
||||
if result == "generated":
|
||||
print_status("generated", str(target_dir / f"{class_name}.png"))
|
||||
else:
|
||||
print_status("skipped", f"{module_name}.{class_name} has no cell")
|
||||
|
||||
print(
|
||||
"\n"
|
||||
+ colorize("Image generation complete.", "bold")
|
||||
+ " "
|
||||
+ status_item("generated", counts["generated"])
|
||||
+ " | "
|
||||
+ status_item("skipped", counts["skipped"])
|
||||
+ " | "
|
||||
+ status_item("failed", counts["failed"])
|
||||
)
|
||||
if counts["failed"]:
|
||||
raise RuntimeError(f"{counts['failed']} primitive image(s) failed to build")
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_component_images()
|
||||
@@ -0,0 +1,272 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_SRC_ROOT = ROOT / "mxpic" / "components"
|
||||
DEFAULT_DOCS_ROOT = ROOT / "docs" / "source" / "mxpic" / "components"
|
||||
DEFAULT_IMAGE_ROOT = ROOT / "docs" / "source" / "images"
|
||||
DEFAULT_PACKAGE_ROOT = "mxpic.components"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ModuleDoc:
|
||||
"""Documentation targets discovered in one Python module."""
|
||||
|
||||
source_path: Path
|
||||
relative_path: Path
|
||||
module_name: str
|
||||
classes: tuple[str, ...]
|
||||
functions: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def output_path(self) -> Path:
|
||||
return self.relative_path.with_suffix(".md")
|
||||
|
||||
|
||||
def public_top_level_members(py_file: Path) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
||||
"""Return public top-level classes and functions without importing a module."""
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||
classes: list[str] = []
|
||||
functions: list[str] = []
|
||||
seen_classes: set[str] = set()
|
||||
seen_functions: set[str] = set()
|
||||
|
||||
for node in tree.body:
|
||||
if (
|
||||
isinstance(node, ast.ClassDef)
|
||||
and not node.name.startswith("_")
|
||||
and node.name not in seen_classes
|
||||
):
|
||||
classes.append(node.name)
|
||||
seen_classes.add(node.name)
|
||||
continue
|
||||
|
||||
if (
|
||||
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
and not node.name.startswith("_")
|
||||
and node.name not in seen_functions
|
||||
):
|
||||
functions.append(node.name)
|
||||
seen_functions.add(node.name)
|
||||
|
||||
return tuple(classes), tuple(functions)
|
||||
|
||||
|
||||
def discover_modules(
|
||||
src_root: Path = DEFAULT_SRC_ROOT,
|
||||
package_root: str = DEFAULT_PACKAGE_ROOT,
|
||||
) -> list[ModuleDoc]:
|
||||
"""Discover Python modules that should receive generated Markdown pages."""
|
||||
src_root = Path(src_root)
|
||||
modules: list[ModuleDoc] = []
|
||||
|
||||
for py_file in sorted(src_root.rglob("*.py")):
|
||||
if py_file.name == "__init__.py":
|
||||
continue
|
||||
|
||||
relative_path = py_file.relative_to(src_root)
|
||||
module_suffix = ".".join(relative_path.with_suffix("").parts)
|
||||
classes, functions = public_top_level_members(py_file)
|
||||
modules.append(
|
||||
ModuleDoc(
|
||||
source_path=py_file,
|
||||
relative_path=relative_path,
|
||||
module_name=f"{package_root}.{module_suffix}",
|
||||
classes=classes,
|
||||
functions=functions,
|
||||
)
|
||||
)
|
||||
|
||||
return modules
|
||||
|
||||
|
||||
def remove_generated_markdown(docs_root: Path) -> None:
|
||||
"""Remove generated Markdown while preserving images and other assets."""
|
||||
docs_root = Path(docs_root)
|
||||
if not docs_root.exists():
|
||||
return
|
||||
|
||||
for md_file in docs_root.rglob("*.md"):
|
||||
md_file.unlink()
|
||||
|
||||
|
||||
def primitive_image_subdir(module: ModuleDoc) -> Optional[Path]:
|
||||
parts = module.relative_path.with_suffix("").parts
|
||||
if len(parts) < 3 or parts[0] != "primitives":
|
||||
return None
|
||||
|
||||
return Path(*parts[1:-1])
|
||||
|
||||
|
||||
def image_path_for_class(
|
||||
module: ModuleDoc,
|
||||
class_name: str,
|
||||
image_root: Path,
|
||||
) -> Optional[Path]:
|
||||
image_subdir = primitive_image_subdir(module)
|
||||
if image_subdir is None:
|
||||
return None
|
||||
|
||||
image_path = Path(image_root) / image_subdir / f"{class_name}.png"
|
||||
if image_path.exists():
|
||||
return image_path
|
||||
return None
|
||||
|
||||
|
||||
def relative_image_reference(image_path: Path, md_file_path: Path) -> str:
|
||||
relative_path = os.path.relpath(image_path, start=md_file_path.parent)
|
||||
return relative_path.replace("\\", "/")
|
||||
|
||||
|
||||
def write_module_page(module: ModuleDoc, docs_root: Path, image_root: Path) -> None:
|
||||
"""Write a Markdown page for one Python module."""
|
||||
md_file_path = Path(docs_root) / module.output_path
|
||||
md_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
lines = [
|
||||
f"# {module.module_name}",
|
||||
"",
|
||||
"```{eval-rst}",
|
||||
f".. automodule:: {module.module_name}",
|
||||
" :no-members:",
|
||||
"```",
|
||||
"",
|
||||
]
|
||||
|
||||
for class_name in module.classes:
|
||||
image_path = image_path_for_class(module, class_name, image_root)
|
||||
lines.extend([f"## {class_name}", "", "```{eval-rst}"])
|
||||
|
||||
if image_path is not None:
|
||||
image_reference = relative_image_reference(image_path, md_file_path)
|
||||
lines.extend(
|
||||
[
|
||||
f".. image:: {image_reference}",
|
||||
" :align: center",
|
||||
" :width: 600px",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
f".. autoclass:: {module.module_name}.{class_name}",
|
||||
" :members:",
|
||||
" :undoc-members:",
|
||||
" :show-inheritance:",
|
||||
"```",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
for function_name in module.functions:
|
||||
lines.extend(
|
||||
[
|
||||
f"## {function_name}",
|
||||
"",
|
||||
"```{eval-rst}",
|
||||
f".. autofunction:: {module.module_name}.{function_name}",
|
||||
"```",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
md_file_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
def title_for_directory(relative_dir: Path) -> str:
|
||||
"""Build a readable title for a generated directory index."""
|
||||
if relative_dir == Path("."):
|
||||
return "Components"
|
||||
|
||||
return relative_dir.name.replace("_", " ").title()
|
||||
|
||||
|
||||
def write_index_page(docs_root: Path, relative_dir: Path, entries: list[str]) -> None:
|
||||
"""Write a MyST toctree index for one docs directory."""
|
||||
index_path = Path(docs_root) / relative_dir / "index.md"
|
||||
index_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
lines = [
|
||||
f"# {title_for_directory(relative_dir)}",
|
||||
"",
|
||||
"```{toctree}",
|
||||
" :maxdepth: 2",
|
||||
"",
|
||||
]
|
||||
lines.extend(entries)
|
||||
lines.extend(["```", ""])
|
||||
index_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
def write_index_pages(modules: list[ModuleDoc], docs_root: Path) -> None:
|
||||
"""Create recursive index.md files that include all generated module pages."""
|
||||
directories: set[Path] = {Path(".")}
|
||||
module_entries: dict[Path, list[str]] = {}
|
||||
|
||||
for module in modules:
|
||||
directory = module.output_path.parent
|
||||
directories.add(directory)
|
||||
module_entries.setdefault(directory, []).append(module.output_path.stem)
|
||||
|
||||
current = directory
|
||||
while current != Path("."):
|
||||
current = current.parent
|
||||
directories.add(current)
|
||||
|
||||
sorted_dirs = sorted(
|
||||
directories,
|
||||
key=lambda path: (len(path.parts), path.as_posix()),
|
||||
reverse=True,
|
||||
)
|
||||
for directory in sorted_dirs:
|
||||
child_dirs = sorted(
|
||||
child
|
||||
for child in directories
|
||||
if child != directory and child.parent == directory
|
||||
)
|
||||
entries = [f"{child.name}/index" for child in child_dirs]
|
||||
entries.extend(sorted(module_entries.get(directory, [])))
|
||||
|
||||
if entries or directory == Path("."):
|
||||
write_index_page(docs_root, directory, entries)
|
||||
|
||||
|
||||
def generate_markdown_handbook(
|
||||
src_root: Path = DEFAULT_SRC_ROOT,
|
||||
docs_root: Path = DEFAULT_DOCS_ROOT,
|
||||
image_root: Path = DEFAULT_IMAGE_ROOT,
|
||||
package_root: str = DEFAULT_PACKAGE_ROOT,
|
||||
) -> list[ModuleDoc]:
|
||||
"""Generate Sphinx Markdown pages for mxpic component modules."""
|
||||
print("Starting mxPIC Markdown generation...")
|
||||
|
||||
src_root = Path(src_root)
|
||||
docs_root = Path(docs_root)
|
||||
image_root = Path(image_root)
|
||||
|
||||
if not src_root.exists():
|
||||
raise FileNotFoundError(f"Source directory not found: {src_root}")
|
||||
|
||||
docs_root.mkdir(parents=True, exist_ok=True)
|
||||
remove_generated_markdown(docs_root)
|
||||
|
||||
modules = discover_modules(src_root=src_root, package_root=package_root)
|
||||
for module in modules:
|
||||
write_module_page(module, docs_root, image_root)
|
||||
print(f"Generated docs for: {module.module_name}")
|
||||
|
||||
write_index_pages(modules, docs_root)
|
||||
|
||||
print(f"Markdown generation complete. Updated {len(modules)} module pages.")
|
||||
return modules
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_markdown_handbook()
|
||||
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
import mxpic.technologies as technologies
|
||||
from scripts.export_technology_manifests import build_silterra_eom1_manifest, export_manifests
|
||||
|
||||
|
||||
class TechnologyManifestExportTest(unittest.TestCase):
|
||||
def test_silterra_manifest_contains_route_xsections(self):
|
||||
manifest = build_silterra_eom1_manifest()
|
||||
self.assertEqual(manifest["foundry"], "Silterra")
|
||||
self.assertEqual(manifest["technology"], "EMO1_2ML_CU_Al_RDL")
|
||||
self.assertEqual(manifest["defaults"]["xsection"], "strip")
|
||||
self.assertEqual(manifest["defaults"]["routing_type"], "euler_bend")
|
||||
self.assertIn("standard_bend", manifest["routing_types"])
|
||||
self.assertEqual(manifest["xsections"]["strip"]["family"], "optical")
|
||||
self.assertEqual(manifest["xsections"]["rib_low"]["family"], "optical")
|
||||
self.assertEqual(manifest["xsections"]["metal_1"]["family"], "electrical")
|
||||
self.assertEqual(manifest["xsections"]["metal_2"]["family"], "electrical")
|
||||
self.assertIn("WG_STRIP", manifest["layers"])
|
||||
|
||||
def test_export_writes_eda_technology_yml(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
export_manifests(temp_dir)
|
||||
output = os.path.join(temp_dir, "Silterra", "EMO1_2ML_CU_Al_RDL", "technology.yml")
|
||||
si_palik = os.path.join(
|
||||
temp_dir,
|
||||
"Silterra",
|
||||
"EMO1_2ML_CU_Al_RDL",
|
||||
"EMO1_2ML_CU_materials",
|
||||
"si_palik.csv",
|
||||
)
|
||||
self.assertTrue(os.path.exists(output))
|
||||
self.assertTrue(os.path.exists(si_palik))
|
||||
with open(output, "r", encoding="utf-8") as file:
|
||||
data = yaml.safe_load(file)
|
||||
self.assertEqual(data["xsections"]["metal_1"]["family"], "electrical")
|
||||
|
||||
def test_silterra_technology_loads_from_manifest(self):
|
||||
technology = technologies.foundry["Silterra"]["EMO1_2ML_CU_Al_RDL"]()
|
||||
expected_manifest = os.path.join(
|
||||
"mxpic",
|
||||
"technologies",
|
||||
"silterra",
|
||||
"EMO1_2ML_CU.yml",
|
||||
)
|
||||
self.assertTrue(str(technology.manifest_path).endswith(expected_manifest))
|
||||
self.assertEqual(technology.layers["WG_HM"].layer, (275, 0))
|
||||
self.assertEqual(technology.layers["WG_HM"].z_start, 0.0)
|
||||
self.assertEqual(technology.layers["WG_HM"].thickness, 0.22)
|
||||
self.assertEqual(technology.layers["WG_HM"].sidewall_angle, 83.0)
|
||||
|
||||
si_palik = technology.materials["si_palik"]
|
||||
self.assertTrue(
|
||||
si_palik.data_file.endswith(
|
||||
os.path.join(
|
||||
"mxpic",
|
||||
"technologies",
|
||||
"silterra",
|
||||
"EMO1_2ML_CU_materials",
|
||||
"si_palik.csv",
|
||||
)
|
||||
)
|
||||
)
|
||||
self.assertTrue(os.path.exists(si_palik.data_file))
|
||||
|
||||
def test_foundry_registry_uses_manifest_names(self):
|
||||
loader = technologies.foundry["Silterra"]["EMO1_2ML_CU_Al_RDL"]
|
||||
self.assertEqual(loader.foundry, "Silterra")
|
||||
self.assertEqual(loader.technology, "EMO1_2ML_CU_Al_RDL")
|
||||
self.assertEqual(loader.manifest, "silterra/EMO1_2ML_CU.yml")
|
||||
|
||||
def test_all_modern_technologies_load_from_manifests(self):
|
||||
technology_loaders = (
|
||||
technologies.foundry["CUMEC"]["CUMEC_CSiP130Cu"],
|
||||
technologies.foundry["CUMEC"]["CUMEC_CSiP180Al_PASSIVE"],
|
||||
technologies.foundry["AMF"]["AMF_Si220_Active"],
|
||||
technologies.foundry["ANT"]["ANT_Si220_MPW"],
|
||||
technologies.foundry["consemi"]["PSIN_SOI"],
|
||||
technologies.foundry["IMEC"]["IMEC_Si220_Active"],
|
||||
technologies.foundry["IMECAS"]["IMECAS_SiP"],
|
||||
technologies.foundry["CompTek"]["CT_CU3ML"],
|
||||
technologies.foundry["SITRI"]["SITRI_LSIN_SOI"],
|
||||
technologies.foundry["Silterra"]["EMO1_2ML_CU_Al_RDL"],
|
||||
)
|
||||
|
||||
for technology_loader in technology_loaders:
|
||||
technology = technology_loader()
|
||||
self.assertIsNotNone(technology.manifest_path)
|
||||
self.assertTrue(os.path.exists(technology.manifest_path))
|
||||
self.assertGreater(len(technology.layers), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,137 @@
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
from generate_handbook import generate_markdown_handbook
|
||||
|
||||
|
||||
def write_text(path: Path, content: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
class GenerateHandbookTest(unittest.TestCase):
|
||||
def test_generates_markdown_without_removing_images(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
src_root = root / "components"
|
||||
docs_root = root / "docs" / "source" / "mxpic" / "components"
|
||||
image_root = root / "docs" / "source" / "images"
|
||||
|
||||
write_text(
|
||||
src_root / "primitives" / "pic" / "device.py",
|
||||
'''
|
||||
class ExistingImage:
|
||||
"""A class with a matching generated picture."""
|
||||
|
||||
|
||||
class MissingImage:
|
||||
"""A class without a picture."""
|
||||
|
||||
|
||||
class ExistingImage:
|
||||
"""A duplicate class name that should be documented once."""
|
||||
|
||||
|
||||
def public_function():
|
||||
"""A public function."""
|
||||
|
||||
|
||||
def public_function():
|
||||
"""A duplicate function name that should be documented once."""
|
||||
|
||||
|
||||
def _hidden_function():
|
||||
"""A private function."""
|
||||
''',
|
||||
)
|
||||
write_text(src_root / "basic.py", "CONSTANT = 1\n")
|
||||
write_text(src_root / "primitives" / "__init__.py", "")
|
||||
|
||||
image_path = image_root / "pic" / "ExistingImage.png"
|
||||
image_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
image_path.write_bytes(b"fake png")
|
||||
write_text(docs_root / "old" / "stale.md", "# Stale\n")
|
||||
|
||||
modules = generate_markdown_handbook(
|
||||
src_root=src_root,
|
||||
docs_root=docs_root,
|
||||
image_root=image_root,
|
||||
package_root="fake.components",
|
||||
)
|
||||
|
||||
self.assertEqual(len(modules), 2)
|
||||
self.assertTrue(image_path.exists())
|
||||
self.assertFalse((docs_root / "old" / "stale.md").exists())
|
||||
|
||||
device_page = (docs_root / "primitives" / "pic" / "device.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn("# fake.components.primitives.pic.device", device_page)
|
||||
self.assertIn(".. autoclass:: fake.components.primitives.pic.device.ExistingImage", device_page)
|
||||
self.assertEqual(
|
||||
device_page.count(".. autoclass:: fake.components.primitives.pic.device.ExistingImage"),
|
||||
1,
|
||||
)
|
||||
self.assertIn(".. image:: ../../../../images/pic/ExistingImage.png", device_page)
|
||||
self.assertIn(".. autoclass:: fake.components.primitives.pic.device.MissingImage", device_page)
|
||||
self.assertNotIn(".. image:: MissingImage.png", device_page)
|
||||
self.assertIn(".. autofunction:: fake.components.primitives.pic.device.public_function", device_page)
|
||||
self.assertEqual(
|
||||
device_page.count(".. autofunction:: fake.components.primitives.pic.device.public_function"),
|
||||
1,
|
||||
)
|
||||
self.assertNotIn("_hidden_function", device_page)
|
||||
|
||||
basic_page = (docs_root / "basic.md").read_text(encoding="utf-8")
|
||||
self.assertIn(".. automodule:: fake.components.basic", basic_page)
|
||||
|
||||
def test_recursive_indexes_point_to_existing_generated_pages(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
src_root = root / "components"
|
||||
docs_root = root / "docs" / "source" / "mxpic" / "components"
|
||||
|
||||
write_text(src_root / "basic.py", "def make_basic():\n return None\n")
|
||||
write_text(src_root / "primitives" / "pic" / "device.py", "class Device:\n pass\n")
|
||||
|
||||
generate_markdown_handbook(
|
||||
src_root=src_root,
|
||||
docs_root=docs_root,
|
||||
image_root=root / "docs" / "source" / "images",
|
||||
package_root="fake.components",
|
||||
)
|
||||
|
||||
root_index = (docs_root / "index.md").read_text(encoding="utf-8")
|
||||
primitives_index = (docs_root / "primitives" / "index.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
pic_index = (docs_root / "primitives" / "pic" / "index.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
self.assertIn("basic", root_index)
|
||||
self.assertIn("primitives/index", root_index)
|
||||
self.assertIn("pic/index", primitives_index)
|
||||
self.assertIn("device", pic_index)
|
||||
self.assert_toctree_entries_exist(docs_root)
|
||||
|
||||
def assert_toctree_entries_exist(self, docs_root: Path) -> None:
|
||||
for index_path in docs_root.rglob("index.md"):
|
||||
for line in index_path.read_text(encoding="utf-8").splitlines():
|
||||
entry = line.strip()
|
||||
if not entry or entry.startswith(("#", ":", "```")):
|
||||
continue
|
||||
|
||||
target = index_path.parent / f"{entry}.md"
|
||||
self.assertTrue(target.exists(), f"{index_path} points to missing {target}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user