Coverage for klayout_pex/fastercap/fastercap_model_generator.py: 96%

662 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-17 17:24 +0000

1# 

2# -------------------------------------------------------------------------------- 

3# SPDX-FileCopyrightText: 2024 Martin Jan Köhler and Harald Pretl 

4# Johannes Kepler University, Institute for Integrated Circuits. 

5# 

6# This file is part of KPEX  

7# (see https://github.com/martinjankoehler/klayout-pex). 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21# SPDX-License-Identifier: GPL-3.0-or-later 

22# -------------------------------------------------------------------------------- 

23# 

24 

25# A class providing a service for building FastCap2 or FasterCap models 

26# 

27# This class is used the following way: 

28# 

29# 1) Create a FasterCapModelBuilder object 

30# Specify the default k value which is the k assumed for "empty space". 

31# You can also specify a maximum area and the "b" parameter for the 

32# triangulation. The b parameter corresponds to the minimum angle 

33# and should be <=1 (b=sin(min_angle)*2). 

34# I.e. b=1 -> min_angle=30 deg, b=0.5 -> min_angle~14.5 deg. 

35# 

36# 2) Add material definitions for the dielectrics 

37# Each material definition consists of a k value and 

38# a material name. 

39# 

40# 3) Add layers in the 2.5d view fashion 

41# Each layer is a sheet in 3d space that is extruded in vertical 

42# direction with the given start and stop z (or height) 

43# The layer must be a DRC::Layer or RBA::Region object. 

44# 

45# Layers can be added in two ways: 

46# 

47# * As conductors: specify the net name 

48# 

49# * As dielectric layer: specify the material name 

50# 

51# The layers can intersect. The package resolves intersections 

52# based on priority: conductors first, dielectrics according to 

53# their position in the "materials" definition (first entries have 

54# higher prio) 

55# 

56# 4) Generate a 3d model using "generate" 

57# This method returns an object you can use to generate STL files 

58# or FastCap files. 

59 

60 

61from __future__ import annotations 

62 

63import base64 

64from collections import defaultdict 

65import hashlib 

66import os 

67from typing import * 

68from dataclasses import dataclass 

69from functools import reduce 

70import math 

71 

72import klayout.db as kdb 

73 

74from ..log import ( 

75 debug, 

76 info, 

77 warning, 

78 error, 

79 subproc 

80) 

81 

82 

83@dataclass 

84class FasterCapModelBuilder: 

85 dbu: float 

86 """Database unit""" 

87 

88 k_void: float 

89 """Default dielectric of 'empty space'""" 

90 

91 delaunay_amax: float 

92 """Maximum area parameter for the Delaunay triangulation""" 

93 

94 delaunay_b: float 

95 """ 

96 The delaunay_b parameter for the Delaunay triangulation  

97 corresponds to the minimum angle 

98 and should be <=1 (b=sin(min_angle)*2). 

99 I.e. b=1 -> min_angle=30 deg, b=0.5 -> min_angle~14.5 deg. 

100 """ 

101 

102 def __init__(self, 

103 dbu: float, 

104 k_void: float, 

105 delaunay_amax: float = 0.0, 

106 delaunay_b: float = 1.0, 

107 ): 

108 self.dbu = dbu 

109 self.k_void = k_void 

110 self.delaunay_amax = delaunay_amax 

111 self.delaunay_b = delaunay_b 

112 

113 self.materials: Dict[str, float] = {} 

114 self.net_names: List[str] = [] 

115 

116 # layer, zstart, zstop 

117 self.clayers: Dict[str, List[Tuple[kdb.Region, float, float]]] = {} 

118 self.dlayers: Dict[str, List[Tuple[kdb.Region, float, float]]] = {} 

119 

120 info(f"DBU: {'%.12g' % self.dbu}") 

121 info(f"Delaunay b: {'%.12g' % self.delaunay_b}") 

122 info(f"Delaunay area_max: {'%.12g' % self.delaunay_amax}") 

123 

124 def add_material(self, name: str, k: float): 

125 self.materials[name] = k 

126 

127 def add_dielectric(self, 

128 material_name: str, 

129 layer: kdb.Region, 

130 z: float, 

131 height: float): 

132 if hasattr(layer, 'data'): 

133 layer = layer.data 

134 self._add_layer(name=material_name, layer=layer, is_dielectric=True, z=z, height=height) 

135 

136 def add_conductor(self, 

137 net_name: str, 

138 layer: kdb.Region, 

139 z: float, 

140 height: float): 

141 if hasattr(layer, 'data'): 

142 layer = layer.data 

143 self._add_layer(name=net_name, layer=layer, is_dielectric=False, z=z, height=height) 

144 

145 def _norm2z(self, z: float) -> float: 

146 return z * self.dbu 

147 

148 def _z2norm(self, z: float) -> float: 

149 return math.floor(z / self.dbu + 1e-6) 

150 

151 def _add_layer(self, 

152 name: str, 

153 layer: kdb.Region, 

154 z: float, 

155 height: float, 

156 is_dielectric: bool): 

157 if is_dielectric and name not in self.materials: 

158 raise ValueError(f"Unknown material {name} - did you use 'add_material'?") 

159 

160 zstart: float = z 

161 zstop: float = zstart + height 

162 

163 if is_dielectric: 

164 if name not in self.dlayers: 

165 self.dlayers[name] = [] 

166 self.dlayers[name].append((layer, self._z2norm(zstart), self._z2norm(zstop))) 

167 else: 

168 if name not in self.clayers: 

169 self.clayers[name] = [] 

170 self.clayers[name].append((layer, self._z2norm(zstart), self._z2norm(zstop))) 

171 

172 def generate(self) -> Optional[FasterCapModelGenerator]: 

173 z: List[float] = [] 

174 for ll in (self.dlayers, self.clayers): 

175 for k, v in ll.items(): 

176 for l in v: 

177 z.extend((l[1], l[2])) 

178 z = sorted([*{*z}]) # sort & uniq 

179 if len(z) == 0: 

180 return None 

181 

182 gen = FasterCapModelGenerator(dbu=self.dbu, 

183 k_void=self.k_void, 

184 delaunay_amax=self.delaunay_amax, 

185 delaunay_b=self.delaunay_b, 

186 materials=self.materials, 

187 net_names=list(self.clayers.keys())) 

188 for zcurr in z: 

189 gen.next_z(self._norm2z(zcurr)) 

190 

191 for nn, v in self.clayers.items(): 

192 for l in v: 

193 if l[1] <= zcurr < l[2]: 

194 gen.add_in(name=f"+{nn}", layer=l[0]) 

195 if l[1] < zcurr <= l[2]: 

196 gen.add_out(name=f"+{nn}", layer=l[0]) 

197 for mn, v in self.dlayers.items(): 

198 for l in v: 

199 if l[1] <= zcurr < l[2]: 

200 gen.add_in(name=f"-{mn}", layer=l[0]) 

201 if l[1] < zcurr <= l[2]: 

202 gen.add_out(name=f"-{mn}", layer=l[0]) 

203 

204 gen.finish_z() 

205 

206 gen.finalize() 

207 return gen 

208 

209 

210@dataclass(frozen=True) 

211class HDielKey: 

212 outside: Optional[str] 

213 inside: Optional[str] 

214 

215 def __str__(self) -> str: 

216 return f"{self.outside or 'void'} <-> {self.inside or 'void'}" 

217 

218 @property 

219 def topic(self) -> str: 

220 return 'dielectric' 

221 

222 def reversed(self) -> HDielKey: 

223 return HDielKey(self.inside, self.outside) 

224 

225 

226@dataclass(frozen=True) 

227class HCondKey: 

228 net_name: str 

229 outside: Optional[str] 

230 

231 def __str__(self) -> str: 

232 return f"{self.outside or 'void'} <-> {self.net_name}" 

233 

234 @property 

235 def topic(self) -> str: 

236 return 'conductor' 

237 

238 

239@dataclass(frozen=True) 

240class VKey: 

241 kk: HDielKey | HCondKey 

242 p0: kdb.DPoint 

243 de: kdb.DVector 

244 

245 

246@dataclass(frozen=True) 

247class Point: 

248 x: float 

249 y: float 

250 z: float 

251 

252 def __sub__(self, other: Point) -> Point: 

253 return Point(self.x - other.x, self.y - other.y, self.z - other.z) 

254 

255 def sq_length(self) -> float: 

256 return self.x**2 + self.y**2 + self.z**2 

257 

258 def to_fastcap(self) -> str: 

259 return '%.12g %.12g %.12g' % (self.x, self.y, self.z) 

260 

261 

262def vector_product(a: Point, b: Point) -> Point: 

263 vp = Point( 

264 a.y * b.z - a.z * b.y, 

265 a.z * b.x - a.x * b.z, 

266 a.x * b.y - a.y * b.x 

267 ) 

268 return vp 

269 

270 

271def dot_product(a: Point, b: Point) -> float: 

272 dp = a.x * b.x + a.y * b.y + a.z * b.z 

273 return dp 

274 

275 

276@dataclass(frozen=True) 

277class Triangle: 

278 p0: Point 

279 p1: Point 

280 p2: Point 

281 

282 def reversed(self) -> Triangle: 

283 return Triangle(self.p2, self.p1, self.p0) 

284 

285 def outside_reference_point(self) -> Point: 

286 v1 = self.p1 - self.p0 

287 v2 = self.p2 - self.p0 

288 vp = Point(v1.y * v2.z - v1.z * v2.y, 

289 -v1.x * v2.z + v1.z * v2.x, 

290 v1.x * v2.y - v1.y * v2.x) 

291 vp_abs = math.sqrt(vp.x ** 2 + vp.y ** 2 + vp.z ** 2) 

292 rp = Point(self.p0.x + vp.x / vp_abs, 

293 self.p0.y + vp.y / vp_abs, 

294 self.p0.z + vp.z / vp_abs) 

295 return rp 

296 

297 def to_fastcap(self) -> str: 

298 return ' '.join([p.to_fastcap() for p in (self.p0, self.p1, self.p2)]) 

299 

300 def __len__(self): 

301 return 3 

302 

303 def __getitem__(self, i) -> Point: 

304 match i: 

305 case 0: return self.p0 

306 case 1: return self.p1 

307 case 2: return self.p2 

308 case _: raise IndexError("list index out of range") 

309 

310 

311@dataclass(frozen=True) 

312class Edge: 

313 p0: Point 

314 p1: Point 

315 

316 def vector_of_edge(self) -> Point: 

317 return Point( 

318 self.p1.x - self.p0.x, 

319 self.p1.y - self.p0.y, 

320 self.p1.z - self.p0.z 

321 ) 

322 

323 def reversed(self) -> Edge: 

324 return Edge(self.p1, self.p0) 

325 

326 

327@dataclass 

328class FasterCapModelGenerator: 

329 dbu: float 

330 """Database unit""" 

331 

332 k_void: float 

333 """Default dielectric of 'empty space'""" 

334 

335 delaunay_amax: float 

336 """Maximum area parameter for the Delaunay triangulation""" 

337 

338 delaunay_b: float 

339 """ 

340 The delaunay_b parameter for the Delaunay triangulation  

341 corresponds to the minimum angle 

342 and should be <=1 (b=sin(min_angle)*2). 

343 I.e. b=1 -> min_angle=30 deg, b=0.5 -> min_angle~14.5 deg. 

344 """ 

345 

346 materials: Dict[str, float] 

347 """Maps material name to dielectric k""" 

348 

349 net_names: List[str] 

350 

351 def __init__(self, 

352 dbu: float, 

353 k_void: float, 

354 delaunay_amax: float, 

355 delaunay_b: float, 

356 materials: Dict[str, float], 

357 net_names: List[str]): 

358 self.k_void = k_void 

359 self.delaunay_amax = delaunay_amax 

360 self.delaunay_b = delaunay_b 

361 self.dbu = dbu 

362 self.materials = materials 

363 self.net_names = net_names 

364 

365 self.z: Optional[float] = None 

366 self.zz: Optional[float] = None 

367 self.layers_in: Dict[str, kdb.Region] = {} 

368 self.layers_out: Dict[str, kdb.Region] = {} 

369 self.state: Dict[str, kdb.Region] = {} 

370 self.current: Dict[str, List[kdb.Region]] = {} 

371 self.diel_data: Dict[HDielKey, List[Triangle]] = {} 

372 self.diel_vdata: Dict[VKey, kdb.Region] = {} 

373 self.cond_data: Dict[HCondKey, List[Triangle]] = {} 

374 self.cond_vdata: Dict[VKey, kdb.Region] = {} 

375 

376 def reset(self): 

377 self.layers_in = {} 

378 self.layers_out = {} 

379 

380 def add_in(self, name: str, layer: kdb.Region): 

381 debug(f"add_in: {name} -> {layer}") 

382 if name not in self.layers_in: 

383 # NOTE: Calling kdb.Region([]) with the empty list enforces, that a flat layer is created. 

384 # The following "+=" packs the polynomials into this flat layer and 

385 # does not copy the hierarchical layer from the LayoutToNetlist database. 

386 self.layers_in[name] = kdb.Region([]) 

387 self.layers_in[name] += layer 

388 

389 def add_out(self, name: str, layer: kdb.Region): 

390 debug(f"add_out: {name} -> {layer}") 

391 if name not in self.layers_out: 

392 # NOTE: Calling kdb.Region([]) with the empty list enforces, that a flat layer is created. 

393 # The following "+=" packs the polynomials into this flat layer and 

394 # does not copy the hierarchical layer from the LayoutToNetlist database. 

395 self.layers_out[name] = kdb.Region([]) 

396 self.layers_out[name] += layer 

397 

398 def finish_z(self): 

399 debug(f"Finishing layer z={self.z}") 

400 

401 din: Dict[str, kdb.Region] = {} 

402 dout: Dict[str, kdb.Region] = {} 

403 all_in = kdb.Region() 

404 all_out = kdb.Region() 

405 all = kdb.Region() 

406 all_cin: Optional[kdb.Region] = None 

407 all_cout: Optional[kdb.Region] = None 

408 

409 for names, prefix in ((self.net_names, '+'), (self.materials.keys(), '-')): 

410 for nn in names: 

411 mk = prefix + nn 

412 

413 # compute merged events 

414 if mk not in self.current: 

415 self.current[mk] = [] 

416 current_before = self.current[mk][0].dup() if len(self.current[mk]) >= 1 else kdb.Region() 

417 lin, lout, current = self._merge_events(pyra=self.current[mk], 

418 lin=self.layers_in.get(mk, None), 

419 lout=self.layers_out.get(mk, None)) 

420 debug(f"Merged events & status for {mk}:") 

421 debug(f" in = {lin}") 

422 debug(f" out = {lout}") 

423 debug(f" state = {current}") 

424 

425 if mk not in self.state: 

426 self.state[mk] = kdb.Region() 

427 

428 # legalize in and out events 

429 lin_org = lin.dup() 

430 lout_org = lout.dup() 

431 lout &= self.state[mk] 

432 lin -= all 

433 lout += current & all_in 

434 lin += current_before & all_out 

435 lin -= lout_org 

436 lout -= lin_org 

437 

438 # tracks the legalized horizontal cuts 

439 self.state[mk] += lin 

440 self.state[mk] -= lout 

441 

442 din[mk] = lin 

443 dout[mk] = lout 

444 

445 debug(f"Legalized events & status for '{mk}':") 

446 debug(f" in = {din[mk]}") 

447 debug(f" out = {dout[mk]}") 

448 debug(f" state = {self.state[mk]}") 

449 

450 all_in += lin 

451 all_out += lout 

452 all += self.state[mk] 

453 

454 if prefix == '+': 

455 all_cin = all_in.dup() 

456 all_cout = all_out.dup() 

457 

458 debug(f"All conductor region in: {all_cin}") 

459 debug(f"All conductor region out: {all_cout}") 

460 

461 # check whether states are separated 

462 a = reduce(lambda x, y: x+y, self.state.values()) 

463 for k, s in self.state.items(): 

464 r: kdb.Region = s - a 

465 if not r.is_empty(): 

466 error(f"State region of {k} ({s}) is not contained entirely " 

467 f"in remaining all state region ({a}) - this means there is an overlap") 

468 a -= s 

469 

470 # Now we have legalized the in and out events 

471 for mni in self.materials.keys(): 

472 lin = din.get(f"-{mni}", None) 

473 if lin: 

474 lin = lin.dup() 

475 lin -= all_cout # handled with the conductor 

476 for mno in self.materials.keys(): 

477 lout = dout.get(f"-{mno}", None) 

478 if lout: 

479 d: kdb.Region = lout & lin 

480 if not d.is_empty(): 

481 self.generate_hdiel(below=mno, above=mni, layer=d) 

482 lin -= lout 

483 if not lin.is_empty(): 

484 self.generate_hdiel(below=None, above=mni, layer=lin) 

485 

486 for mno in self.materials.keys(): 

487 lout = dout.get(f"-{mno}", None) 

488 if lout: 

489 lout = lout.dup() 

490 lout -= all_cin # handled with the conductor 

491 for mni in self.materials.keys(): 

492 lin = din.get(f"-{mni}", None) 

493 if lin: 

494 lout -= lin 

495 if not lout.is_empty(): 

496 self.generate_hdiel(below=mno, above=None, layer=lout) 

497 

498 for nn in self.net_names: 

499 lin = din.get(f"+{nn}", None) 

500 if lin: 

501 lin = lin.dup() 

502 for mno in self.materials.keys(): 

503 lout = dout.get(f"-{mno}", None) 

504 if lout: 

505 d = lout & lin 

506 if not d.is_empty(): 

507 self.generate_hcond_in(net_name=nn, below=mno, layer=d) 

508 lin -= lout 

509 if not lin.is_empty(): 

510 self.generate_hcond_in(net_name=nn, below=None, layer=lin) 

511 

512 for nn in self.net_names: 

513 lout = dout.get(f"+{nn}", None) 

514 if lout: 

515 lout = lout.dup() 

516 lout -= all_cin # handled with the conductor 

517 for mni in self.materials.keys(): 

518 lin = din.get(f"-{mni}", None) 

519 if lin: 

520 d = lout & lin 

521 if not d.is_empty(): 

522 self.generate_hcond_out(net_name=nn, above=mni, layer=d) 

523 lout -= lin 

524 if not lout.is_empty(): 

525 self.generate_hcond_out(net_name=nn, above=None, layer=lout) 

526 

527 def next_z(self, z: float): 

528 debug(f"Next layer {z}") 

529 

530 self.reset() 

531 

532 if self.z is None: 

533 self.z = z 

534 return 

535 

536 self.zz = z 

537 

538 all_cond = kdb.Region() 

539 for nn in self.net_names: 

540 mk = f"+{nn}" 

541 if mk in self.state: 

542 all_cond += self.state[mk] 

543 all_cond = all_cond.edges() 

544 

545 for i, mni in enumerate(self.materials): 

546 linside = self.state.get(f"-{mni}", None) 

547 if linside: 

548 linside = linside.edges() 

549 linside -= all_cond # handled with the conductor 

550 for o, mno in enumerate(self.materials): 

551 if i != o: 

552 loutside = self.state.get(f"-{mno}", None) 

553 if loutside: 

554 loutside = loutside.edges() 

555 if o > i: 

556 d = loutside & linside 

557 for e in d: 

558 # NOTE: we need to swap points as we started from "outside" 

559 self.generate_vdiel(outside=mno, inside=mni, edge=e.swapped_points()) 

560 linside -= loutside 

561 

562 for e in linside: 

563 self.generate_vdiel(outside=None, inside=mni, edge=e) 

564 

565 for nn in self.net_names: 

566 mk = f"+{nn}" 

567 linside = self.state.get(mk, None) 

568 if linside: 

569 linside = linside.edges() 

570 for mno in self.materials: 

571 loutside = self.state.get(f"-{mno}", None) 

572 if loutside: 

573 loutside = loutside.edges() 

574 d = loutside & linside 

575 for e in d: 

576 # NOTE: we need to swap points as we started from "outside" 

577 self.generate_vcond(net_name=nn, outside=mno, edge=e.swapped_points()) 

578 linside -= loutside 

579 for e in linside: 

580 self.generate_vcond(net_name=nn, outside=None, edge=e) 

581 

582 self.z = z 

583 

584 def generate_hdiel(self, 

585 below: Optional[str], 

586 above: Optional[str], 

587 layer: kdb.Region): 

588 k = HDielKey(below, above) 

589 debug(f"Generating horizontal dielectric surface {k} as {layer}") 

590 if k not in self.diel_data: 

591 self.diel_data[k] = [] 

592 data = self.diel_data[k] 

593 

594 for t in layer.delaunay(self.delaunay_amax / self.dbu ** 2, self.delaunay_b): 

595 # NOTE: normal is facing downwards (to "below") 

596 pl = list(map(lambda pt: Point(pt.x * self.dbu, pt.y * self.dbu, self.z), 

597 t.each_point_hull())) 

598 tri = Triangle(*pl) 

599 data.append(tri) 

600 debug(f" {tri}") 

601 

602 def generate_v_surface(self, 

603 kk: HDielKey | HCondKey, 

604 edge: kdb.Edge) -> Tuple[VKey, kdb.Box]: 

605 debug(f"Generating vertical {kk.topic} surface {kk} with edge {edge}") 

606 

607 el = math.sqrt(edge.sq_length()) 

608 de = kdb.DVector(edge.d().x / el, edge.d().y / el) 

609 ne = kdb.DVector(edge.d().y / el, -edge.d().x / el) 

610 p0 = ne * ne.sprod(kdb.DPoint(edge.p1) - kdb.DPoint()) + kdb.DPoint() 

611 x1 = (edge.p1 - p0).sprod(de) 

612 x2 = (edge.p2 - p0).sprod(de) 

613 

614 key = VKey(kk, p0, de) 

615 surface = kdb.Box(x1, 

616 math.floor(self.z / self.dbu + 0.5), 

617 x2, 

618 math.floor(self.zz / self.dbu + 0.5)) 

619 return key, surface 

620 

621 def generate_vdiel(self, 

622 outside: Optional[str], 

623 inside: Optional[str], 

624 edge: kdb.Edge): 

625 if edge.is_degenerate(): 

626 return 

627 

628 key, surface = self.generate_v_surface(HDielKey(outside, inside), edge) 

629 if key not in self.diel_vdata: 

630 self.diel_vdata[key] = kdb.Region() 

631 

632 self.diel_vdata[key].insert(surface) 

633 

634 def generate_hcond_in(self, 

635 net_name: str, 

636 below: Optional[str], 

637 layer: kdb.Region): 

638 k = HCondKey(net_name, below) 

639 debug(f"Generating horizontal bottom conductor surface {k} as {layer}") 

640 

641 if k not in self.cond_data: 

642 self.cond_data[k] = [] 

643 data = self.cond_data[k] 

644 

645 for t in layer.delaunay(self.delaunay_amax / self.dbu ** 2, self.delaunay_b): 

646 # NOTE: normal is facing downwards (to "below") 

647 pl = list(map(lambda pt: Point(pt.x * self.dbu, pt.y * self.dbu, self.z), 

648 t.each_point_hull())) 

649 tri = Triangle(*pl) 

650 data.append(tri) 

651 debug(f" {tri}") 

652 

653 def generate_hcond_out(self, 

654 net_name: str, 

655 above: Optional[str], 

656 layer: kdb.Region): 

657 k = HCondKey(net_name, above) 

658 debug(f"Generating horizontal top conductor surface {k} as {layer}") 

659 

660 if k not in self.cond_data: 

661 self.cond_data[k] = [] 

662 data = self.cond_data[k] 

663 

664 for t in layer.delaunay(self.delaunay_amax / self.dbu ** 2, self.delaunay_b): 

665 # NOTE: normal is facing downwards (into conductor) 

666 pl = list(map(lambda pt: Point(pt.x * self.dbu, pt.y * self.dbu, self.z), 

667 t.each_point_hull())) 

668 tri = Triangle(*pl) 

669 # now it is facing outside (to "above") 

670 tri = tri.reversed() 

671 data.append(tri) 

672 debug(f" {tri}") 

673 

674 def generate_vcond(self, 

675 net_name: str, 

676 outside: Optional[str], 

677 edge: kdb.Edge): 

678 if edge.is_degenerate(): 

679 return 

680 

681 key, surface = self.generate_v_surface(HCondKey(net_name, outside), edge) 

682 if key not in self.cond_vdata: 

683 self.cond_vdata[key] = kdb.Region() 

684 

685 self.cond_vdata[key].insert(surface) 

686 

687 def triangulate(self, p0: kdb.DPoint, de: kdb.DVector, region: kdb.Region, data: List[Triangle]): 

688 def convert_point(pt: kdb.Point) -> Point: 

689 pxy = (p0 + de * pt.x) * self.dbu 

690 pz = pt.y * self.dbu 

691 return Point(pxy.x, pxy.y, pz) 

692 

693 for t in region.delaunay(self.delaunay_amax / self.dbu ** 2, self.delaunay_b): 

694 # NOTE: normal is facing outwards (to "left") 

695 pl = list(map(convert_point, t.each_point_hull())) 

696 tri = Triangle(*pl) 

697 # now it is facing outside (to "above") 

698 data.append(tri) 

699 debug(f" {tri}") 

700 

701 def finalize(self): 

702 for k, r in self.diel_vdata.items(): 

703 debug(f"Finishing vertical dielectric plane {k.kk} at {k.p0}/{k.de}") 

704 

705 if k.kk not in self.diel_data: 

706 self.diel_data[k.kk] = [] 

707 data = self.diel_data[k.kk] 

708 

709 self.triangulate(p0=k.p0, de=k.de, region=r, data=data) 

710 

711 for k, r in self.cond_vdata.items(): 

712 debug(f"Finishing vertical conductor plane {k.kk} at {k.p0} / {k.de}") 

713 

714 if k.kk not in self.cond_data: 

715 self.cond_data[k.kk] = [] 

716 data = self.cond_data[k.kk] 

717 

718 self.triangulate(p0=k.p0, de=k.de, region=r, data=data) 

719 

720 dk: Dict[HDielKey, List[Triangle]] = {} 

721 

722 for k in self.diel_data.keys(): 

723 kk = k.reversed() 

724 if kk not in dk: 

725 dk[k] = [] 

726 else: 

727 debug(f"Combining dielectric surfaces {kk} with reverse") 

728 

729 for k, v in self.diel_data.items(): 

730 kk = k.reversed() 

731 if kk in dk: 

732 dk[kk] += list(map(lambda t: t.reversed(), v)) 

733 else: 

734 dk[k] += v 

735 

736 self.diel_data = dk 

737 

738 def write_fastcap(self, output_dir_path: str, prefix: str) -> str: 

739 max_filename_length: Optional[int] = None 

740 try: 

741 max_filename_length = os.pathconf(output_dir_path, 'PC_NAME_MAX') 

742 except AttributeError: 

743 pass # NOTE: windows does not support the os.pathconf attribute 

744 

745 lst_fn = os.path.join(output_dir_path, f"{prefix}.lst") 

746 file_num = 0 

747 lst_file: List[str] = [f"* k_void={'%.12g' % self.k_void}"] 

748 

749 for k, data in self.diel_data.items(): 

750 if len(data) == 0: 

751 continue 

752 

753 file_num += 1 

754 

755 k_outside = self.materials[k.outside] if k.outside else self.k_void 

756 k_inside = self.materials[k.inside] if k.inside else self.k_void 

757 

758 # lst_file.append(f"* Dielectric interface: outside={outside}, inside={inside}") 

759 

760 fn = f"{prefix}{file_num}_outside={k.outside or '(void)'}_inside={k.inside or '(void)'}.geo" 

761 output_path = os.path.join(output_dir_path, fn) 

762 self._write_fastercap_geo(output_path=output_path, 

763 data=data, 

764 cond_name=None, 

765 cond_number=file_num, 

766 rename_conductor=False) 

767 

768 # NOTE: for now, we compute the reference points for each triangle 

769 # This is a FasterCap feature, reference point in the *.geo file (end of each T line) 

770 rp_s = "0 0 0" 

771 lst_file.append(f"D {fn} {'%.12g' % k_outside} {'%.12g' % k_inside} 0 0 0 {rp_s}") 

772 

773 # 

774 # Feedback from FastFieldSolvers: 

775 # 

776 # - using the '+' trailing statements (conductor collation), 

777 # only the same conductor should be collated 

778 # 

779 # - renaming different conductor numbers ('N' rule line) is not allowed (currently a bug) 

780 # - Example: 1->VDD (1.geo) and 2->VDD (2.geo) is not possible 

781 # - Both conductor *.geo files should have the same number 

782 # - only the last conductor *.geo file should contain the 'N' rule 

783 # 

784 # - reference points 

785 # 

786 cond_data_grouped_by_net = defaultdict(list) 

787 for k, data in self.cond_data.items(): 

788 if len(data) == 0: 

789 continue 

790 cond_data_grouped_by_net[k.net_name].append((k.outside, data)) 

791 

792 cond_num = file_num 

793 

794 for nn, cond_list in cond_data_grouped_by_net.items(): 

795 cond_num += 1 

796 last_cond_index = len(cond_list) - 1 

797 for idx, (outside, data) in enumerate(cond_list): 

798 file_num += 1 

799 k_outside = self.materials[outside] if outside else self.k_void 

800 

801 outside = outside or '(void)' 

802 # lst_file.append(f"* Conductor interface: outside={outside}, net={nn}") 

803 fn = f"{prefix}{file_num}_outside={outside}_net={nn}.geo" 

804 if max_filename_length is not None and len(fn) > max_filename_length: 

805 warning(f"Unusual long net name detected: {nn}") 

806 d = hashlib.md5(nn.encode('utf-8')).digest() 

807 h = base64.urlsafe_b64encode(d).decode('utf-8').rstrip('=') 

808 remaining_len = len(f"{prefix}_{file_num}_outside={outside}_net=.geo") 

809 short_nn = nn[0: (max_filename_length - remaining_len - len(h) - 1)] + f"_{h}" 

810 fn = f"{prefix}{file_num}_outside={outside}_net={short_nn}.geo" 

811 output_path = os.path.join(output_dir_path, fn) 

812 self._write_fastercap_geo(output_path=output_path, 

813 data=data, 

814 cond_number=cond_num, 

815 cond_name=nn, 

816 rename_conductor=(idx == last_cond_index)) 

817 collation_operator = '' if idx == last_cond_index else ' +' 

818 lst_file.append(f"C {fn} {'%.12g' % k_outside} 0 0 0{collation_operator}") 

819 

820 subproc(lst_fn) 

821 with open(lst_fn, "w") as f: 

822 f.write('\n'.join(lst_file)) 

823 f.write('\n') 

824 

825 return lst_fn 

826 

827 @staticmethod 

828 def _write_fastercap_geo(output_path: str, 

829 data: List[Triangle], 

830 cond_number: int, 

831 cond_name: Optional[str], 

832 rename_conductor: bool): 

833 subproc(output_path) 

834 with open(output_path, "w") as f: 

835 f.write(f"0 GEO File\n") 

836 for t in data: 

837 f.write(f"T {cond_number}") 

838 f.write(' ' + t.to_fastcap()) 

839 

840 # compute a reference point "outside" 

841 rp = t.outside_reference_point() 

842 rp_s = rp.to_fastcap() 

843 

844 f.write(f" {rp_s}\n") 

845 if cond_name and rename_conductor: 

846 f.write(f"N {cond_number} {cond_name}\n") 

847 

848 def check(self): 

849 info("Checking …") 

850 errors = 0 

851 

852 for mn in self.materials.keys(): 

853 tris = self._collect_diel_tris(mn) 

854 info(f"Material {mn} -> {len(tris)} triangles") 

855 errors += self._check_tris(f"Material '{mn}'", tris) 

856 

857 for nn in self.net_names: 

858 tris = self._collect_cond_tris(nn) 

859 info(f"Net '{nn}' -> {len(tris)} triangles") 

860 errors += self._check_tris(f"Net '{nn}'", tris) 

861 

862 if errors == 0: 

863 info(" No errors found") 

864 else: 

865 info(f" {errors} error{'s' if errors >= 2 else ''} found") 

866 

867 def _check_tris(self, msg: str, triangles: List[Triangle]) -> int: 

868 errors = 0 

869 

870 edge_set: Set[Edge] = set() 

871 edges = self._normed_edges(triangles) 

872 

873 for e in edges: 

874 if e in edge_set: 

875 error(f"{msg}: duplicate edge {self._edge2s(e)}") 

876 errors += 1 

877 else: 

878 edge_set.add(e) 

879 

880 self._split_edges(edge_set) 

881 

882 for e in edge_set: 

883 if e.reversed() not in edge_set: 

884 error(f"{msg}: edge {self._edge2s(e)} not connected with reverse edge (open surface)") 

885 errors += 1 

886 

887 return errors 

888 

889 def _normed_edges(self, triangles: List[Triangle]) -> List[Edge]: 

890 edges = [] 

891 

892 def normed_dbu(p: Point): 

893 return Point(*tuple(map(lambda c: math.floor(c / self.dbu + 0.5), 

894 (p.x, p.y, p.z)))) 

895 

896 for t in triangles: 

897 for i in range(0, 3): 

898 p1 = normed_dbu(t[i]) 

899 p2 = normed_dbu(t[(i + 1) % 3]) 

900 edges.append(Edge(p1, p2)) 

901 

902 return edges 

903 

904 def _point2s(self, p: Point) -> str: 

905 return f"(%.12g, %.12g, %.12g)" % (p.x * self.dbu, p.y * self.dbu, p.z * self.dbu) 

906 

907 def _edge2s(self, e: Edge) -> str: 

908 return f"{self._point2s(e.p0)}-{self._point2s(e.p1)}" 

909 

910 @staticmethod 

911 def _is_antiparallel(a: Point, 

912 b: Point) -> bool: 

913 vp = vector_product(a, b) 

914 if abs(vp.sq_length()) > 0.5: # we got normalized! 

915 return False 

916 

917 sp = dot_product(a, b) 

918 return sp < 0 

919 

920 def _split_edges(self, edges: set[Edge]): 

921 edges_by_p2: DefaultDict[Point, List[Edge]] = defaultdict(list) 

922 edges_by_p1: DefaultDict[Point, List[Edge]] = defaultdict(list) 

923 for e in edges: 

924 edges_by_p2[e.p1].append(e) 

925 edges_by_p1[e.p0].append(e) 

926 

927 i = 0 

928 while True: 

929 i += 1 

930 subst: DefaultDict[Edge, List[Edge]] = defaultdict(list) 

931 

932 for e in edges: 

933 ee = edges_by_p2.get(e.p0, []) 

934 for eee in ee: 

935 ve = e.vector_of_edge() 

936 veee = eee.vector_of_edge() 

937 if self._is_antiparallel(ve, veee) and \ 

938 (veee.sq_length() < ve.sq_length() - 0.5): 

939 # There is a shorter edge antiparallel -> 

940 # this means we need to insert a split point into e 

941 subst[e] += [Edge(e.p0, eee.p0), Edge(eee.p0, e.p1)] 

942 

943 for e in edges: 

944 ee = edges_by_p1.get(e.p1, []) 

945 for eee in ee: 

946 ve = e.vector_of_edge() 

947 veee = eee.vector_of_edge() 

948 if self._is_antiparallel(ve, veee) and \ 

949 (veee.sq_length() < ve.sq_length() - 0.5): 

950 # There is a shorter edge antiparallel -> 

951 # this means we need to insert a split point into e 

952 subst[e] += [Edge(e.p0, eee.p1), Edge(eee.p1, e.p1)] 

953 

954 if len(subst) == 0: 

955 break 

956 

957 for e, replacement in subst.items(): 

958 edges_by_p1[e.p0].remove(e) 

959 edges_by_p2[e.p1].remove(e) 

960 edges.remove(e) 

961 for r in replacement: 

962 edges.add(r) 

963 edges_by_p1[r.p0].append(r) 

964 edges_by_p2[r.p1].append(r) 

965 

966 def dump_stl(self, output_dir_path: str, prefix: str): 

967 for mn in self.materials.keys(): 

968 tris = self._collect_diel_tris(mn) 

969 output_path = os.path.join(output_dir_path, f"{prefix}diel_{mn}.stl") 

970 self._write_as_stl(output_path, tris) 

971 

972 for nn in self.net_names: 

973 tris = self._collect_cond_tris(nn) 

974 output_path = os.path.join(output_dir_path, f"{prefix}cond_{nn}.stl") 

975 self._write_as_stl(output_path, tris) 

976 

977 @staticmethod 

978 def _write_as_stl(file_name: str, 

979 tris: List[Triangle]): 

980 if len(tris) == 0: 

981 return 

982 

983 subproc(file_name) 

984 with open(file_name, "w") as f: 

985 f.write("solid stl\n") 

986 for t in tris: 

987 f.write(" facet normal 0 0 0\n") 

988 f.write(" outer loop\n") 

989 t = t.reversed() 

990 for p in (t.p0, t.p1, t.p2): 

991 f.write(f" vertex {p.to_fastcap()}\n") 

992 f.write(" endloop\n") 

993 f.write(" endfacet\n") 

994 f.write("endsolid stl\n") 

995 

996 @staticmethod 

997 def _merge_events(pyra: List[Optional[kdb.Region]], 

998 lin: Optional[kdb.Region], 

999 lout: Optional[kdb.Region]) -> Tuple[kdb.Region, kdb.Region, kdb.Region]: 

1000 lin = lin.dup() if lin else kdb.Region() 

1001 lout = lout.dup() if lout else kdb.Region() 

1002 past = pyra[0].dup() if len(pyra) >= 1 else kdb.Region() 

1003 

1004 for i in range(0, len(pyra)): 

1005 ii = len(pyra) - i 

1006 added: kdb.Region = lin & pyra[ii - 1] 

1007 if not added.is_empty(): 

1008 if ii >= len(pyra): 

1009 pyra.append(kdb.Region()) 

1010 assert len(pyra) == ii + 1 

1011 pyra[ii] += added 

1012 lin -= added 

1013 

1014 if len(pyra) == 0: 

1015 pyra.append(kdb.Region()) 

1016 pyra[0] += lin 

1017 

1018 for i in range(0, len(pyra)): 

1019 ii = len(pyra) - i 

1020 removed: kdb.Region = lout & pyra[ii - 1] 

1021 if not removed.is_empty(): 

1022 pyra[ii - 1] -= removed 

1023 lout -= removed 

1024 

1025 # compute merged events 

1026 lin = pyra[0] - past 

1027 lout = past - pyra[0] 

1028 return lin, lout, pyra[0] 

1029 

1030 def _collect_diel_tris(self, material_name: str) -> List[Triangle]: 

1031 tris = [] 

1032 

1033 for k, v in self.diel_data.items(): 

1034 if material_name == k.outside: 

1035 tris += v 

1036 elif material_name == k.inside: 

1037 tris += [t.reversed() for t in v] 

1038 

1039 for k, v in self.cond_data.items(): 

1040 if material_name == k.outside: 

1041 tris += v 

1042 

1043 return tris 

1044 

1045 def _collect_cond_tris(self, net_name: str) -> List[Triangle]: 

1046 tris = [] 

1047 for k, v in self.cond_data.items(): 

1048 if k.net_name == net_name: 

1049 tris += [t.reversed() for t in v] 

1050 return tris