""" EM Embedded - Utility Functions Contains shared utilities for grid snapping, coordinate helpers, regex patterns, and logo loading. """ import re import os import base64 import numpy as np __all__ = [ "SAMPLE_PAIR_RE", "nearest_node_index", "snap_samples_to_grid", "nearest_gridline", "load_logo_data_uri", "normalized_position_label", "format_grid_label", ] # Regex pattern for parsing coordinate pairs like "(0.5, 0.5)" SAMPLE_PAIR_RE = re.compile(r"\(\s*([-+]?\d*\.?\d+)\s*,\s*([-+]?\d*\.?\d+)\s*\)") def nearest_node_index(x: float, y: float, nx: int, ny: int = None) -> tuple: """Map normalized [0,1] coordinates to nearest node index on an nx×ny grid.""" if ny is None: ny = nx ix = int(round(x * (nx - 1))) iy = int(round(y * (ny - 1))) ix = max(0, min(nx - 1, ix)) iy = max(0, min(ny - 1, iy)) return ix, iy def nearest_gridline(val: float, nx: int) -> float: """Snap a value to the nearest gridline.""" return round(val * (nx - 1)) / (nx - 1) if nx > 1 else val def snap_samples_to_grid(sample_str: str, nx: int) -> tuple: """ Parse and snap sample points to grid. Returns: tuple: (snapped_gridpoints_str, display_info_str) """ if nx is None or nx < 2: return "", "" matches = SAMPLE_PAIR_RE.findall(sample_str) if not matches: return "", "Enter sample position(s) as (x, y) pairs in [0,1] x [0,1]." snapped = [] info_parts = [] for x_str, y_str in matches: try: x_norm = float(x_str) y_norm = float(y_str) # Clamp to [0, 1] x_norm = max(0.0, min(1.0, x_norm)) y_norm = max(0.0, min(1.0, y_norm)) # Snap to grid ix = int(round(x_norm * (nx - 1))) iy = int(round(y_norm * (nx - 1))) ix = max(0, min(nx - 1, ix)) iy = max(0, min(nx - 1, iy)) snapped.append((ix, iy)) # Compute snapped normalized coords for display x_snapped = ix / (nx - 1) if nx > 1 else 0.0 y_snapped = iy / (nx - 1) if nx > 1 else 0.0 changed = (abs(x_norm - x_snapped) > 1e-9 or abs(y_norm - y_snapped) > 1e-9) descriptor = "adjusted" if changed else "aligned" info_parts.append(f"Input ({x_norm:.3f}, {y_norm:.3f}) {descriptor} to ({x_snapped:.3f}, {y_snapped:.3f}) → ({ix}, {iy})") except (ValueError, ZeroDivisionError): continue gridpoints_str = ", ".join(f"({ix}, {iy})" for ix, iy in snapped) info_str = "\n".join(info_parts) return gridpoints_str, info_str def normalized_position_label(px: int, py: int, gw: int, gh: int) -> str: """Create a normalized position label like 'Position (0.500, 0.500)'.""" px_i, py_i = int(px), int(py) denom_x = float(max(gw - 1, 1)) denom_y = float(max(gh - 1, 1)) x_norm = px_i / denom_x y_norm = py_i / denom_y return f"Position ({x_norm:.3f}, {y_norm:.3f})" def format_grid_label(px: int, py: int, field: str = None, nx: int = None, label_map: dict = None) -> str: """Format a grid position label, optionally using a label map.""" px_i, py_i = int(px), int(py) if label_map and field: label = label_map.get((str(field), px_i, py_i)) if label: return label if label_map: for (fld, gx, gy), label in label_map.items(): if gx == px_i and gy == py_i: return label if nx: denom = float(max(int(nx) - 1, 1)) return f"Position ({px_i / denom:.3f}, {py_i / denom:.3f})" return f"Position ({px_i}, {py_i})" def load_logo_data_uri() -> str: """Load the Synopsys logo as a data URI.""" base_dir = os.path.dirname(os.path.dirname(__file__)) # quantum_embedded folder candidates = [ os.path.join(base_dir, "ansys-part-of-synopsys-logo.svg"), # Also check in parent quantum folder os.path.join(os.path.dirname(base_dir), "quantum", "ansys-part-of-synopsys-logo.svg"), ] for p in candidates: if os.path.exists(p): ext = os.path.splitext(p)[1].lower() if ext == ".svg": mime = "image/svg+xml" elif ext == ".png": mime = "image/png" else: mime = "image/jpeg" try: with open(p, "rb") as f: b64 = base64.b64encode(f.read()).decode("ascii") return f"data:{mime};base64,{b64}" except Exception: continue return None def install_synopsys_plotly_theme(): """Install a Synopsys-aligned Plotly theme.""" import plotly.io as pio import plotly.graph_objects as go base = go.layout.Template(pio.templates["plotly_white"]) base.layout.update( font=dict( family="Inter, Segoe UI, Roboto, Helvetica, Arial, sans-serif", size=13, color="#1A1A1A", ), paper_bgcolor="#FFFFFF", plot_bgcolor="#FFFFFF", colorway=["#5F259F", "#7A3DB5", "#AE8BD8", "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"], hoverlabel=dict(bgcolor="#FFFFFF", bordercolor="#5F259F", font=dict(color="#1A1A1A")), legend=dict(orientation="h", x=1, xanchor="right", y=1.02, yanchor="bottom", title_text=""), margin=dict(l=40, r=20, t=40, b=40), ) base.layout.xaxis.update( showgrid=True, gridcolor="rgba(95,37,159,0.1)", zeroline=False, linecolor="rgba(0,0,0,.2)", ticks="outside", tickformat=".2f", ) base.layout.yaxis.update( showgrid=True, gridcolor="rgba(95,37,159,0.1)", zeroline=True, zerolinecolor="rgba(0,0,0,.25)", linecolor="rgba(0,0,0,.2)", ticks="outside", tickformat=".3g", ) pio.templates["syn_white"] = base pio.templates.default = "syn_white"