File size: 6,005 Bytes
7f9a25d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
"""
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"