updated with github #5

Merged
PotatoMaxwell merged 19 commits from qinyue_main into develope 2026-06-01 05:21:23 +00:00
25 changed files with 439 additions and 196 deletions
Showing only changes of commit 1215bf978a - Show all commits
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10 -6
View File
@@ -48,6 +48,7 @@ def init_db():
"created_at": "ALTER TABLE users ADD COLUMN created_at TEXT", "created_at": "ALTER TABLE users ADD COLUMN created_at TEXT",
"credits": "ALTER TABLE users ADD COLUMN credits INTEGER NOT NULL DEFAULT 0", "credits": "ALTER TABLE users ADD COLUMN credits INTEGER NOT NULL DEFAULT 0",
"occupation": "ALTER TABLE users ADD COLUMN occupation TEXT NOT NULL DEFAULT 'intern'", "occupation": "ALTER TABLE users ADD COLUMN occupation TEXT NOT NULL DEFAULT 'intern'",
"user_group": "ALTER TABLE users ADD COLUMN user_group TEXT NOT NULL DEFAULT 'user'",
} }
for column, statement in migrations.items(): for column, statement in migrations.items():
if column not in existing_columns: if column not in existing_columns:
@@ -55,14 +56,17 @@ def init_db():
now = datetime.utcnow().strftime("%Y-%m-%d") now = datetime.utcnow().strftime("%Y-%m-%d")
cursor.execute("UPDATE users SET created_at = ? WHERE created_at IS NULL OR created_at = ''", (now,)) cursor.execute("UPDATE users SET created_at = ? WHERE created_at IS NULL OR created_at = ''", (now,))
cursor.execute("UPDATE users SET user_group = 'manager' WHERE username = 'admin'")
cursor.execute("UPDATE users SET user_group = 'developers' WHERE username = 'engineer'")
cursor.execute("UPDATE users SET user_group = 'user' WHERE user_group IS NULL OR user_group = ''")
# Insert default users for local multi-account development. # Insert default users for local multi-account development.
cursor.execute("SELECT * FROM users WHERE username = 'admin'") cursor.execute("SELECT * FROM users WHERE username = 'admin'")
if not cursor.fetchone(): if not cursor.fetchone():
test_hash = generate_password_hash("123456") test_hash = generate_password_hash("123456")
cursor.execute( cursor.execute(
"INSERT INTO users (username, password_hash, created_at, credits, occupation) VALUES (?, ?, ?, ?, ?)", "INSERT INTO users (username, password_hash, created_at, credits, occupation, user_group) VALUES (?, ?, ?, ?, ?, ?)",
("admin", test_hash, now, 0, "principle engineer") ("admin", test_hash, now, 0, "principle engineer", "manager")
) )
print("Test user created. Username: admin | Password: 123456") print("Test user created. Username: admin | Password: 123456")
@@ -70,8 +74,8 @@ def init_db():
if not cursor.fetchone(): if not cursor.fetchone():
test_hash = generate_password_hash("123456") test_hash = generate_password_hash("123456")
cursor.execute( cursor.execute(
"INSERT INTO users (username, password_hash, created_at, credits, occupation) VALUES (?, ?, ?, ?, ?)", "INSERT INTO users (username, password_hash, created_at, credits, occupation, user_group) VALUES (?, ?, ?, ?, ?, ?)",
("engineer", test_hash, now, 0, "junior engineer") ("engineer", test_hash, now, 0, "junior engineer", "developers")
) )
print("Second test user created. Username: engineer | Password: 123456") print("Second test user created. Username: engineer | Password: 123456")
@@ -81,7 +85,7 @@ def init_db():
def get_user(username): def get_user(username):
conn = connect_db() conn = connect_db()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT id, username, password_hash FROM users WHERE username = ?", (username,)) cursor.execute("SELECT id, username, password_hash, user_group FROM users WHERE username = ?", (username,))
user = cursor.fetchone() user = cursor.fetchone()
conn.close() conn.close()
return user return user
@@ -90,7 +94,7 @@ def get_user_profile(user_id):
conn = connect_db() conn = connect_db()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
"SELECT id, username, created_at, credits, occupation FROM users WHERE id = ?", "SELECT id, username, created_at, credits, occupation, user_group FROM users WHERE id = ?",
(user_id,) (user_id,)
) )
user = cursor.fetchone() user = cursor.fetchone()
+59 -2
View File
@@ -1,5 +1,6 @@
import math import math
import os import os
import sys
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, List from typing import Dict, List
@@ -16,13 +17,34 @@ class BuildResult:
warnings: List[str] = field(default_factory=list) warnings: List[str] = field(default_factory=list)
def build_project_gds(project_dir: str, output_path: str, pdk_public_root: str) -> BuildResult: def build_project_gds(
project_dir: str,
output_path: str,
pdk_public_root: str,
technology_manifest_path: str = None,
prefer_full_gds: bool = False,
) -> BuildResult:
"""Build a hierarchical project GDS from saved cell YAML files.""" """Build a hierarchical project GDS from saved cell YAML files."""
cells = _load_project_cells(project_dir) cells = _load_project_cells(project_dir)
if not cells: if not cells:
raise ValueError("No saved cell YAML files found for this project") raise ValueError("No saved cell YAML files found for this project")
registry = PdkRegistry(pdk_public_root) try:
return _build_with_mxpic_router(
project_dir,
output_path,
pdk_public_root,
technology_manifest_path,
prefer_full_gds,
)
except ImportError as router_error:
if _cells_have_links(cells):
raise RuntimeError(
"Routed Build GDS requires mxpic_router, nazca, and mxpic_forge when layout links are present. "
f"Router import failed: {router_error}"
) from router_error
registry = PdkRegistry(pdk_public_root, prefer_full_gds=prefer_full_gds)
os.makedirs(os.path.dirname(output_path), exist_ok=True) os.makedirs(os.path.dirname(output_path), exist_ok=True)
try: try:
@@ -37,6 +59,33 @@ def build_project_gds(project_dir: str, output_path: str, pdk_public_root: str)
) from nazca_error ) from nazca_error
def _build_with_mxpic_router(
project_dir: str,
output_path: str,
pdk_root: str,
technology_manifest_path: str,
prefer_full_gds: bool,
) -> BuildResult:
router_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "mxpic_router"))
if os.path.isdir(router_root) and router_root not in sys.path:
sys.path.insert(0, router_root)
from mxpic_router import build_project_gds as build_routed_project_gds
result = build_routed_project_gds(
project_dir=project_dir,
output_path=output_path,
pdk_root=pdk_root,
technology_manifest_path=technology_manifest_path,
prefer_full_gds=prefer_full_gds,
)
return BuildResult(
output_path=result.get("output_path", output_path),
engine=result.get("engine", "mxpic_router"),
cells_built=result.get("cells_built", []),
warnings=result.get("warnings", []),
)
def _load_project_cells(project_dir: str) -> Dict[str, dict]: def _load_project_cells(project_dir: str) -> Dict[str, dict]:
cells = {} cells = {}
for filename in sorted(os.listdir(project_dir)): for filename in sorted(os.listdir(project_dir)):
@@ -56,6 +105,14 @@ def _ordered_cell_names(cells: Dict[str, dict]) -> List[str]:
return composites + projects return composites + projects
def _cells_have_links(cells: Dict[str, dict]) -> bool:
for data in cells.values():
for bundle in (data.get("bundles") or {}).values():
if bundle.get("links"):
return True
return False
def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult: def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
import gdstk import gdstk
+60
View File
@@ -0,0 +1,60 @@
import os
import time
import shutil
import uuid
MANAGER_GROUP = "manager"
DEVELOPER_GROUP = "developers"
USER_GROUP = "user"
ALLOWED_GROUPS = {MANAGER_GROUP, DEVELOPER_GROUP, USER_GROUP}
def normalize_user_group(user_group: str) -> str:
group = (user_group or USER_GROUP).strip().lower()
return group if group in ALLOWED_GROUPS else USER_GROUP
def pdk_root_for_group(user_group: str, repo_root: str) -> str:
group = normalize_user_group(user_group)
if group == MANAGER_GROUP:
return os.path.abspath(os.environ.get(
"MXPIC_PDK_ATLAS_ROOT",
os.path.join(repo_root, "opt_pdk_atlas", "foundries"),
))
return os.path.abspath(os.environ.get(
"MXPIC_PDK_PUBLIC_ROOT",
os.path.join(repo_root, "opt_pdk_public", "foundries"),
))
def pdk_root_for_session(session_obj, repo_root: str) -> str:
return pdk_root_for_group(session_obj.get("user_group"), repo_root)
def prefer_full_gds_for_session(session_obj) -> bool:
return normalize_user_group(session_obj.get("user_group")) == MANAGER_GROUP
def create_export_path(export_root: str, project_name: str) -> tuple[str, str, str]:
export_id = uuid.uuid4().hex
filename = f"{project_name}.gds"
export_dir = os.path.abspath(os.path.join(export_root, export_id))
os.makedirs(export_dir, exist_ok=True)
return export_id, filename, os.path.join(export_dir, filename)
def cleanup_expired_exports(export_root: str, max_age_seconds: int = 86400) -> None:
if not os.path.isdir(export_root):
return
now = time.time()
for name in os.listdir(export_root):
path = os.path.join(export_root, name)
if not os.path.isdir(path):
continue
try:
age = now - os.path.getmtime(path)
if age > max_age_seconds:
shutil.rmtree(path, ignore_errors=True)
except OSError:
continue
+18 -11
View File
@@ -16,8 +16,9 @@ class PdkAsset:
class PdkRegistry: class PdkRegistry:
"""Resolve public PDK component names to metadata and public GDS assets.""" """Resolve public PDK component names to metadata and public GDS assets."""
def __init__(self, public_root: str): def __init__(self, public_root: str, prefer_full_gds: bool = False):
self.public_root = os.path.abspath(public_root) self.public_root = os.path.abspath(public_root)
self.prefer_full_gds = prefer_full_gds
self._asset_cache = {} self._asset_cache = {}
def resolve(self, component: str) -> PdkAsset: def resolve(self, component: str) -> PdkAsset:
@@ -60,20 +61,26 @@ class PdkRegistry:
def _find_gds(self, key: str, yaml_path: Optional[str]) -> Optional[str]: def _find_gds(self, key: str, yaml_path: Optional[str]) -> Optional[str]:
search_dir = os.path.dirname(yaml_path) if yaml_path else os.path.join(self.public_root, *key.split("/")) search_dir = os.path.dirname(yaml_path) if yaml_path else os.path.join(self.public_root, *key.split("/"))
name = key.split("/")[-1] name = key.split("/")[-1]
candidates = [ if self.prefer_full_gds:
os.path.join(search_dir, f"{name}_BB.gds"), candidates = [
os.path.join(search_dir, f"{name}.gds"), os.path.join(search_dir, f"{name}.gds"),
] os.path.join(search_dir, f"{name}_BB.gds"),
]
else:
candidates = [
os.path.join(search_dir, f"{name}_BB.gds"),
os.path.join(search_dir, f"{name}.gds"),
]
for candidate in candidates: for candidate in candidates:
if self._inside_root(candidate) and os.path.exists(candidate): if self._inside_root(candidate) and os.path.exists(candidate):
return os.path.abspath(candidate) return os.path.abspath(candidate)
if os.path.isdir(search_dir): if os.path.isdir(search_dir):
for filename in sorted(os.listdir(search_dir)): gds_files = sorted(filename for filename in os.listdir(search_dir) if filename.lower().endswith(".gds"))
if filename.lower().endswith("_bb.gds"): full_files = [filename for filename in gds_files if not filename.lower().endswith("_bb.gds")]
return os.path.join(search_dir, filename) bb_files = [filename for filename in gds_files if filename.lower().endswith("_bb.gds")]
for filename in sorted(os.listdir(search_dir)): ordered = (full_files + bb_files) if self.prefer_full_gds else (bb_files + full_files)
if filename.lower().endswith(".gds"): if ordered:
return os.path.join(search_dir, filename) return os.path.join(search_dir, ordered[0])
return None return None
def _load_yaml(self, yaml_path: Optional[str]) -> Optional[dict]: def _load_yaml(self, yaml_path: Optional[str]) -> Optional[dict]:
+51
View File
@@ -0,0 +1,51 @@
import os
import sys
import tempfile
import yaml
def create_routed_layout_svg(
yaml_content: str,
output_path: str,
pdk_root: str,
project_dir: str,
technology_manifest_path: str = None,
prefer_full_gds: bool = False,
) -> str:
"""Create an SVG preview from routed GDS geometry generated by mxpic_router."""
import gdstk
layout = yaml.safe_load(yaml_content) or {}
cell_name = str(layout.get("name") or "layout")
router_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "mxpic_router"))
if os.path.isdir(router_root) and router_root not in sys.path:
sys.path.insert(0, router_root)
from mxpic_router import build_project_gds
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with tempfile.TemporaryDirectory(prefix="mxpic_routed_preview_") as temp_dir:
temp_gds = os.path.join(temp_dir, f"{cell_name}.gds")
build_project_gds(
project_dir=project_dir,
output_path=temp_gds,
pdk_root=pdk_root,
technology_manifest_path=technology_manifest_path,
prefer_full_gds=prefer_full_gds,
target_cell_name=cell_name,
)
library = gdstk.read_gds(temp_gds)
top_cells = library.top_level()
if not top_cells:
raise RuntimeError("Routed preview GDS has no top-level cell")
top_cells[0].write_svg(output_path)
return output_path
def layout_has_links(yaml_content: str) -> bool:
layout = yaml.safe_load(yaml_content) or {}
for bundle in (layout.get("bundles") or {}).values():
links = bundle.get("links") or []
if links:
return True
return False
+81 -13
View File
@@ -13,6 +13,13 @@ from flask import Response
from gds_builder import build_project_gds from gds_builder import build_project_gds
from layout_preview import create_layout_svg_from_gds from layout_preview import create_layout_svg_from_gds
from pdk_registry import PdkRegistry from pdk_registry import PdkRegistry
from pdk_access import (
cleanup_expired_exports,
create_export_path,
pdk_root_for_session,
prefer_full_gds_for_session,
)
from routed_layout_preview import create_routed_layout_svg, layout_has_links
from technology_manifest import TechnologyManifestError, read_technology_manifest from technology_manifest import TechnologyManifestError, read_technology_manifest
# --- Path Configurations --- # --- Path Configurations ---
@@ -26,13 +33,12 @@ PDK_PUBLIC_ROOT = os.path.abspath(os.environ.get(
)) ))
EDA_PDK_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs')) EDA_PDK_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs'))
YML_PATH = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra', 'directories.yaml') YML_PATH = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra', 'directories.yaml')
COMPS_ROOT = os.environ.get('MXPIC_COMPONENT_ROOT', PDK_PUBLIC_ROOT)
# Define where your new icons folder is located (adjust if it's placed elsewhere) # Define where your new icons folder is located (adjust if it's placed elsewhere)
ICONS_DIR = os.path.join(BASE_DIR, 'icons') ICONS_DIR = os.path.join(BASE_DIR, 'icons')
#build layout save path #build layout save path
DATABASE_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'database')) DATABASE_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'database'))
PDK_REGISTRY = PdkRegistry(PDK_PUBLIC_ROOT) EXPORT_ROOT = os.path.abspath(os.path.join(DATABASE_ROOT, '_exports'))
app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR) app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR)
@@ -120,6 +126,25 @@ def project_gds_path(project_name):
return os.path.join(project_root(project_name), f"{safe_name(project_name, 'project_1')}.gds") return os.path.join(project_root(project_name), f"{safe_name(project_name, 'project_1')}.gds")
def technology_manifest_path_for_project(project_name):
technology_id = read_project_meta(project_name).get("technology") or ""
if "/" not in technology_id:
return None
foundry, technology = technology_id.split("/", 1)
path = os.path.abspath(os.path.join(EDA_PDK_ROOT, safe_name(foundry, ''), safe_name(technology, ''), "technology.yml"))
if path.startswith(EDA_PDK_ROOT + os.sep) and os.path.exists(path):
return path
return None
def current_pdk_root():
return pdk_root_for_session(session, REPO_ROOT)
def current_pdk_registry():
return PdkRegistry(current_pdk_root(), prefer_full_gds=prefer_full_gds_for_session(session))
def project_meta_path(project_name): def project_meta_path(project_name):
return os.path.join(project_root(project_name), ".project.json") return os.path.join(project_root(project_name), ".project.json")
@@ -220,9 +245,10 @@ def getIcon(category):
# ... [Keep existing API routes below] ... # ... [Keep existing API routes below] ...
def readCompYaml(compName): def readCompYaml(compName, comps_root=None):
"""Load YAML from component folder.""" """Load YAML from component folder."""
for root, dirs, files in os.walk(COMPS_ROOT): search_root = comps_root or current_pdk_root()
for root, dirs, files in os.walk(search_root):
if os.path.basename(root) == compName: if os.path.basename(root) == compName:
dirs.clear() dirs.clear()
ymlFiles = [f for f in files if f.endswith('.yml')] ymlFiles = [f for f in files if f.endswith('.yml')]
@@ -252,6 +278,7 @@ def login():
if user and check_password_hash(user[2], password): if user and check_password_hash(user[2], password):
session['user_id'] = user[0] session['user_id'] = user[0]
session['username'] = user[1] session['username'] = user[1]
session['user_group'] = user[3] or 'user'
record_action('login') record_action('login')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
else: else:
@@ -355,6 +382,7 @@ def account_profile():
"created_at": profile[2], "created_at": profile[2],
"credits": profile[3] or 0, "credits": profile[3] or 0,
"occupation": profile[4] or "intern", "occupation": profile[4] or "intern",
"user_group": profile[5] or "user",
"occupations": sorted(occupations) "occupations": sorted(occupations)
}) })
@@ -568,7 +596,17 @@ def save_layout():
f.write(content) f.write(content)
svg_path = cell_svg_path(project, cell) svg_path = cell_svg_path(project, cell)
create_layout_svg_from_gds(content, svg_path, pdk_registry=PDK_REGISTRY, project_dir=project_root(project)) if layout_has_links(content):
create_routed_layout_svg(
content,
svg_path,
pdk_root=current_pdk_root(),
project_dir=project_root(project),
technology_manifest_path=technology_manifest_path_for_project(project),
prefer_full_gds=prefer_full_gds_for_session(session),
)
else:
create_layout_svg_from_gds(content, svg_path, pdk_registry=current_pdk_registry(), project_dir=project_root(project))
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path}) record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path})
return jsonify({ return jsonify({
@@ -605,11 +643,18 @@ def build_gds():
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}
project = safe_name(data.get('project'), 'project_1') project = safe_name(data.get('project'), 'project_1')
try: try:
cleanup_expired_exports(EXPORT_ROOT)
project_dir = ensure_project_path(project) project_dir = ensure_project_path(project)
if not os.path.isdir(project_dir): if not os.path.isdir(project_dir):
return jsonify({"error": "Project not found"}), 404 return jsonify({"error": "Project not found"}), 404
output_path = project_gds_path(project) export_id, filename, output_path = create_export_path(EXPORT_ROOT, safe_name(project, 'project_1'))
result = build_project_gds(project_dir, output_path, PDK_PUBLIC_ROOT) result = build_project_gds(
project_dir,
output_path,
current_pdk_root(),
technology_manifest_path=technology_manifest_path_for_project(project),
prefer_full_gds=prefer_full_gds_for_session(session),
)
record_action('gds.build', project=project, detail={ record_action('gds.build', project=project, detail={
"path": result.output_path, "path": result.output_path,
"engine": result.engine, "engine": result.engine,
@@ -619,7 +664,8 @@ def build_gds():
"message": "GDS built", "message": "GDS built",
"project": project, "project": project,
"path": result.output_path, "path": result.output_path,
"gds_url": url_for('get_project_gds', project_name=project, filename=os.path.basename(result.output_path)), "filename": filename,
"download_url": url_for('download_export', export_id=export_id, filename=filename),
"engine": result.engine, "engine": result.engine,
"cells_built": result.cells_built, "cells_built": result.cells_built,
"warnings": result.warnings "warnings": result.warnings
@@ -628,6 +674,24 @@ def build_gds():
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@app.route('/api/exports/<export_id>/<filename>')
@login_required_json
def download_export(export_id, filename):
export_id = safe_name(export_id, '')
safe_filename = safe_name(filename, 'layout.gds')
export_dir = os.path.abspath(os.path.join(EXPORT_ROOT, export_id))
if not export_id or not export_dir.startswith(EXPORT_ROOT + os.sep):
return jsonify({"error": "Invalid export path"}), 400
if not safe_filename.lower().endswith('.gds'):
return jsonify({"error": "Invalid GDS filename"}), 400
gds_path = os.path.abspath(os.path.join(export_dir, safe_filename))
if not gds_path.startswith(export_dir + os.sep) or not os.path.exists(gds_path):
return jsonify({"error": "GDS export not found"}), 404
response = send_from_directory(export_dir, safe_filename, as_attachment=True)
response.call_on_close(lambda: shutil.rmtree(export_dir, ignore_errors=True))
return response
@app.route('/api/projects/<project_name>/gds/<filename>') @app.route('/api/projects/<project_name>/gds/<filename>')
@login_required_json @login_required_json
def get_project_gds(project_name, filename): def get_project_gds(project_name, filename):
@@ -649,28 +713,32 @@ def get_project_gds(project_name, filename):
# --- API ROUTES (Library & Components) --- # --- API ROUTES (Library & Components) ---
@app.route('/api/library') @app.route('/api/library')
@login_required_json
def getLib(): def getLib():
"""Get library structure.""" """Get library structure."""
# tree = buildTree(YML_PATH) comps_root = current_pdk_root()
if os.path.isdir(COMPS_ROOT): fresh_tree = {}
compMap = findComps(COMPS_ROOT) if os.path.isdir(comps_root):
compMap = findComps(comps_root)
fresh_tree = addCompsToTree(compMap) fresh_tree = addCompsToTree(compMap)
return jsonify(fresh_tree) return jsonify(fresh_tree)
@app.route('/api/component/<component_name>') @app.route('/api/component/<component_name>')
@login_required_json
def getComp(component_name): def getComp(component_name):
"""Return component YAML data.""" """Return component YAML data."""
data = readCompYaml(component_name) data = readCompYaml(component_name, current_pdk_root())
if data is None: if data is None:
return jsonify({"error": "Component not found"}), 404 return jsonify({"error": "Component not found"}), 404
return jsonify(data) return jsonify(data)
@app.route('/api/component/<component_name>/image') @app.route('/api/component/<component_name>/image')
@login_required_json
def getCompImg(component_name): def getCompImg(component_name):
"""Return first image in component folder.""" """Return first image in component folder."""
for root, dirs, files in os.walk(COMPS_ROOT): for root, dirs, files in os.walk(current_pdk_root()):
if os.path.basename(root) == component_name: if os.path.basename(root) == component_name:
dirs.clear() dirs.clear()
for ext in ('.png', '.jpg', '.jpeg', '.svg'): for ext in ('.png', '.jpg', '.jpeg', '.svg'):
Binary file not shown.
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="5048.835" height="1611.555" viewBox="1290.0075 -3803.8125 5048.835 1611.555">
<defs>
<style type="text/css">
.l1200d0 {stroke: #F38400; fill: #F38400; fill-opacity: 0.5;}
.l1205d0 {stroke: #008856; fill: #008856; fill-opacity: 0.5;}
.l1001t0 {stroke: none; fill: #A1CAF1;}
</style>
<g id="Si_EUB_1310_H220_w2000_L50_QY_202604">
<polygon id="0000028EA5D10720" class="l1200d0" points="0,-47.54 409.35,-47.54 409.35,361.82 0,361.82"/>
<polygon id="0000028EA5D102C0" class="l1205d0" points="12.5,12.5 -12.5,12.5 -12.5,-12.5 12.5,-12.5"/>
<polygon id="0000028EA5D103A0" class="l1205d0" points="374.31,349.32 374.31,374.32 349.31,374.32 349.31,349.32"/>
<polygon id="0000028EA5D10410" class="l1205d0" points="12.5,12.5 -12.5,12.5 -12.5,-12.5 12.5,-12.5"/>
<polygon id="0000028EA81CD1D0" class="l1205d0" points="374.31,349.32 374.31,374.32 349.31,374.32 349.31,349.32"/>
<text id="0000028EA5D7B970" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a0</text>
<text id="0000028EA5D7BBB0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(361.81 361.82) scale(1 -1)">b0</text>
<text id="0000028EA5D7BE80" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a1</text>
<text id="0000028EA5D7D590" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(361.81 361.82) scale(1 -1)">b1</text>
</g>
<g id="2x2MMI_1310nm_TE_Silterra_202603_ZKY">
<polygon id="0000028EA81CD4E0" class="l1200d0" points="-917,-148.25 917,-148.25 917,148.25 -917,148.25"/>
<polygon id="0000028EA81CD940" class="l1205d0" points="-913.5,48.25 -920.5,48.25 -920.5,41.25 -913.5,41.25"/>
<polygon id="0000028EA81CCDE0" class="l1205d0" points="-913.5,-41.25 -920.5,-41.25 -920.5,-48.25 -913.5,-48.25"/>
<polygon id="0000028EA81CD320" class="l1205d0" points="913.5,41.25 920.5,41.25 920.5,48.25 913.5,48.25"/>
<polygon id="0000028EA81CCEC0" class="l1205d0" points="913.5,-48.25 920.5,-48.25 920.5,-41.25 913.5,-41.25"/>
<polygon id="0000028EA81CDC50" class="l1205d0" points="0,0 0,0 0,0 0,0"/>
<polygon id="0000028EA81CD080" class="l1205d0" points="0,0 0,0 0,0 0,0"/>
<text id="0000028EA5D7B7C0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(-917 44.75) scale(1 -1)">a1</text>
<text id="0000028EA5D7CED0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(-917 -44.75) scale(1 -1)">a2</text>
<text id="0000028EA5D7BC40" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(917 44.75) scale(1 -1)">b1</text>
<text id="0000028EA5D7C4B0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(917 -44.75) scale(1 -1)">b2</text>
<text id="0000028EA5D7C420" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a0</text>
<text id="0000028EA5D7C390" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">b0</text>
</g>
</defs>
<rect x="1290.0075" y="-3803.8125" width="5048.835" height="1611.555" fill="#222222" stroke="none"/>
<g id="canvas_1" transform="scale(1 -1)">
<use transform="translate(5700 3200)" xlink:href="#Si_EUB_1310_H220_w2000_L50_QY_202604"/>
<use transform="translate(2440 2570)" xlink:href="#2x2MMI_1310nm_TE_Silterra_202603_ZKY"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

@@ -1,61 +0,0 @@
# =============================================
# mxPIC Cell/Project Definition File
# =============================================
schema_version: "2.0.0"
kind: cell
project: mxpic_project_1
name: canvas_1
type: composite
version: "1.0.0"
# 1. External Ports (How this cell connects to the outside world)
ports:
- name: port
layer: WG_CORE
x: 0.0
y: 0.0
angle: 0.0
width: 0.5
# 2. Instances (The sub-components dropped onto this canvas)
instances:
component_5:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/bendings/Si_EUB_1310_H220_w2000_L50_QY_202604
x: 570.0
y: 320.0
rotation: 0.0
mirror: false
settings:
length:
component_6:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/2x2MMI_1310nm_TE_Silterra_202603_ZKY
x: 244.0
y: 257.0
rotation: 0.0
mirror: false
settings:
length:
elements:
port:
type: port
x: 0.0
y: 0.0
angle: 0.0
layer: WG_CORE
width: 0.5
description: ""
# 3. Bundles (Grouped links for multi-bus/parallel routing)
bundles:
output_bus:
routing_type: euler_bend
links:
- from: component_5:a1
to: component_6:b2
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="4982.835" height="3175.555" viewBox="6753.0075 -5700.8125 4982.835 3175.555">
<defs>
<style type="text/css">
.l1200d0 {stroke: #F38400; fill: #F38400; fill-opacity: 0.5;}
.l1205d0 {stroke: #008856; fill: #008856; fill-opacity: 0.5;}
.l1001t0 {stroke: none; fill: #A1CAF1;}
</style>
<g id="canvas_1">
<use transform="translate(7200 2200)" xlink:href="#Si_EUB_1310_H220_w2000_L50_QY_202604"/>
<use transform="translate(4000 1900)" xlink:href="#2x2MMI_1310nm_TE_Silterra_202603_ZKY"/>
</g>
<g id="Si_EUB_1310_H220_w2000_L50_QY_202604">
<polygon id="0000019DC7BDA230" class="l1200d0" points="0,-47.54 409.35,-47.54 409.35,361.82 0,361.82"/>
<polygon id="0000019DC7BDA310" class="l1205d0" points="12.5,12.5 -12.5,12.5 -12.5,-12.5 12.5,-12.5"/>
<polygon id="0000019DC7BDA460" class="l1205d0" points="374.31,349.32 374.31,374.32 349.31,374.32 349.31,349.32"/>
<polygon id="0000019DC7BDA4D0" class="l1205d0" points="12.5,12.5 -12.5,12.5 -12.5,-12.5 12.5,-12.5"/>
<polygon id="0000019DCA15A120" class="l1205d0" points="374.31,349.32 374.31,374.32 349.31,374.32 349.31,349.32"/>
<text id="0000019DC9DB67A0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a0</text>
<text id="0000019DC9DB5990" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(361.81 361.82) scale(1 -1)">b0</text>
<text id="0000019DC9DB64D0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a1</text>
<text id="0000019DC9DB5A20" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(361.81 361.82) scale(1 -1)">b1</text>
</g>
<g id="2x2MMI_1310nm_TE_Silterra_202603_ZKY">
<polygon id="0000019DCA15A270" class="l1200d0" points="-917,-148.25 917,-148.25 917,148.25 -917,148.25"/>
<polygon id="0000019DCA159DA0" class="l1205d0" points="-913.5,48.25 -920.5,48.25 -920.5,41.25 -913.5,41.25"/>
<polygon id="0000019DCA159F60" class="l1205d0" points="-913.5,-41.25 -920.5,-41.25 -920.5,-48.25 -913.5,-48.25"/>
<polygon id="0000019DCA15A200" class="l1205d0" points="913.5,41.25 920.5,41.25 920.5,48.25 913.5,48.25"/>
<polygon id="0000019DCA15A2E0" class="l1205d0" points="913.5,-48.25 920.5,-48.25 920.5,-41.25 913.5,-41.25"/>
<polygon id="0000019DCA15A350" class="l1205d0" points="0,0 0,0 0,0 0,0"/>
<polygon id="0000019DCA15A4A0" class="l1205d0" points="0,0 0,0 0,0 0,0"/>
<text id="0000019DC9DB6050" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(-917 44.75) scale(1 -1)">a1</text>
<text id="0000019DC9DB6C20" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(-917 -44.75) scale(1 -1)">a2</text>
<text id="0000019DC9DB7130" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(917 44.75) scale(1 -1)">b1</text>
<text id="0000019DC9DB5EA0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(917 -44.75) scale(1 -1)">b2</text>
<text id="0000019DC9DB71C0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a0</text>
<text id="0000019DC9DB7400" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">b0</text>
</g>
</defs>
<rect x="6753.0075" y="-5700.8125" width="4982.835" height="3175.555" fill="#222222" stroke="none"/>
<g id="mxpic_project_1" transform="scale(1 -1)">
<use transform="translate(3900 2900)" xlink:href="#canvas_1"/>
<use transform="translate(8400 2900)" xlink:href="#2x2MMI_1310nm_TE_Silterra_202603_ZKY"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

@@ -9,34 +9,69 @@ type: project
version: "1.0.0" version: "1.0.0"
# 1. External Ports (How this cell connects to the outside world) # 1. External Ports (How this cell connects to the outside world)
ports: [] ports:
- name: port
layer: WG_CORE
x: 50.0
y: 150.0
angle: 0.0
width: 0.5
# 2. Instances (The sub-components dropped onto this canvas) # 2. Instances (The sub-components dropped onto this canvas)
instances: instances:
canvas_1: component_1:
component: canvas_1
x: 390.0
y: 290.0
rotation: 0.0
mirror: false
settings:
length:
component_7:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/2x2MMI_1310nm_TE_Silterra_202603_ZKY component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/2x2MMI_1310nm_TE_Silterra_202603_ZKY
x: 840.0 x: 300.0
y: 290.0 y: 440.0
rotation: 0.0 rotation: 0.0
mirror: false mirror: false
settings: settings:
length: length:
elements: {} component_2:
component: Silterra/EMO1_2ML_CU_Al_RDL/composites/Mach_Zender_modulators/MZM_1600G_L3000_GSSG_TRAIL_TypeA2_QY_v1_20260303
x: 820.0
y: 250.0
rotation: 0.0
mirror: false
settings:
length:
component_3:
component: Silterra/EMO1_2ML_CU_Al_RDL/composites/Mach_Zender_modulators/MZM_1600G_L3000_GSSG_TRAIL_TypeA2_QY_v1_20260303
x: 820.0
y: 660.0
rotation: 0.0
mirror: false
settings:
length:
elements:
port:
type: port
x: 50.0
y: 150.0
angle: 0.0
layer: WG_CORE
width: 0.5
description: ""
# 3. Bundles (Grouped links for multi-bus/parallel routing) # 3. Bundles (Grouped links for multi-bus/parallel routing)
bundles: bundles:
output_bus: output_bus:
routing_type: euler_bend routing_type: euler_bend
links: links:
- from: component_7:a1 - from: component_2:g2b
to: canvas_1:port to: component_1:b1
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: component_1:b2
to: component_3:g2b
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
Binary file not shown.
+10 -1
View File
@@ -3948,7 +3948,16 @@ ${bundlesBlock}`;
const warningText = result.warnings && result.warnings.length > 0 const warningText = result.warnings && result.warnings.length > 0
? ` (${result.warnings.length} warnings)` ? ` (${result.warnings.length} warnings)`
: ''; : '';
addLog(`GDS built with ${result.engine}: ${result.path}${warningText}`); if (result.download_url) {
const link = document.createElement('a');
link.href = result.download_url;
link.download = result.filename || `${currentProjectName}.gds`;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
link.remove();
}
addLog(`GDS built with ${result.engine}: ${result.filename || result.path}${warningText}`);
} catch (err) { } catch (err) {
addLog(`Build GDS network error: ${err.message}. Check that the Flask server is running from the same host and Python environment.`); addLog(`Build GDS network error: ${err.message}. Check that the Flask server is running from the same host and Python environment.`);
} finally { } finally {
+6
View File
@@ -608,6 +608,10 @@
<label>Credits</label> <label>Credits</label>
<div class="profile-value" id="profile-credits">0</div> <div class="profile-value" id="profile-credits">0</div>
</div> </div>
<div class="profile-item">
<label>User Group</label>
<div class="profile-value" id="profile-group">-</div>
</div>
<div class="profile-item"> <div class="profile-item">
<label>Occupation</label> <label>Occupation</label>
<select id="profile-occupation"></select> <select id="profile-occupation"></select>
@@ -694,6 +698,7 @@
const profileUsername = document.getElementById('profile-username'); const profileUsername = document.getElementById('profile-username');
const profileCreated = document.getElementById('profile-created'); const profileCreated = document.getElementById('profile-created');
const profileCredits = document.getElementById('profile-credits'); const profileCredits = document.getElementById('profile-credits');
const profileGroup = document.getElementById('profile-group');
const profileOccupation = document.getElementById('profile-occupation'); const profileOccupation = document.getElementById('profile-occupation');
const themeToggle = document.getElementById('theme-toggle'); const themeToggle = document.getElementById('theme-toggle');
const logTerminal = document.getElementById('log-terminal'); const logTerminal = document.getElementById('log-terminal');
@@ -733,6 +738,7 @@
profileUsername.textContent = profile.username; profileUsername.textContent = profile.username;
profileCreated.textContent = profile.created_at || '-'; profileCreated.textContent = profile.created_at || '-';
profileCredits.textContent = profile.credits ?? 0; profileCredits.textContent = profile.credits ?? 0;
profileGroup.textContent = profile.user_group || 'user';
profileOccupation.innerHTML = ''; profileOccupation.innerHTML = '';
(profile.occupations || []).forEach(occupation => { (profile.occupations || []).forEach(occupation => {
const option = document.createElement('option'); const option = document.createElement('option');
@@ -13,6 +13,7 @@ layers:
WG_HM: {layer: 275, datatype: 0} WG_HM: {layer: 275, datatype: 0}
WG_STRIP: {layer: 101, datatype: 251} WG_STRIP: {layer: 101, datatype: 251}
WG_LOWRIB: {layer: 100, datatype: 90} WG_LOWRIB: {layer: 100, datatype: 90}
WG_SRIB: {layer: 100, datatype: 90}
WG_HIGHRIB: {layer: 232, datatype: 0} WG_HIGHRIB: {layer: 232, datatype: 0}
HEATER: {layer: 29, datatype: 30} HEATER: {layer: 29, datatype: 30}
CT_SI: {layer: 268, datatype: 0} CT_SI: {layer: 268, datatype: 0}
+46
View File
@@ -0,0 +1,46 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const root = path.resolve(__dirname, '..');
const backend = path.join(root, 'backend');
const databasePy = fs.readFileSync(path.join(backend, 'database.py'), 'utf8');
const serverPy = fs.readFileSync(path.join(backend, 'server.py'), 'utf8');
const dashboardHtml = fs.readFileSync(path.join(root, 'frontend', 'dashboard.html'), 'utf8');
assert(
databasePy.includes('user_group'),
'database migration should add users.user_group'
);
assert(
databasePy.includes("'admin'") && databasePy.includes("'manager'"),
'admin should be migrated/seeded as manager'
);
assert(
databasePy.includes("'engineer'") && databasePy.includes("'developers'"),
'engineer should be migrated/seeded as developers'
);
assert(
serverPy.includes("session['user_group']") || serverPy.includes('session["user_group"]'),
'login should store user_group in the session'
);
assert(
serverPy.includes('"user_group"'),
'/api/profile should return user_group'
);
assert(
fs.existsSync(path.join(backend, 'pdk_access.py')),
'backend/pdk_access.py should resolve role-based PDK roots'
);
assert(
serverPy.includes('pdk_root_for_session'),
'server should resolve PDK root per logged-in user group'
);
assert(
dashboardHtml.includes('profile-group'),
'dashboard should show read-only profile group information'
);
assert(
!dashboardHtml.includes('profile-group"></select'),
'profile group must not be editable'
);
+32
View File
@@ -0,0 +1,32 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const root = path.resolve(__dirname, '..');
const serverPy = fs.readFileSync(path.join(root, 'backend', 'server.py'), 'utf8');
const canvasHtml = fs.readFileSync(path.join(root, 'frontend', 'canvas.html'), 'utf8');
assert(
serverPy.includes('EXPORT_ROOT'),
'server should build GDS exports into a temporary export root'
);
assert(
serverPy.includes("@app.route('/api/exports/<export_id>/<filename>'"),
'server should expose an authenticated export download route'
);
assert(
serverPy.includes('download_url'),
'Build GDS response should include download_url'
);
assert(
serverPy.includes('cleanup_expired_exports'),
'server should clean exports older than the retention period'
);
assert(
canvasHtml.includes('download_url'),
'frontend should read download_url from Build GDS response'
);
assert(
canvasHtml.includes('document.createElement') && canvasHtml.includes('.download'),
'frontend should trigger a browser download for generated GDS'
);
+14
View File
@@ -18,10 +18,18 @@ assert(
fs.existsSync(path.join(backendDir, 'gds_builder.py')), fs.existsSync(path.join(backendDir, 'gds_builder.py')),
'backend/gds_builder.py should build hierarchical GDS from saved project YAML' 'backend/gds_builder.py should build hierarchical GDS from saved project YAML'
); );
assert(
fs.existsSync(path.join(backendDir, 'routed_layout_preview.py')),
'backend/routed_layout_preview.py should create routed SVG previews through mxpic_router'
);
assert( assert(
serverPy.includes('create_layout_svg_from_gds'), serverPy.includes('create_layout_svg_from_gds'),
'save-layout route should create a GDS-derived layout SVG preview' 'save-layout route should create a GDS-derived layout SVG preview'
); );
assert(
serverPy.includes('create_routed_layout_svg'),
'save-layout route should use routed preview generation when links exist'
);
assert( assert(
serverPy.includes('svg_url'), serverPy.includes('svg_url'),
'save-layout response should include an svg_url for the new layout tab' 'save-layout response should include an svg_url for the new layout tab'
@@ -64,3 +72,9 @@ assert(
layoutPreviewPy.includes('_BB.gds') || layoutPreviewPy.includes('gds_path'), layoutPreviewPy.includes('_BB.gds') || layoutPreviewPy.includes('gds_path'),
'layout_preview.py should resolve public GDS assets for placed components' 'layout_preview.py should resolve public GDS assets for placed components'
); );
const gdsBuilderPy = fs.readFileSync(path.join(backendDir, 'gds_builder.py'), 'utf8');
assert(
gdsBuilderPy.includes('_cells_have_links') && gdsBuilderPy.includes('Routed Build GDS requires mxpic_router'),
'Build GDS should not silently fall back to unrouted gdstk when links are present'
);