|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | + |
| 4 | +""" |
| 5 | +Generate a variable-pitch .OPT3DMapping file from a polynomial surface model. |
| 6 | +
|
| 7 | +If the JSON model does not exist, this script will try to import and run |
| 8 | +`create_polynomial_surface()` from `1_1_create_stl_polynomial_surface.py`. |
| 9 | +
|
| 10 | +Everything can also be executed independently (standalone). |
| 11 | +""" |
| 12 | + |
| 13 | +import json |
| 14 | +import os |
| 15 | +from typing import List |
| 16 | +from typing import Tuple |
| 17 | + |
| 18 | +import numpy as np |
| 19 | + |
| 20 | +# Try importing the fitting script |
| 21 | +try: |
| 22 | + from A_1_Create_stl_polynomial_surface import create_polynomial_surface |
| 23 | + |
| 24 | + print("[INFO] Successfully imported create_polynomial_surface from 1_1_create_stl_polynomial_surface.py") |
| 25 | +except ModuleNotFoundError as e: |
| 26 | + print(f"[ERROR] Could not import Script 1: {e}") |
| 27 | + print("Make sure both scripts are in the same folder or that this folder is in sys.path.") |
| 28 | + raise |
| 29 | + |
| 30 | + |
| 31 | +# ============================== CONFIG ============================== # |
| 32 | + |
| 33 | +MODEL_JSON = ( |
| 34 | + r"C:\Users\amarin\OneDrive - ANSYS, Inc\Articules and Trainings ACE\3D Texture - Light Guide" |
| 35 | + r"\#2. Variable pitch\FittedSurface_Model.json" |
| 36 | +) |
| 37 | +OUTPUT_MAPPING = ( |
| 38 | + r"C:\Users\amarin\OneDrive - ANSYS, Inc\Articules and Trainings ACE\3D Texture - Light Guide" |
| 39 | + r"\#2. Variable pitch\VariablePitch.OPT3DMapping" |
| 40 | +) |
| 41 | +INPUT_MAPPING = ( |
| 42 | + r"C:\Users\amarin\OneDrive - ANSYS, Inc\Articules and Trainings ACE\3D Texture - Light Guide" |
| 43 | + r"\#2. Variable pitch\TL L.3D Texture.2.OPT3DMapping" |
| 44 | +) |
| 45 | +OUTPUT_STL = ( |
| 46 | + r"C:\Users\amarin\OneDrive - ANSYS, Inc\Articules and Trainings ACE\3D Texture - Light Guide" |
| 47 | + r"\#2. Variable pitch\FittedSurface_Global_HighQuality.stl" |
| 48 | +) |
| 49 | + |
| 50 | +PITCH_X_START = 2.0 |
| 51 | +PITCH_X_END = 5.0 |
| 52 | +PITCH_Y = 1.0 |
| 53 | +INCLUDE_EDGES = True |
| 54 | + |
| 55 | +X_MIN = None |
| 56 | +X_MAX = None |
| 57 | +Y_MIN = None |
| 58 | +Y_MAX = None |
| 59 | + |
| 60 | +EXTRA_CONSTANTS = ["1", "0", "0", "0", "1", "0", "0.5", "0.5", "1"] |
| 61 | +FLOAT_FMT = ".6f" |
| 62 | + |
| 63 | +# ==================================================================== # |
| 64 | + |
| 65 | + |
| 66 | +def linear_pitch_x(x: float, x_min: float, x_max: float, p_start: float, p_end: float) -> float: |
| 67 | + """Linear interpolation of pitch along X.""" |
| 68 | + if x_max == x_min: |
| 69 | + return p_start |
| 70 | + t = np.clip((x - x_min) / (x_max - x_min), 0.0, 1.0) |
| 71 | + return p_start * (1.0 - t) + p_end * t |
| 72 | + |
| 73 | + |
| 74 | +def generate_points( |
| 75 | + domain: Tuple[float, float, float, float], p_start: float, p_end: float, p_y: float, include_edges: bool = True |
| 76 | +) -> Tuple[np.ndarray, np.ndarray]: |
| 77 | + """ |
| 78 | + Generate (X, Y) grid with variable X pitch and constant Y pitch. |
| 79 | +
|
| 80 | + Parameters |
| 81 | + ---------- |
| 82 | + domain : tuple |
| 83 | + (x_min, x_max, y_min, y_max) |
| 84 | + p_start, p_end, p_y : float |
| 85 | + Pitch configuration. |
| 86 | + include_edges : bool |
| 87 | + Whether to force the last row/column at the domain edge. |
| 88 | +
|
| 89 | + Returns |
| 90 | + ------- |
| 91 | + X_pts, Y_pts : ndarray |
| 92 | + Flattened arrays of all coordinates. |
| 93 | + """ |
| 94 | + x_min, x_max, y_min, y_max = domain |
| 95 | + eps = 1e-12 |
| 96 | + if p_start <= 0 or p_end <= 0 or p_y <= 0: |
| 97 | + raise ValueError("All pitch values must be positive.") |
| 98 | + |
| 99 | + ys = [] |
| 100 | + y = y_min |
| 101 | + while y <= y_max + eps: |
| 102 | + ys.append(min(y, y_max)) |
| 103 | + y += p_y |
| 104 | + if include_edges and abs(ys[-1] - y_max) > 1e-9: |
| 105 | + ys.append(y_max) |
| 106 | + |
| 107 | + X_list, Y_list = [], [] |
| 108 | + for yy in ys: |
| 109 | + x = x_min |
| 110 | + row = [] |
| 111 | + while x <= x_max + eps: |
| 112 | + row.append(min(x, x_max)) |
| 113 | + x += linear_pitch_x(x, x_min, x_max, p_start, p_end) |
| 114 | + if include_edges and abs(row[-1] - x_max) > 1e-9: |
| 115 | + row.append(x_max) |
| 116 | + X_list.extend(row) |
| 117 | + Y_list.extend([yy] * len(row)) |
| 118 | + return np.asarray(X_list), np.asarray(Y_list) |
| 119 | + |
| 120 | + |
| 121 | +def eval_poly2d(coeffs: np.ndarray, x_norm: np.ndarray, y_norm: np.ndarray, order: int) -> np.ndarray: |
| 122 | + """Evaluate z = f(x, y) using polynomial coefficients.""" |
| 123 | + z = np.zeros_like(x_norm) |
| 124 | + idx = 0 |
| 125 | + for i in range(order + 1): |
| 126 | + for j in range(order + 1 - i): |
| 127 | + z += coeffs[idx] * (x_norm**i) * (y_norm**j) |
| 128 | + idx += 1 |
| 129 | + return z |
| 130 | + |
| 131 | + |
| 132 | +def write_opt3d_mapping( |
| 133 | + path: str, |
| 134 | + X: np.ndarray, |
| 135 | + Y: np.ndarray, |
| 136 | + Z: np.ndarray, |
| 137 | + extra_constants: List[str], |
| 138 | + float_fmt: str = ".6f", |
| 139 | +) -> None: |
| 140 | + """Write the .OPT3DMapping file.""" |
| 141 | + if len(extra_constants) != 9: |
| 142 | + raise ValueError("Exactly 9 extra constants are required.") |
| 143 | + n = len(X) |
| 144 | + os.makedirs(os.path.dirname(path), exist_ok=True) |
| 145 | + ffmt = f"{{:{float_fmt}}}" # noqa: E231 |
| 146 | + with open(path, "w", encoding="utf-8") as f: |
| 147 | + f.write(f"{n}\n") |
| 148 | + for x, y, z in zip(X, Y, Z): |
| 149 | + formatted_values = " ".join([ffmt.format(x), ffmt.format(y), ffmt.format(z)] + extra_constants) |
| 150 | + f.write(formatted_values + "\n") |
| 151 | + |
| 152 | + |
| 153 | +def ensure_model_json(): |
| 154 | + """Ensure the polynomial model JSON exists; generate it if missing.""" |
| 155 | + if os.path.isfile(MODEL_JSON): |
| 156 | + return |
| 157 | + print("[INFO] JSON not found. Running Script 1 via direct import...") |
| 158 | + create_polynomial_surface(INPUT_MAPPING, OUTPUT_STL, MODEL_JSON) |
| 159 | + if not os.path.isfile(MODEL_JSON): |
| 160 | + raise FileNotFoundError(f"Model JSON not generated: {MODEL_JSON}") |
| 161 | + |
| 162 | + |
| 163 | +def main(): |
| 164 | + """Main entry point.""" |
| 165 | + ensure_model_json() |
| 166 | + |
| 167 | + with open(MODEL_JSON, "r", encoding="utf-8") as f: |
| 168 | + model = json.load(f) |
| 169 | + |
| 170 | + order = int(model["order"]) |
| 171 | + coeffs = np.asarray(model["coeffs"], float) |
| 172 | + x_mean, x_std = float(model["x_mean"]), float(model["x_std"]) |
| 173 | + y_mean, y_std = float(model["y_mean"]), float(model["y_std"]) |
| 174 | + |
| 175 | + x_min = X_MIN or float(model["x_min"]) |
| 176 | + x_max = X_MAX or float(model["x_max"]) |
| 177 | + y_min = Y_MIN or float(model["y_min"]) |
| 178 | + y_max = Y_MAX or float(model["y_max"]) |
| 179 | + |
| 180 | + X_pts, Y_pts = generate_points((x_min, x_max, y_min, y_max), PITCH_X_START, PITCH_X_END, PITCH_Y, INCLUDE_EDGES) |
| 181 | + x_norm = (X_pts - x_mean) / x_std |
| 182 | + y_norm = (Y_pts - y_mean) / y_std |
| 183 | + Z_pts = eval_poly2d(coeffs, x_norm, y_norm, order) |
| 184 | + write_opt3d_mapping(OUTPUT_MAPPING, X_pts, Y_pts, Z_pts, EXTRA_CONSTANTS, FLOAT_FMT) |
| 185 | + |
| 186 | + print(f"[OK] Mapping created: {OUTPUT_MAPPING}") |
| 187 | + print(f"[INFO] Total points: {len(X_pts)}") |
| 188 | + print(f"[INFO] Domain X[{x_min}, {x_max}] Y[{y_min}, {y_max}]") |
| 189 | + print(f"[INFO] Pitch X: {PITCH_X_START} → {PITCH_X_END} | Pitch Y: {PITCH_Y}") |
| 190 | + |
| 191 | + |
| 192 | +if __name__ == "__main__": |
| 193 | + main() |
0 commit comments