@tscircuit/cli 0.1.2287 → 0.1.2288
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/main.js +3950 -7
- package/dist/cli/python/flux.py +99 -0
- package/dist/cli/python/mesh.py +485 -0
- package/dist/cli/python/mesh_multilayer.py +376 -0
- package/dist/cli/python/requirements.txt +4 -0
- package/dist/cli/python/sample.py +352 -0
- package/dist/cli/python/surface_sample.py +88 -0
- package/dist/lib/index.js +10 -6
- package/package.json +11 -7
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Read Palace phasors, normalize port basis excitations, and sample ground only."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import csv
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import re
|
|
9
|
+
import xml.etree.ElementTree as ET
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import vtk
|
|
14
|
+
from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy
|
|
15
|
+
from surface_sample import sample_surface
|
|
16
|
+
|
|
17
|
+
# Keep VTK sampling deterministic and avoid oversubscribing FEM jobs.
|
|
18
|
+
vtk.vtkSMPTools.Initialize(1)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def digest(path):
|
|
22
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_ports(path):
|
|
26
|
+
with path.open() as stream:
|
|
27
|
+
rows = list(csv.DictReader(stream, skipinitialspace=True))
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
key.strip(): (float(number) if number.strip() != "NULL" else None)
|
|
31
|
+
for key, number in row.items()
|
|
32
|
+
}
|
|
33
|
+
for row in rows
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_ground(path):
|
|
38
|
+
# Read piece files directly. vtkXMLPUnstructuredGridReader in VTK 9.3 can
|
|
39
|
+
# expose invalid shared arrays to extract filters for Palace's output.
|
|
40
|
+
pieces = [
|
|
41
|
+
path.parent / piece.attrib["Source"] for piece in ET.parse(path).iter("Piece")
|
|
42
|
+
]
|
|
43
|
+
readers = []
|
|
44
|
+
append = vtk.vtkAppendFilter()
|
|
45
|
+
for piece in pieces:
|
|
46
|
+
reader = vtk.vtkXMLUnstructuredGridReader()
|
|
47
|
+
reader.SetFileName(str(piece))
|
|
48
|
+
reader.Update()
|
|
49
|
+
if reader.GetOutput().GetNumberOfCells() == 0:
|
|
50
|
+
raise ValueError(f"Empty Palace volume piece: {piece}")
|
|
51
|
+
readers.append(reader)
|
|
52
|
+
append.AddInputData(reader.GetOutput())
|
|
53
|
+
if not readers:
|
|
54
|
+
raise ValueError("Palace data.pvtu contains no piece files")
|
|
55
|
+
if len(readers) == 1:
|
|
56
|
+
mesh = readers[0].GetOutput()
|
|
57
|
+
else:
|
|
58
|
+
append.Update()
|
|
59
|
+
mesh = append.GetOutput()
|
|
60
|
+
attributes = vtk_to_numpy(mesh.GetCellData().GetArray("attribute"))
|
|
61
|
+
copper_cells = np.flatnonzero(attributes == 3)
|
|
62
|
+
ground_cells = vtk.vtkIdList()
|
|
63
|
+
for index in copper_cells:
|
|
64
|
+
# Probe coordinates, not vertex heights, select bottom foil. A plated
|
|
65
|
+
# barrel can share valid copper tetrahedra across z=0; excluding those
|
|
66
|
+
# loses real bottom-plane samples around a ground via.
|
|
67
|
+
ground_cells.InsertNextId(int(index))
|
|
68
|
+
if ground_cells.GetNumberOfIds() == 0:
|
|
69
|
+
raise ValueError("Palace output has no bottom copper volume cells")
|
|
70
|
+
extract = vtk.vtkExtractCells()
|
|
71
|
+
extract.SetInputData(mesh)
|
|
72
|
+
extract.SetCellList(ground_cells)
|
|
73
|
+
extract.Update()
|
|
74
|
+
return extract.GetOutput()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def probe_ground(ground, options):
|
|
78
|
+
coordinates = options["coordinates"]
|
|
79
|
+
model = options["model"]
|
|
80
|
+
electric_field_scale = options["electricFieldScale"]
|
|
81
|
+
locations = vtk.vtkPoints()
|
|
82
|
+
locations.SetData(numpy_to_vtk(coordinates, deep=True))
|
|
83
|
+
mesh = vtk.vtkPolyData()
|
|
84
|
+
mesh.SetPoints(locations)
|
|
85
|
+
probe = vtk.vtkProbeFilter()
|
|
86
|
+
probe.SetInputData(mesh)
|
|
87
|
+
probe.SetSourceData(ground)
|
|
88
|
+
probe.SetCellLocatorPrototype(vtk.vtkStaticCellLocator())
|
|
89
|
+
probe.SetComputeTolerance(False)
|
|
90
|
+
probe.SetTolerance(1e-5)
|
|
91
|
+
probe.Update()
|
|
92
|
+
sampled = probe.GetOutput().GetPointData()
|
|
93
|
+
valid = vtk_to_numpy(sampled.GetArray("vtkValidPointMask")).astype(bool)
|
|
94
|
+
if not valid.all():
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"{np.count_nonzero(~valid)} sample locations do not lie on Palace ground triangles"
|
|
97
|
+
)
|
|
98
|
+
real = vtk_to_numpy(sampled.GetArray("E_real"))
|
|
99
|
+
imag = vtk_to_numpy(sampled.GetArray("E_imag"))
|
|
100
|
+
if not np.isfinite(real).all() or not np.isfinite(imag).all():
|
|
101
|
+
raise ValueError("Non-finite Palace copper electric field")
|
|
102
|
+
# Ohmic J = sigma E, A/m² → A/mm². Integrate copper depth for K.
|
|
103
|
+
current = (
|
|
104
|
+
(real[:, :2] + 1j * imag[:, :2])
|
|
105
|
+
* electric_field_scale
|
|
106
|
+
* model["copperConductivity"]
|
|
107
|
+
/ 1e6
|
|
108
|
+
)
|
|
109
|
+
current = current.reshape(5, -1, 2)
|
|
110
|
+
_, weights = np.polynomial.legendre.leggauss(5)
|
|
111
|
+
return np.einsum("d,dnc->nc", weights / 2, current) * model["copperThickness"]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def coordinate(number):
|
|
115
|
+
return f"{0 if number == 0 else number:.9f}"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def point(position):
|
|
119
|
+
return [coordinate(position["x"]), coordinate(position["y"])]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def geometry_signature(geometry):
|
|
123
|
+
signature = [
|
|
124
|
+
[point(position) for position in geometry["boardOutline"]],
|
|
125
|
+
[
|
|
126
|
+
[
|
|
127
|
+
[point(position) for position in region["outer"]],
|
|
128
|
+
[[point(position) for position in hole] for hole in [*region["holes"], *region.get("maskCutouts", [])]],
|
|
129
|
+
]
|
|
130
|
+
for region in geometry["groundRegions"]
|
|
131
|
+
],
|
|
132
|
+
[[point(position) for position in outline] for outline in geometry["cutouts"]],
|
|
133
|
+
[
|
|
134
|
+
[
|
|
135
|
+
[*point(route), coordinate(route["width"]), route["layer"]]
|
|
136
|
+
for route in signal["route"]
|
|
137
|
+
if route["route_type"] == "wire"
|
|
138
|
+
]
|
|
139
|
+
for signal in geometry["signals"]
|
|
140
|
+
],
|
|
141
|
+
*([geometry["physicalModelSignature"]] if geometry.get("physicalModelSignature") else []),
|
|
142
|
+
[
|
|
143
|
+
[
|
|
144
|
+
coordinate(excitation["current"]),
|
|
145
|
+
point(excitation["return_source"]),
|
|
146
|
+
point(excitation["return_sink"]),
|
|
147
|
+
*([[excitation.get("source_port", {}).get("reference_layer", "bottom"), coordinate(excitation.get("source_port", {}).get("resistance", 50)), excitation.get("load_port", {}).get("reference_layer", "bottom"), coordinate(excitation.get("load_port", {}).get("resistance", 50))]] if excitation.get("source_port") or excitation.get("load_port") else []),
|
|
148
|
+
]
|
|
149
|
+
for excitation in geometry["excitations"]
|
|
150
|
+
],
|
|
151
|
+
]
|
|
152
|
+
return json.dumps(signature, separators=(",", ":"))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def serialize_currents(currents):
|
|
156
|
+
return [
|
|
157
|
+
{"real": float(current.real), "imag": float(current.imag)}
|
|
158
|
+
for current in currents
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def sample_case(case):
|
|
163
|
+
model = json.loads((case / "model.json").read_text())
|
|
164
|
+
grid = json.loads((case / "sample-grid.json").read_text())
|
|
165
|
+
config = json.loads((case / "palace.json").read_text())
|
|
166
|
+
log = (case / "palace.log").read_text()
|
|
167
|
+
version = re.search(r"Git changeset ID:\s*(\S+)", log)
|
|
168
|
+
if not version or version[1] != "v0.14.0":
|
|
169
|
+
raise ValueError(
|
|
170
|
+
"This importer requires Palace v0.14.0, whose ParaView fields are nondimensional; other versions require an audited units adapter"
|
|
171
|
+
)
|
|
172
|
+
if not re.search(r"^Total\s+[0-9.]+\s+[0-9.]+\s+[0-9.]+", log, re.MULTILINE):
|
|
173
|
+
raise ValueError("Only completed Palace runs can be imported")
|
|
174
|
+
if re.search(r"solver.*(?:did not|failed to).*converge", log, re.IGNORECASE):
|
|
175
|
+
raise ValueError("Palace linear solver failed to converge")
|
|
176
|
+
postpro = case / config["Problem"]["Output"]
|
|
177
|
+
rows = read_ports(postpro / "port-I.csv")
|
|
178
|
+
excitations = model["geometry"]["excitations"]
|
|
179
|
+
count = len(excitations)
|
|
180
|
+
if len(rows) != 1:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
"Expected exactly one frequency row containing all port basis solutions"
|
|
183
|
+
)
|
|
184
|
+
# Palace reports V/R, the current in each port's termination, not the net
|
|
185
|
+
# current injected into the PCB. A driven port also has a Norton source
|
|
186
|
+
# of 2*I_inc. Thus I_into_PCB = 2*I_inc - I_termination at active ports.
|
|
187
|
+
source_matrix = np.zeros((count, count), dtype=complex)
|
|
188
|
+
load_matrix = np.zeros_like(source_matrix)
|
|
189
|
+
fields = []
|
|
190
|
+
surface_receipts = []
|
|
191
|
+
outline = model["geometry"]["boardOutline"]
|
|
192
|
+
width = (
|
|
193
|
+
max(p["x"] for p in outline)
|
|
194
|
+
- min(p["x"] for p in outline)
|
|
195
|
+
+ 2 * model["airPadding"]
|
|
196
|
+
)
|
|
197
|
+
height = (
|
|
198
|
+
max(p["y"] for p in outline)
|
|
199
|
+
- min(p["y"] for p in outline)
|
|
200
|
+
+ 2 * model["airPadding"]
|
|
201
|
+
)
|
|
202
|
+
depth = model["layerSeparation"] + 2 * model["airPadding"]
|
|
203
|
+
characteristic_length_m = (
|
|
204
|
+
config["Model"].get("Lc", max(width, height, depth)) * config["Model"]["L0"]
|
|
205
|
+
)
|
|
206
|
+
impedance_free_space = math.sqrt((4e-7 * math.pi) / 8.8541878176e-12)
|
|
207
|
+
electric_field_scale = impedance_free_space**0.5 / characteristic_length_m
|
|
208
|
+
surface_impedance = model["copperModel"] == "surface_impedance_copper"
|
|
209
|
+
surface_current_scale = 1 / (impedance_free_space**0.5 * characteristic_length_m * 1000)
|
|
210
|
+
coordinates = np.array([[point["x"], point["y"]] for point in grid["points"]])
|
|
211
|
+
depth_nodes, _ = np.polynomial.legendre.leggauss(5)
|
|
212
|
+
depths = (depth_nodes - 1) / 2 * model["copperThickness"]
|
|
213
|
+
if model.get("multilayer"):
|
|
214
|
+
foil = next(layer for layer in model["multilayer"]["stackup"]["copperLayers"]
|
|
215
|
+
if layer["name"] == model["multilayer"]["sampleLayer"])
|
|
216
|
+
depths = foil["zMin"] + (depth_nodes + 1) / 2 * (foil["zMax"] - foil["zMin"])
|
|
217
|
+
volume_coordinates = np.concatenate(
|
|
218
|
+
[
|
|
219
|
+
np.column_stack([coordinates, np.full(len(coordinates), depth)])
|
|
220
|
+
for depth in depths
|
|
221
|
+
]
|
|
222
|
+
)
|
|
223
|
+
for basis in range(count):
|
|
224
|
+
row = rows[0]
|
|
225
|
+
excitation_index = 2 * basis + 1
|
|
226
|
+
if (
|
|
227
|
+
abs(row["f (GHz)"] * 1e9 - model["frequencyHz"])
|
|
228
|
+
> model["frequencyHz"] * 1e-8
|
|
229
|
+
):
|
|
230
|
+
raise ValueError("Palace output frequency does not match the model")
|
|
231
|
+
for index in range(count):
|
|
232
|
+
source_index, load_index = 2 * index + 1, 2 * index + 2
|
|
233
|
+
source_label = (
|
|
234
|
+
f"[{source_index}][{excitation_index}]"
|
|
235
|
+
if count > 1
|
|
236
|
+
else f"[{source_index}]"
|
|
237
|
+
)
|
|
238
|
+
load_label = (
|
|
239
|
+
f"[{load_index}][{excitation_index}]"
|
|
240
|
+
if count > 1
|
|
241
|
+
else f"[{load_index}]"
|
|
242
|
+
)
|
|
243
|
+
incident_label = (
|
|
244
|
+
f"[{source_index}][{excitation_index}]"
|
|
245
|
+
if count > 1
|
|
246
|
+
else f"[{source_index}]"
|
|
247
|
+
)
|
|
248
|
+
termination = complex(
|
|
249
|
+
row[f"Re{{I{source_label}}} (A)"], row[f"Im{{I{source_label}}} (A)"]
|
|
250
|
+
)
|
|
251
|
+
incident = (
|
|
252
|
+
row.get(f"I_inc{incident_label} (A)", 0.0) if index == basis else 0.0
|
|
253
|
+
)
|
|
254
|
+
source_matrix[index, basis] = 2 * incident - termination
|
|
255
|
+
load_matrix[index, basis] = complex(
|
|
256
|
+
row[f"Re{{I{load_label}}} (A)"], row[f"Im{{I{load_label}}} (A)"]
|
|
257
|
+
)
|
|
258
|
+
boundary = postpro / "paraview" / ("driven_boundary" if surface_impedance else "driven")
|
|
259
|
+
if count > 1:
|
|
260
|
+
boundary = boundary / f"excitation_{excitation_index}"
|
|
261
|
+
path = boundary / "Cycle000001" / "data.pvtu"
|
|
262
|
+
fields.append(sample_surface(path, coordinates, model, surface_current_scale, surface_receipts) if surface_impedance else
|
|
263
|
+
probe_ground(
|
|
264
|
+
read_ground(path),
|
|
265
|
+
{
|
|
266
|
+
"coordinates": volume_coordinates,
|
|
267
|
+
"model": model,
|
|
268
|
+
"electricFieldScale": electric_field_scale,
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
condition = float(np.linalg.cond(source_matrix))
|
|
273
|
+
if not np.isfinite(condition) or condition > 1e8:
|
|
274
|
+
raise ValueError("Source-current normalization is singular or ill-conditioned")
|
|
275
|
+
desired = np.array(
|
|
276
|
+
[excitation["current"] for excitation in excitations], dtype=complex
|
|
277
|
+
)
|
|
278
|
+
coefficients = np.linalg.solve(source_matrix, desired)
|
|
279
|
+
combined = np.einsum("b,bnc->nc", coefficients, np.stack(fields))
|
|
280
|
+
normalized_sources = source_matrix @ coefficients
|
|
281
|
+
normalized_loads = load_matrix @ coefficients
|
|
282
|
+
if not np.allclose(normalized_sources, desired, atol=1e-10, rtol=1e-10):
|
|
283
|
+
raise ValueError("Source-current normalization failed")
|
|
284
|
+
samples = [
|
|
285
|
+
{
|
|
286
|
+
"x": float(x),
|
|
287
|
+
"y": float(y),
|
|
288
|
+
"sheetCurrentXReal": float(current[0].real),
|
|
289
|
+
"sheetCurrentYReal": float(current[1].real),
|
|
290
|
+
"sheetCurrentXImag": float(current[0].imag),
|
|
291
|
+
"sheetCurrentYImag": float(current[1].imag),
|
|
292
|
+
}
|
|
293
|
+
for (x, y), current in zip(coordinates, combined)
|
|
294
|
+
]
|
|
295
|
+
reference = {
|
|
296
|
+
"schemaVersion": 1,
|
|
297
|
+
**({"sampleLayer": model["multilayer"]["sampleLayer"]} if model.get("multilayer") else {}),
|
|
298
|
+
"solver": "palace",
|
|
299
|
+
"solverVersion": version[1],
|
|
300
|
+
"femOrder": model["order"],
|
|
301
|
+
"frequencyHz": model["frequencyHz"],
|
|
302
|
+
"copperModel": model["copperModel"],
|
|
303
|
+
**({"samplingMethod": "sum_foil_face_surface_currents", "surfaceCurrentScaleAmpsPerMm": surface_current_scale} if surface_impedance else {}),
|
|
304
|
+
"copperThickness": model["copperThickness"],
|
|
305
|
+
"layerSeparation": model["layerSeparation"],
|
|
306
|
+
**{key: grid[key] for key in ["cellWidth", "cellHeight", "columns", "rows"]},
|
|
307
|
+
"samples": samples,
|
|
308
|
+
"sourceCurrents": serialize_currents(normalized_sources),
|
|
309
|
+
"loadCurrents": serialize_currents(normalized_loads),
|
|
310
|
+
"normalizationConditionNumber": condition,
|
|
311
|
+
"electricFieldScaleVoltsPerMeter": electric_field_scale,
|
|
312
|
+
"provenance": {
|
|
313
|
+
"circuitSha256": digest(case / "circuit.json"),
|
|
314
|
+
"geometrySignature": geometry_signature(model["geometry"]),
|
|
315
|
+
"modelSha256": digest(case / "model.json"),
|
|
316
|
+
"meshSha256": digest(case / "mesh.msh"),
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
(case / "reference.json").write_text(
|
|
320
|
+
json.dumps(reference, separators=(",", ":"), allow_nan=False) + "\n"
|
|
321
|
+
)
|
|
322
|
+
(case / "normalization.json").write_text(
|
|
323
|
+
json.dumps(
|
|
324
|
+
{
|
|
325
|
+
"basisCoefficients": serialize_currents(coefficients),
|
|
326
|
+
"sourceCurrents": reference["sourceCurrents"],
|
|
327
|
+
"loadCurrents": reference["loadCurrents"],
|
|
328
|
+
"conditionNumber": condition,
|
|
329
|
+
},
|
|
330
|
+
indent=2,
|
|
331
|
+
)
|
|
332
|
+
+ "\n"
|
|
333
|
+
)
|
|
334
|
+
if surface_impedance:
|
|
335
|
+
(case / "surface-sampling.json").write_text(json.dumps({
|
|
336
|
+
"copperModel": model["copperModel"],
|
|
337
|
+
"method": "sum_foil_face_surface_currents",
|
|
338
|
+
"normalConvention": "metal-outward normal crossed with H; no additional face sign flip",
|
|
339
|
+
"units": "A/mm, complex peak phasors after source-current basis normalization",
|
|
340
|
+
"bases": surface_receipts,
|
|
341
|
+
}, indent=2, allow_nan=False) + "\n")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def main():
|
|
345
|
+
parser = argparse.ArgumentParser()
|
|
346
|
+
parser.add_argument("case")
|
|
347
|
+
args = parser.parse_args()
|
|
348
|
+
sample_case(Path(args.case).resolve())
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
if __name__ == "__main__":
|
|
352
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Import Palace's actual finite-conductivity boundary current coefficients."""
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
import numpy as np
|
|
4
|
+
import vtk
|
|
5
|
+
from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def read_boundary(path):
|
|
9
|
+
pieces = [path.parent / piece.attrib["Source"] for piece in ET.parse(path).iter("Piece")]
|
|
10
|
+
readers, append = [], vtk.vtkAppendFilter()
|
|
11
|
+
append.MergePointsOff()
|
|
12
|
+
for piece in pieces:
|
|
13
|
+
reader = vtk.vtkXMLUnstructuredGridReader()
|
|
14
|
+
reader.SetFileName(str(piece))
|
|
15
|
+
reader.Update()
|
|
16
|
+
readers.append(reader)
|
|
17
|
+
append.AddInputData(reader.GetOutput())
|
|
18
|
+
if not readers:
|
|
19
|
+
raise ValueError("No Palace boundary pieces")
|
|
20
|
+
append.Update()
|
|
21
|
+
return append.GetOutput()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def probe_face(boundary, coordinates, z, receipt=None):
|
|
25
|
+
attribute = boundary.GetCellData().GetArray("attribute")
|
|
26
|
+
if attribute is None:
|
|
27
|
+
raise ValueError("Palace boundary has no material-face attributes")
|
|
28
|
+
ids = vtk.vtkIdList()
|
|
29
|
+
attributes = vtk_to_numpy(attribute)
|
|
30
|
+
face_heights = []
|
|
31
|
+
for index in np.flatnonzero(attributes == 11):
|
|
32
|
+
cell = boundary.GetCell(int(index))
|
|
33
|
+
if all(abs(cell.GetPoints().GetPoint(point)[2] - z) < 1e-7 for point in range(cell.GetNumberOfPoints())):
|
|
34
|
+
ids.InsertNextId(int(index))
|
|
35
|
+
face_heights.extend(cell.GetPoints().GetPoint(point)[2] for point in range(cell.GetNumberOfPoints()))
|
|
36
|
+
if ids.GetNumberOfIds() == 0:
|
|
37
|
+
if receipt is not None:
|
|
38
|
+
receipt.update({"physicalZMm": z, "triangles": 0, "validSamples": 0})
|
|
39
|
+
return np.zeros((len(coordinates), 2), dtype=complex), np.zeros(len(coordinates), dtype=bool)
|
|
40
|
+
extract = vtk.vtkExtractCells()
|
|
41
|
+
extract.SetInputData(boundary)
|
|
42
|
+
extract.SetCellList(ids)
|
|
43
|
+
extract.Update()
|
|
44
|
+
# Palace writes float32 VTK coordinates. For a flat triangle locator its
|
|
45
|
+
# bounding box can reject the exact physical double-precision Z before the
|
|
46
|
+
# probe tolerance is applied (e.g. -0.035 versus -0.035000000149). Probe the
|
|
47
|
+
# verified exported face plane, without moving XY or filling absent faces.
|
|
48
|
+
probe_z = float(np.mean(face_heights))
|
|
49
|
+
if max(face_heights) - min(face_heights) > 1e-10:
|
|
50
|
+
raise ValueError("Selected Palace foil face is not planar")
|
|
51
|
+
locations = vtk.vtkPoints()
|
|
52
|
+
locations.SetData(numpy_to_vtk(np.column_stack([coordinates, np.full(len(coordinates), probe_z)]), deep=True))
|
|
53
|
+
points = vtk.vtkPolyData()
|
|
54
|
+
points.SetPoints(locations)
|
|
55
|
+
probe = vtk.vtkProbeFilter()
|
|
56
|
+
probe.SetInputData(points)
|
|
57
|
+
probe.SetSourceData(extract.GetOutput())
|
|
58
|
+
probe.SetCellLocatorPrototype(vtk.vtkStaticCellLocator())
|
|
59
|
+
probe.SetComputeTolerance(False)
|
|
60
|
+
probe.SetTolerance(1e-5)
|
|
61
|
+
probe.Update()
|
|
62
|
+
data = probe.GetOutput().GetPointData()
|
|
63
|
+
valid = vtk_to_numpy(data.GetArray("vtkValidPointMask")).astype(bool)
|
|
64
|
+
real, imag = data.GetArray("J_s_real"), data.GetArray("J_s_imag")
|
|
65
|
+
if real is None or imag is None:
|
|
66
|
+
raise ValueError("Palace did not export J_s_real/J_s_imag finite-conductivity boundary currents")
|
|
67
|
+
current = vtk_to_numpy(real)[:, :2] + 1j * vtk_to_numpy(imag)[:, :2]
|
|
68
|
+
if not np.isfinite(current[valid]).all():
|
|
69
|
+
raise ValueError("Non-finite Palace surface current")
|
|
70
|
+
current[~valid] = 0
|
|
71
|
+
if receipt is not None:
|
|
72
|
+
receipt.update({"physicalZMm": z, "exportedZMm": probe_z, "triangles": ids.GetNumberOfIds(), "validSamples": int(np.count_nonzero(valid))})
|
|
73
|
+
return current, valid
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def sample_surface(path, coordinates, model, scale_amps_per_mm, receipts=None):
|
|
77
|
+
boundary = read_boundary(path)
|
|
78
|
+
top_receipt, bottom_receipt = {}, {}
|
|
79
|
+
top, top_valid = probe_face(boundary, coordinates, 0, top_receipt)
|
|
80
|
+
bottom, bottom_valid = probe_face(boundary, coordinates, -model["copperThickness"], bottom_receipt)
|
|
81
|
+
if not (top_valid | bottom_valid).all():
|
|
82
|
+
raise ValueError(f"{np.count_nonzero(~(top_valid | bottom_valid))} declared conductor samples have no exposed foil face")
|
|
83
|
+
if receipts is not None:
|
|
84
|
+
receipts.append({"attribute": 11, "fieldArrays": ["J_s_real", "J_s_imag"], "scaleAmpsPerMm": scale_amps_per_mm, "declaredConductorSamples": len(coordinates), "bothFaces": int(np.count_nonzero(top_valid & bottom_valid)), "oneFace": int(np.count_nonzero(top_valid ^ bottom_valid)), "faces": [top_receipt, bottom_receipt]})
|
|
85
|
+
# J_s is n_out_of_metal × H, already signed on each exterior face. Sum the
|
|
86
|
+
# two physical currents; no extra normal/sign flip. A buried via junction
|
|
87
|
+
# may expose only one face and contributes only that physical face.
|
|
88
|
+
return (top + bottom) * scale_amps_per_mm
|
package/dist/lib/index.js
CHANGED
|
@@ -69139,7 +69139,7 @@ var package_default = {
|
|
|
69139
69139
|
"@tscircuit/krt-wasm": "^0.1.6",
|
|
69140
69140
|
"@tscircuit/math-utils": "0.0.36",
|
|
69141
69141
|
"@tscircuit/modelprinter": "^0.0.11",
|
|
69142
|
-
"@tscircuit/props": "^0.0.
|
|
69142
|
+
"@tscircuit/props": "^0.0.698",
|
|
69143
69143
|
"@tscircuit/runframe": "^0.0.2953",
|
|
69144
69144
|
"@tscircuit/schematic-match-adapt": "^0.0.22",
|
|
69145
69145
|
"@tscircuit/schematic-trace-solver": "^0.0.204",
|
|
@@ -69155,7 +69155,7 @@ var package_default = {
|
|
|
69155
69155
|
"@types/semver": "^7.5.8",
|
|
69156
69156
|
"bun-match-svg": "^0.0.12",
|
|
69157
69157
|
chokidar: "4.0.1",
|
|
69158
|
-
"circuit-json": "^0.0.
|
|
69158
|
+
"circuit-json": "^0.0.525",
|
|
69159
69159
|
"circuit-json-to-3d-png": "^0.0.6",
|
|
69160
69160
|
"circuit-json-to-altium": "github:tscircuit/circuit-json-to-altium#1a50d0dd2c0737cf940d92c0315852dc0111dd87",
|
|
69161
69161
|
"circuit-json-to-bom-csv": "^0.0.19",
|
|
@@ -69171,7 +69171,7 @@ var package_default = {
|
|
|
69171
69171
|
"circuit-json-to-tscircuit": "^0.0.50",
|
|
69172
69172
|
"circuit-json-trace-length-analysis": "github:tscircuit/circuit-json-trace-length-analysis#2b44792a40df0ca83b6bfb6ac95ed5e35e7168b8",
|
|
69173
69173
|
"circuit-to-canvas": "^0.0.123",
|
|
69174
|
-
"circuit-to-svg": "^0.0.
|
|
69174
|
+
"circuit-to-svg": "^0.0.447",
|
|
69175
69175
|
commander: "^14.0.0",
|
|
69176
69176
|
conf: "^13.1.0",
|
|
69177
69177
|
configstore: "^7.0.0",
|
|
@@ -69203,6 +69203,7 @@ var package_default = {
|
|
|
69203
69203
|
redaxios: "^0.5.1",
|
|
69204
69204
|
"schematic-symbols": "^0.0.246",
|
|
69205
69205
|
semver: "^7.6.3",
|
|
69206
|
+
"simulate-return-current": "https://pkg.pr.new/tscircuit/simulate-return-current@6a7c84cb4f9f66c2cb54199aee50d3c7453748e8",
|
|
69206
69207
|
spicets: "^0.0.4",
|
|
69207
69208
|
stepts: "^0.0.3",
|
|
69208
69209
|
tempy: "^3.1.0",
|
|
@@ -69212,7 +69213,9 @@ var package_default = {
|
|
|
69212
69213
|
zod: "^3.23.8"
|
|
69213
69214
|
},
|
|
69214
69215
|
peerDependencies: {
|
|
69215
|
-
"
|
|
69216
|
+
"@tscircuit/math-utils": "0.0.36",
|
|
69217
|
+
"circuit-json": "^0.0.525",
|
|
69218
|
+
"circuit-to-svg": "^0.0.447",
|
|
69216
69219
|
tscircuit: "*"
|
|
69217
69220
|
},
|
|
69218
69221
|
bin: {
|
|
@@ -69229,10 +69232,11 @@ var package_default = {
|
|
|
69229
69232
|
}
|
|
69230
69233
|
},
|
|
69231
69234
|
overrides: {
|
|
69232
|
-
"circuit-json": "^0.0.
|
|
69235
|
+
"circuit-json": "^0.0.525",
|
|
69233
69236
|
altiumts: "0.0.59",
|
|
69234
69237
|
poppygl: "0.0.34",
|
|
69235
|
-
"@tscircuit/core": "^0.0.
|
|
69238
|
+
"@tscircuit/core": "^0.0.2121",
|
|
69239
|
+
"jscad-planner": "0.0.18"
|
|
69236
69240
|
},
|
|
69237
69241
|
scripts: {
|
|
69238
69242
|
start: "bun run dev",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2288",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/tscircuit/cli"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@tscircuit/krt-wasm": "^0.1.6",
|
|
28
28
|
"@tscircuit/math-utils": "0.0.36",
|
|
29
29
|
"@tscircuit/modelprinter": "^0.0.11",
|
|
30
|
-
"@tscircuit/props": "^0.0.
|
|
30
|
+
"@tscircuit/props": "^0.0.698",
|
|
31
31
|
"@tscircuit/runframe": "^0.0.2953",
|
|
32
32
|
"@tscircuit/schematic-match-adapt": "^0.0.22",
|
|
33
33
|
"@tscircuit/schematic-trace-solver": "^0.0.204",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@types/semver": "^7.5.8",
|
|
44
44
|
"bun-match-svg": "^0.0.12",
|
|
45
45
|
"chokidar": "4.0.1",
|
|
46
|
-
"circuit-json": "^0.0.
|
|
46
|
+
"circuit-json": "^0.0.525",
|
|
47
47
|
"circuit-json-to-3d-png": "^0.0.6",
|
|
48
48
|
"circuit-json-to-altium": "github:tscircuit/circuit-json-to-altium#1a50d0dd2c0737cf940d92c0315852dc0111dd87",
|
|
49
49
|
"circuit-json-to-bom-csv": "^0.0.19",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"circuit-json-to-tscircuit": "^0.0.50",
|
|
60
60
|
"circuit-json-trace-length-analysis": "github:tscircuit/circuit-json-trace-length-analysis#2b44792a40df0ca83b6bfb6ac95ed5e35e7168b8",
|
|
61
61
|
"circuit-to-canvas": "^0.0.123",
|
|
62
|
-
"circuit-to-svg": "^0.0.
|
|
62
|
+
"circuit-to-svg": "^0.0.447",
|
|
63
63
|
"commander": "^14.0.0",
|
|
64
64
|
"conf": "^13.1.0",
|
|
65
65
|
"configstore": "^7.0.0",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"redaxios": "^0.5.1",
|
|
92
92
|
"schematic-symbols": "^0.0.246",
|
|
93
93
|
"semver": "^7.6.3",
|
|
94
|
+
"simulate-return-current": "https://pkg.pr.new/tscircuit/simulate-return-current@6a7c84cb4f9f66c2cb54199aee50d3c7453748e8",
|
|
94
95
|
"spicets": "^0.0.4",
|
|
95
96
|
"stepts": "^0.0.3",
|
|
96
97
|
"tempy": "^3.1.0",
|
|
@@ -100,7 +101,9 @@
|
|
|
100
101
|
"zod": "^3.23.8"
|
|
101
102
|
},
|
|
102
103
|
"peerDependencies": {
|
|
103
|
-
"
|
|
104
|
+
"@tscircuit/math-utils": "0.0.36",
|
|
105
|
+
"circuit-json": "^0.0.525",
|
|
106
|
+
"circuit-to-svg": "^0.0.447",
|
|
104
107
|
"tscircuit": "*"
|
|
105
108
|
},
|
|
106
109
|
"bin": {
|
|
@@ -117,10 +120,11 @@
|
|
|
117
120
|
}
|
|
118
121
|
},
|
|
119
122
|
"overrides": {
|
|
120
|
-
"circuit-json": "^0.0.
|
|
123
|
+
"circuit-json": "^0.0.525",
|
|
121
124
|
"altiumts": "0.0.59",
|
|
122
125
|
"poppygl": "0.0.34",
|
|
123
|
-
"@tscircuit/core": "^0.0.
|
|
126
|
+
"@tscircuit/core": "^0.0.2121",
|
|
127
|
+
"jscad-planner": "0.0.18"
|
|
124
128
|
},
|
|
125
129
|
"scripts": {
|
|
126
130
|
"start": "bun run dev",
|