Move coil stuff to separate repo

autoroute
jaseg 2023-10-26 00:48:52 +02:00
rodzic a35125b123
commit 9624e46147
15 zmienionych plików z 0 dodań i 4589 usunięć

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1,318 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
import os
from math import *
from pathlib import Path
from itertools import cycle
from gerbonara.cad.kicad import pcb as kicad_pcb
from gerbonara.cad.kicad import footprints as kicad_fp
from gerbonara.cad.kicad import graphical_primitives as kicad_gr
import click
__version__ = '1.0.0'
def point_line_distance(p, l1, l2):
x0, y0 = p
x1, y1 = l1
x2, y2 = l2
# https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
return abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) / sqrt((x2-x1)**2 + (y2-y1)**2)
def line_line_intersection(l1, l2):
p1, p2 = l1
p3, p4 = l2
x1, y1 = p1
x2, y2 = p2
x3, y3 = p3
x4, y4 = p4
# https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
px = ((x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4))/((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4))
py = ((x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4))/((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4))
return px, py
def angle_between_vectors(va, vb):
angle = atan2(vb[1], vb[0]) - atan2(va[1], va[0])
if angle < 0:
angle += 2*pi
return angle
@click.command()
@click.argument('infile', required=False, type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
@click.option('--footprint-name', help="Name for the generated footprint. Default: Output file name sans extension.")
@click.option('--target-layer', default='F.Cu', help="Target KiCad layer for the generated footprint. Default: F.Cu.")
@click.option('--polygon', type=int, default=0, help="Use n'th polygon instead of first one. 0-based index.")
@click.option('--start-angle', type=float, default=0, help='Angle for the start at the outermost layer of the spiral in degree')
@click.option('--windings', type=int, default=5, help='Number of windings to generate')
@click.option('--trace-width', type=float, default=0.15)
@click.option('--clearance', type=float, default=0.15)
@click.option('--clipboard/--no-clipboard', help='Use clipboard integration (requires wl-clipboard)')
@click.option('--counter-clockwise/--clockwise', help='Direction of generated spiral. Default: clockwise when wound from the inside.')
def generate(infile, outfile, polygon, start_angle, windings, trace_width, clearance, footprint_name, target_layer, clipboard, counter_clockwise):
if 'WAYLAND_DISPLAY' in os.environ:
copy, paste, cliputil = ['wl-copy'], ['wl-paste'], 'xclip'
else:
copy, paste, cliputil = ['xclip', '-i', '-sel', 'clipboard'], ['xclip', '-o', '-sel' 'clipboard'], 'wl-clipboard'
if clipboard:
try:
proc = subprocess.run(paste, capture_output=True, text=True, check=True)
except FileNotFoundError:
print(f'Error: --clipboard requires the {copy[0]} and {paste[0]} utilities from {cliputil} to be installed.', file=sys.stderr)
board = kicad_pcb.Board.load(proc.stdout)
elif not infile:
board = kicad_pcb.Board.load(sys.stdin.read())
else:
board = kicad_pcb.Board.open(infile)
objs = [obj for obj in board.objects() if isinstance(obj, kicad_gr.Polygon)]
print(f'Found {len(objs)} polygon(s).', file=sys.stderr)
poly = objs[polygon]
xy = [(pt.x, pt.y) for pt in poly.pts.xy]
if counter_clockwise:
xy = [(-x, y) for x, y in xy]
segments = list(zip(xy, xy[1:] + xy[:1]))
# normalize orientation, make xy counter-clockwise
if sum((x2 - x1) * (y2 + y1) for (x1, y1), (x2, y2) in segments) < 0:
print(f'Reversing polygon direction.', file=sys.stderr)
xy = xy[::-1]
segments = list(zip(xy, xy[1:] + xy[:1]))
vbx, vby = min(x for x, y in xy), min(y for x, y, in xy)
vbw, vbh = max(x for x, y in xy), max(y for x, y, in xy)
vbw, vbh = vbw-vbx, vbh-vby
vbx -= 5
vby -= 5
vbw += 10
vbh += 10
cx, cy = 0, 0
ls = 0
for (x1, y1), (x2, y2) in segments:
l = dist((x1, y1), (x2, y2))
cx += x1*l/2 + x2*l/2
cy += y1*l/2 + y2*l/2
ls += l
cx /= ls
cy /= ls
segment_angles = [(atan2(y1-cy, x1-cx) - atan2(y2-cy, x2-cx) + 2*pi) % (2*pi) for (x1, y1), (x2, y2) in segments]
angle_strs = [f'{degrees(a):.2f}' for a in segment_angles]
segment_heights = [point_line_distance((cx, cy), (x1, y1), (x2, y2)) for (x1, y1), (x2, y2) in segments]
segment_foo = list(zip(segment_heights, segments))
midpoints = []
for h, ((x1, y1), (x2, y2)) in segment_foo:
xb = (x1 + x2) / 2
yb = (y1 + y2) / 2
midpoints.append((xb, yb))
normals = []
for h, ((x1, y1), (x2, y2)) in segment_foo:
d12 = dist((x1, y1), (x2, y2))
dx = x2 - x1
dy = y2 - y1
normals.append((-dy/d12, dx/d12))
smallest_radius = min(segment_heights)
#trace_radius = smallest_radius - stop_radius
trace_radius = smallest_radius
segment_foo = list(zip(segment_heights, segments, segment_angles, midpoints, normals))
dbg_lines1, dbg_lines2 = [], []
spiral_points = []
dr_tot = trace_width/2
for n in range(windings):
for (ha, (pa1, pa2), aa, ma, na), (hb, (pb1, pb2), ab, mb, nb) in zip(segment_foo[-1:] + segment_foo[:-1], segment_foo):
pitch = clearance + trace_width
dr_tot_a = dr_tot
dr_tot_b = n * pitch + trace_width/2
xma, yma = ma
xna, yna = na
xmb, ymb = mb
xnb, ynb = nb
xa1, ya1 = pa1
xa2, ya2 = pa2
xb1, yb1 = pb1
xb2, yb2 = pb2
dma = dist(pa2, ma)
dmb = dist(pb1, mb)
x_cons_a, y_cons_a = p_cons_a = line_line_intersection((pa2, (cx, cy)), (ma, (xma-xna, yma-yna)))
d_cons_a = dist(p_cons_a, ma)
qa = dma * dr_tot_a / d_cons_a
dra = hypot(qa, dr_tot_a)
nrax = (xa2 - cx) / dist((cx, cy), pa2)
nray = (ya2 - cy) / dist((cx, cy), pa2)
xea = xa2 - nrax*dra
yea = ya2 - nray*dra
x_cons_b, y_cons_b = p_cons_b = line_line_intersection((pb1, (cx, cy)), (mb, (xmb-xnb, ymb-ynb)))
d_cons_b = dist(p_cons_b, mb)
qb = dmb * dr_tot_b / d_cons_b
drb = hypot(qb, dr_tot_b)
nrbx = (xb1 - cx) / dist((cx, cy), pb1)
nrby = (yb1 - cy) / dist((cx, cy), pb1)
xeb = xb1 - nrbx*drb
yeb = yb1 - nrby*drb
xsa = xma - xna*dr_tot_a
ysa = yma - yna*dr_tot_a
xsb = xmb - xnb*dr_tot_b
ysb = ymb - ynb*dr_tot_b
l1 = (xsa, ysa), (xea, yea)
l2 = (xsb, ysb), (xeb, yeb)
dbg_lines1.append(l1)
dbg_lines2.append(l2)
pic = line_line_intersection(l1, l2)
spiral_points.append(pic)
dr_tot = dr_tot_b
#spiral_points = []
#r_now = 0
#for winding in range(num_windings):
# for angle, ((x1, y1), (x2, y2)) in zip(segment_angles, segments):
# angle_frac = angle/(2*pi)
# d_r = angle_frac * (clearance + trace_width)
# r_pt = dist((cx, cy), (x1, y1)) * (num_windings - winding) / num_windings
#
# x1, y1 = x1-cx, y1-cy
# x2, y2 = x2-cx, y2-cy
# l1, l2 = hypot(x1, y1), hypot(x2, y2)
# x1, y1 = x1/l1, y1/l1
# x2, y2 = x2/l2, y2/l2
#
# r_now += d_r
# spiral_points.append((cx + x1*r_pt, cy + y1*r_pt))
out = [spiral_points[0]]
ndrop = 0
for i, (pa, pb, pc) in enumerate(zip(spiral_points, spiral_points[1:], spiral_points[2:])):
xa, ya = pa
xb, yb = pb
xc, yc = pc
if ndrop:
ndrop -= 1
continue
angle = angle_between_vectors((xa-xb, ya-yb), (xc-xb, yc-yb))
if angle > pi:
ndrop += 1
for pd, pe in zip(spiral_points[i+2:], spiral_points[i+3:]):
xd, yd = pd
xe, ye = pe
angle = angle_between_vectors((xa-xb, ya-yb), (xe-xd, ye-yd))
if angle > pi:
ndrop += 1
else:
out.append(line_line_intersection((pa, pb), (pd, pe)))
break
else:
out.append(pb)
spiral_points = out
path_d = ' '.join([f'M {xy[0][0]} {xy[0][1]}', *[f'L {x} {y}' for x, y in xy[1:]], 'Z'])
path_d2 = ' '.join(f'M {cx} {cy} L {x} {y}' for x, y in xy)
path_d3 = ' '.join([f'M {spiral_points[0][0]} {spiral_points[0][1]}', *[f'L {x} {y}' for x, y in spiral_points[1:]]])
with open('/tmp/test.svg', 'w') as f:
f.write('<?xml version="1.0" standalone="no"?>\n')
f.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
f.write(f'<svg version="1.1" width="200mm" height="200mm" viewBox="{vbx} {vby} {vbw} {vbh}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">>\n')
f.write(f'<path fill="none" stroke="#303030" stroke-width="0.05" d="{path_d}"/>\n')
f.write(f'<path fill="none" stroke="#a0a0a0" stroke-width="0.05" d="{path_d2}"/>\n')
f.write(f'<path fill="none" stroke="#ff00ff" opacity="0.5" stroke-width="{trace_width}" d="{path_d3}"/>\n')
for (x1, y1), (x2, y2) in dbg_lines1:
f.write(f'<path fill="none" stroke="#ff0000" opacity="0.2" stroke-width="0.05" d="M {x1} {y1} L {x2} {y2}"/>')
for (x1, y1), (x2, y2) in dbg_lines2:
f.write(f'<path fill="none" stroke="#0000ff" opacity="0.2" stroke-width="0.05" d="M {x1} {y1} L {x2} {y2}"/>')
for x, y in midpoints:
f.write(f'<path fill="none" stroke="#a0a0ff" stroke-width="0.05" d="M {cx} {cy} L {x} {y}"/>')
f.write(f'<circle r="0.1" fill="blue" stroke="none" cx="{x}" cy="{y}"/>\n')
f.write(f'<circle r="0.1" fill="red" stroke="none" cx="{cx}" cy="{cy}"/>\n')
f.write('</svg>\n')
if counter_clockwise:
spiral_points = [(-x, y) for x, y in spiral_points]
fp_lines = [
kicad_fp.Line(
start=kicad_fp.XYCoord(x=x1, y=y1),
end=kicad_fp.XYCoord(x=x2, y=y2),
layer=target_layer,
stroke=kicad_fp.Stroke(width=trace_width))
for (x1, y1), (x2, y2) in zip(spiral_points, spiral_points[1:])]
make_pad = lambda num, x, y: kicad_fp.Pad(
number=str(num),
type=kicad_fp.Atom.smd,
shape=kicad_fp.Atom.circle,
at=kicad_fp.AtPos(x=x, y=y),
size=kicad_fp.XYCoord(x=trace_width, y=trace_width),
layers=[target_layer],
clearance=clearance,
zone_connect=0,
)
if footprint_name:
name = footprint_name
elif outfile:
name = outfile.stem,
else:
name = 'generated_coil'
fp = kicad_fp.Footprint(
name=name,
generator=kicad_fp.Atom('GerbonaraCoilGenV1'),
layer='F.Cu',
descr=f"{windings} winding coil footprint generated by gerbonara'c Coil generator, version {__version__}",
clearance=clearance,
zone_connect=0,
lines=fp_lines,
pads=[make_pad(1, *spiral_points[0]), make_pad(2, *spiral_points[-1])],
)
if clipboard:
try:
print(f'Running {copy[0]}.')
proc = subprocess.Popen(copy, stdin=subprocess.PIPE, text=True)
proc.communicate(fp.serialize())
except FileNotFoundError:
print(f'Error: --clipboard requires the {copy[0]} and {paste[0]} utilities from {cliputil} to be installed.', file=sys.stderr)
elif not outfile:
print(fp.serialize())
else:
fp.write(outfile)
if __name__ == '__main__':
generate()

Wyświetl plik

@ -1,38 +0,0 @@
air:
Density: 1.1885 # 20°C
Electric Conductivity: 0.0
Heat Capacity: 1006.4 # 20°C
Heat Conductivity: 0.025873 # 20°C
Relative Permeability: 1
Relative Permittivity: 1
ro4003c:
Density: 1790 # 23°C
Relative Permeability: 1
Relative Permittivity: 3.55
fr4:
Density: 1850 # 23°C
Relative Permeability: 1
Relative Permittivity: 4.4
Heat Conductivity: 0.81 # in-plane
ideal:
Relative Permittivity: 1
copper:
Density: 8960.0 # 0°C
Electric Conductivity: 59600000 # 20°C
Emissivity: 0.012 # 327°C
Heat Capacity: 415.0 # 200°C
Heat Conductivity: 401.0 # 0°C
Relative Permeability: 1
Relative Permittivity: 1
steel_1.4541:
Density: 7900.0 # 20°C
Electric Conductivity: 1370
Emissivity: 0.111 # 200°C
Heat Capacity: 470.0 # 20°C
Heat Conductivity: 15.0 # 20°C
Relative Permeability: 1
Relative Permittivity: 1
water:
Density: 1000.0
Heat Capacity: 4182.0
Heat Conductivity: 0.6

Wyświetl plik

@ -1,14 +0,0 @@
3D_steady:
Mesh Levels: 1
Max Output Level: 7
Coordinate System: Cartesian
Coordinate Mapping(3): 1 2 3
Simulation Type: Steady state
Steady State Max Iterations: 1
Output Intervals: 1
Timestepping Method: BDF
Simulation Timing: True
BDF Order: 1
Solver Input File: case.sif
Post File: case.vtu
Output File: case.result

Wyświetl plik

@ -1,55 +0,0 @@
Static_Current_Conduction:
Equation: Static Current Conduction
Variable: Potential
Variable DOFs: 1
Procedure: '"StatCurrentSolve" "StatCurrentSolver"'
Calculate Volume Current: True
Calculate Joule Heating: False
Optimize Bandwidth: True
Nonlinear System Max Iterations: 1
Linear System Solver: Iterative
Linear System Iterative Method: CG
Linear System Max Iterations: 10000
Linear System Convergence Tolerance: 1.0e-10
Linear System Preconditioning: ILU3
Linear System ILUT Tolerance: 1.0e-3
Linear System Abort Not Converged: False
Linear System Residual Output: 20
Linear System Precondition Recompute: 1
Magneto_Dynamics:
Equation: MGDynamics
Procedure: '"MagnetoDynamics" "WhitneyAVSolver"'
Newton-Raphson Iteration: True
Nonlinear System Max Iterations: 1
Fix Input Current Density: True
Linear System Solver: Iterative
Linear System Preconditioning: none
Linear System Convergence Tolerance: 1e-8
Linear System Abort Not Converged: False
Linear System Residual Output: 20
Linear System Max Iterations: 5000
Linear System Iterative Method: TFQMR
BiCGStabL Polynomial Degree: 4
"Jfix: Linear System Max Iterations": 5000
Idrs Parameter: 6
Solver Timing: True
Magneto_Dynamics_Calculations:
Equation: MGDynamicsCalc
Procedure: '"MagnetoDynamics" "MagnetoDynamicsCalcFields"'
Linear System Symmetric: True
Calculate Magnetic Field Strength: True
Calculate JxB: False
Calculate Current Density: True
Calculate Electric Field: True
Calculate Nodal Fields: False
Calculate Elemental Fields: True
Linear System Solver: Iterative
Linear System Preconditioning: ILU0
Linear System Abort Not Converged: False
Linear System Residual Output: 0
Linear System Max Iterations: 5000
Linear System Iterative Method: CG
Linear System Convergence Tolerance: 1.0e-8

Wyświetl plik

@ -1,595 +0,0 @@
#!/usr/bin/env python3
import math
from pathlib import Path
import multiprocessing
import re
import tempfile
import fnmatch
import shutil
import numpy as np
from pyelmer import elmer
import subprocess_tee
import click
from scipy import constants
def enumerate_mesh_bodies(msh_file):
with open(msh_file, 'r') as f:
for line in f:
if line.startswith('$PhysicalNames'):
break
else:
raise ValueError('No physcial bodies found in mesh file.')
_num_names = next(f)
for line in f:
if line.startswith('$EndPhysicalNames'):
break
dim, _, line = line.strip().partition(' ')
tag, _, name = line.partition(' ')
yield name.strip().strip('"'), (int(dim), int(tag))
# https://en.wikipedia.org/wiki/Metric_prefix
SI_PREFIX = 'QRYZEPTGMk mµnpfazyrq'
def format_si(value, unit='', fractional_digits=1):
mag = int(math.log10(abs(value))//3)
value /= 1000**mag
prefix = SI_PREFIX[SI_PREFIX.find(' ') - mag].strip()
value = f'{{:.{fractional_digits}f}}'.format(value)
return f'{value} {prefix}{unit}'
INPUT_EXT_MAP = {
'.grd': 1,
'.mesh*': 2,
'.ep': 3,
'.ansys': 4,
'.inp': 5,
'.fil': 6,
'.FDNEUT': 7,
'.unv': 8,
'.mphtxt': 9,
'.dat': 10,
'.node': 11,
'.ele': 11,
'.mesh': 12,
'.msh': 14,
'.ep.i': 15,
'.2dm': 16}
OUTPUT_EXT_MAP = {
'.grd': 1,
'.mesh*': 2,
'.ep': 3,
'.msh': 4,
'.vtu': 5}
def elmer_grid(infile, outfile=None, intype=None, outtype=None, cwd=None, stdout_log=None, stderr_log=None, **kwargs):
infile = Path(infile)
if outfile is not None:
outfile = Path(outfile)
if intype is None:
intype = str(INPUT_EXT_MAP[infile.suffix])
if outtype is None:
if outfile is not None and outfile.suffix:
outtype = str(OUTPUT_EXT_MAP[outfile.suffix])
else:
outtype = '2'
if outfile is not None:
kwargs['out'] = str(outfile)
args = ['ElmerGrid', intype, outtype, str(infile)]
for key, value in kwargs.items():
args.append(f'-{key}')
if isinstance(value, (tuple, list)):
args.extend(str(v) for v in value)
else:
args.append(str(value))
result = subprocess_tee.run(args, cwd=cwd, check=True)
if stdout_log:
Path(stdout_log).write_text(result.stdout or '')
if stderr_log:
Path(stderr_log).write_text(result.stderr or '')
def elmer_solver(cwd, stdout_log=None, stderr_log=None):
result = subprocess_tee.run(['ElmerSolver'], cwd=cwd, check=True)
if stdout_log:
Path(stdout_log).write_text(result.stdout or '')
if stderr_log:
Path(stderr_log).write_text(result.stderr or '')
return result
@click.group()
def cli():
pass
@cli.command()
@click.option('-d', '--sim-dir', type=click.Path(dir_okay=True, file_okay=False, path_type=Path))
@click.option('-o', '--output', type=click.Path(dir_okay=False, writable=True, path_type=Path), help='Capacitance matrix output file')
@click.argument('mesh_file', type=click.Path(dir_okay=False, path_type=Path))
def capacitance_matrix(mesh_file, sim_dir, output):
physical = dict(enumerate_mesh_bodies(mesh_file))
if sim_dir is not None:
sim_dir = Path(sim_dir)
sim_dir.mkdir(exist_ok=True)
sim = elmer.load_simulation('3D_steady', 'coil_parasitics_sim.yml')
mesh_dir = '.'
mesh_fn = 'mesh'
sim.header['Mesh DB'] = f'"{mesh_dir}" "{mesh_fn}"'
sim.constants.update({
'Permittivity of Vacuum': str(constants.epsilon_0),
'Gravity(4)': f'0 -1 0 {constants.g}',
'Boltzmann Constant': str(constants.Boltzmann),
'Unit Charge': str(constants.elementary_charge)})
air = elmer.load_material('air', sim, 'coil_parasitics_materials.yml')
fr4 = elmer.load_material('fr4', sim, 'coil_parasitics_materials.yml')
solver_electrostatic = elmer.load_solver('Electrostatics_Capacitance', sim, 'coil_parasitics_solvers.yml')
solver_electrostatic.data['Potential Difference'] = '1.0'
eqn = elmer.Equation(sim, 'main', [solver_electrostatic])
bdy_sub = elmer.Body(sim, 'substrate', [physical['substrate'][1]])
bdy_sub.material = fr4
bdy_sub.equation = eqn
bdy_ab = elmer.Body(sim, 'airbox', [physical['airbox'][1]])
bdy_ab.material = air
bdy_ab.equation = eqn
max_num = -1
# boundaries
for name, identity in physical.items():
if (m := re.fullmatch(r'trace([0-9]+)', name)):
num = int(m.group(1))
max_num = max(num, max_num)
bndry_m2 = elmer.Boundary(sim, name, [identity[1]])
bndry_m2.data['Capacitance Body'] = str(num)
if (tr := physical.get('trace')):
bndry_m2 = elmer.Boundary(sim, 'trace', [tr[1]])
bndry_m2.data['Capacitance Body'] = f'{max_num+1}'
boundary_airbox = elmer.Boundary(sim, 'FarField', [physical['airbox_surface'][1]])
boundary_airbox.data['Electric Infinity BC'] = 'True'
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = sim_dir if sim_dir else Path(tmpdir)
sim.write_startinfo(tmpdir)
sim.write_sif(tmpdir)
# Convert mesh from gmsh to elemer formats. Also scale it from 1 unit = 1 mm to 1 unit = 1 m (SI units)
elmer_grid(mesh_file.absolute(), 'mesh', cwd=tmpdir, scale=[1e-3, 1e-3, 1e-3],
stdout_log=(tmpdir / 'ElmerGrid_stdout.log'),
stderr_log=(tmpdir / 'ElmerGrid_stderr.log'))
elmer_solver(tmpdir,
stdout_log=(tmpdir / 'ElmerSolver_stdout.log'),
stderr_log=(tmpdir / 'ElmerSolver_stderr.log'))
capacitance_matrix = np.loadtxt(tmpdir / 'capacitance.txt')
np.savetxt(output, capacitance_matrix)
@cli.command()
@click.option('-d', '--sim-dir', type=click.Path(dir_okay=True, file_okay=False, path_type=Path))
@click.option('--solver-method')
@click.argument('mesh_file', type=click.Path(dir_okay=False, path_type=Path))
def inductance(mesh_file, sim_dir, solver_method):
physical = dict(enumerate_mesh_bodies(mesh_file))
if sim_dir is not None:
sim_dir = Path(sim_dir)
sim_dir.mkdir(exist_ok=True)
sim = elmer.load_simulation('3D_steady', 'coil_mag_sim.yml')
mesh_dir = '.'
mesh_fn = 'mesh'
sim.header['Mesh DB'] = f'"{mesh_dir}" "{mesh_fn}"'
sim.constants.update({
'Permittivity of Vacuum': str(constants.epsilon_0),
'Gravity(4)': f'0 -1 0 {constants.g}',
'Boltzmann Constant': str(constants.Boltzmann),
'Unit Charge': str(constants.elementary_charge)})
air = elmer.load_material('air', sim, 'coil_mag_materials.yml')
fr4 = elmer.load_material('fr4', sim, 'coil_mag_materials.yml')
copper = elmer.load_material('copper', sim, 'coil_mag_materials.yml')
solver_current = elmer.load_solver('Static_Current_Conduction', sim, 'coil_mag_solvers.yml')
solver_magdyn = elmer.load_solver('Magneto_Dynamics', sim, 'coil_mag_solvers.yml')
if solver_method:
solver_magdyn.data['Linear System Iterative Method'] = solver_method
solver_magdyn_calc = elmer.load_solver('Magneto_Dynamics_Calculations', sim, 'coil_mag_solvers.yml')
copper_eqn = elmer.Equation(sim, 'copperEqn', [solver_current, solver_magdyn, solver_magdyn_calc])
air_eqn = elmer.Equation(sim, 'airEqn', [solver_magdyn, solver_magdyn_calc])
bdy_trace = elmer.Body(sim, 'trace', [physical['trace'][1]])
bdy_trace.material = copper
bdy_trace.equation = copper_eqn
bdy_sub = elmer.Body(sim, 'substrate', [physical['substrate'][1]])
bdy_sub.material = fr4
bdy_sub.equation = air_eqn
bdy_ab = elmer.Body(sim, 'airbox', [physical['airbox'][1]])
bdy_ab.material = air
bdy_ab.equation = air_eqn
bdy_if_top = elmer.Body(sim, 'interface_top', [physical['interface_top'][1]])
bdy_if_top.material = copper
bdy_if_top.equation = copper_eqn
bdy_if_bottom = elmer.Body(sim, 'interface_bottom', [physical['interface_bottom'][1]])
bdy_if_bottom.material = copper
bdy_if_bottom.equation = copper_eqn
potential_force = elmer.BodyForce(sim, 'electric_potential', {'Electric Potential': 'Equals "Potential"'})
bdy_trace.body_force = potential_force
# boundaries
boundary_airbox = elmer.Boundary(sim, 'FarField', [physical['airbox_surface'][1]])
boundary_airbox.data['Electric Infinity BC'] = 'True'
boundary_vplus = elmer.Boundary(sim, 'Vplus', [physical['interface_top'][1]])
boundary_vplus.data['Potential'] = 1.0
boundary_vplus.data['Save Scalars'] = True
boundary_vminus = elmer.Boundary(sim, 'Vminus', [physical['interface_bottom'][1]])
boundary_vminus.data['Potential'] = 0.0
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = sim_dir if sim_dir else Path(tmpdir)
sim.write_startinfo(tmpdir)
sim.write_sif(tmpdir)
# Convert mesh from gmsh to elemer formats. Also scale it from 1 unit = 1 mm to 1 unit = 1 m (SI units)
elmer_grid(mesh_file.absolute(), 'mesh', cwd=tmpdir, scale=[1e-3, 1e-3, 1e-3],
stdout_log=(tmpdir / 'ElmerGrid_stdout.log'),
stderr_log=(tmpdir / 'ElmerGrid_stderr.log'))
solver_stdout, solver_stderr = (tmpdir / 'ElmerSolver_stdout.log'), (tmpdir / 'ElmerSolver_stderr.log')
res = elmer_solver(tmpdir,
stdout_log=solver_stdout,
stderr_log=solver_stderr)
P, R, U_mag = None, None, None
solver_error = False
for l in res.stdout.splitlines():
if (m := re.fullmatch(r'StatCurrentSolve:\s*Total Heating Power\s*:\s*([0-9.+-Ee]+)\s*', l)):
P = float(m.group(1))
elif (m := re.fullmatch(r'StatCurrentSolve:\s*Effective Resistance\s*:\s*([0-9.+-Ee]+)\s*', l)):
R = float(m.group(1))
elif (m := re.fullmatch(r'MagnetoDynamicsCalcFields:\s*ElectroMagnetic Field Energy\s*:\s*([0-9.+-Ee]+)\s*', l)):
U_mag = float(m.group(1))
elif re.fullmatch(r'IterSolve: Linear iteration did not converge to tolerance', l):
solver_error = True
if solver_error:
raise click.ClickException(f'Error: One of the solvers did not converge. See log files for details:\n{solver_stdout.absolute()}\n{solver_stderr.absolute()}')
elif P is None or R is None or U_mag is None:
raise click.ClickException(f'Error during solver execution. Electrical parameters could not be calculated. See log files for details:\n{solver_stdout.absolute()}\n{solver_stderr.absolute()}')
V = math.sqrt(P*R)
I = math.sqrt(P/R)
L = 2*U_mag / (I**2)
assert math.isclose(V, 1.0, abs_tol=1e-3)
print(f'Total magnetic field energy: {format_si(U_mag, "J")}')
print(f'Reference coil current: {format_si(I, "Ω")}')
print(f'Coil resistance calculated by solver: {format_si(R, "Ω")}')
print(f'Inductance calucated from field: {format_si(L, "H")}')
@cli.command()
@click.option('-r', '--reference-field', type=float, required=True)
@click.option('-d', '--sim-dir', type=click.Path(dir_okay=True, file_okay=False, path_type=Path))
@click.argument('mesh_file', type=click.Path(dir_okay=False, path_type=Path))
def mutual_inductance(mesh_file, sim_dir, reference_field):
physical = dict(enumerate_mesh_bodies(mesh_file))
if sim_dir is not None:
sim_dir = Path(sim_dir)
sim_dir.mkdir(exist_ok=True)
sim = elmer.load_simulation('3D_steady', 'coil_mag_sim.yml')
mesh_dir = '.'
mesh_fn = 'mesh'
sim.header['Mesh DB'] = f'"{mesh_dir}" "{mesh_fn}"'
sim.constants.update({
'Permittivity of Vacuum': str(constants.epsilon_0),
'Gravity(4)': f'0 -1 0 {constants.g}',
'Boltzmann Constant': str(constants.Boltzmann),
'Unit Charge': str(constants.elementary_charge)})
air = elmer.load_material('air', sim, 'coil_mag_materials.yml')
fr4 = elmer.load_material('fr4', sim, 'coil_mag_materials.yml')
copper = elmer.load_material('copper', sim, 'coil_mag_materials.yml')
solver_current = elmer.load_solver('Static_Current_Conduction', sim, 'coil_mag_solvers.yml')
solver_magdyn = elmer.load_solver('Magneto_Dynamics', sim, 'coil_mag_solvers.yml')
solver_magdyn_calc = elmer.load_solver('Magneto_Dynamics_Calculations', sim, 'coil_mag_solvers.yml')
copper_eqn = elmer.Equation(sim, 'copperEqn', [solver_current, solver_magdyn, solver_magdyn_calc])
air_eqn = elmer.Equation(sim, 'airEqn', [solver_magdyn, solver_magdyn_calc])
bdy_trace1 = elmer.Body(sim, 'trace1', [physical['trace1'][1]])
bdy_trace1.material = copper
bdy_trace1.equation = copper_eqn
bdy_trace2 = elmer.Body(sim, 'trace2', [physical['trace2'][1]])
bdy_trace2.material = copper
bdy_trace2.equation = copper_eqn
bdy_sub1 = elmer.Body(sim, 'substrate1', [physical['substrate1'][1]])
bdy_sub1.material = fr4
bdy_sub1.equation = air_eqn
bdy_sub2 = elmer.Body(sim, 'substrate2', [physical['substrate2'][1]])
bdy_sub2.material = fr4
bdy_sub2.equation = air_eqn
bdy_ab = elmer.Body(sim, 'airbox', [physical['airbox'][1]])
bdy_ab.material = air
bdy_ab.equation = air_eqn
bdy_if_top1 = elmer.Body(sim, 'interface_top1', [physical['interface_top1'][1]])
bdy_if_top1.material = copper
bdy_if_top1.equation = copper_eqn
bdy_if_bottom1 = elmer.Body(sim, 'interface_bottom1', [physical['interface_bottom1'][1]])
bdy_if_bottom1.material = copper
bdy_if_bottom1.equation = copper_eqn
bdy_if_top2 = elmer.Body(sim, 'interface_top2', [physical['interface_top2'][1]])
bdy_if_top2.material = copper
bdy_if_top2.equation = copper_eqn
bdy_if_bottom2 = elmer.Body(sim, 'interface_bottom2', [physical['interface_bottom2'][1]])
bdy_if_bottom2.material = copper
bdy_if_bottom2.equation = copper_eqn
potential_force = elmer.BodyForce(sim, 'electric_potential', {'Electric Potential': 'Equals "Potential"'})
bdy_trace1.body_force = potential_force
bdy_trace2.body_force = potential_force
# boundaries
boundary_airbox = elmer.Boundary(sim, 'FarField', [physical['airbox_surface'][1]])
boundary_airbox.data['Electric Infinity BC'] = 'True'
boundary_vplus1 = elmer.Boundary(sim, 'Vplus1', [physical['interface_top1'][1]])
boundary_vplus1.data['Potential'] = 1.0
boundary_vplus1.data['Save Scalars'] = True
boundary_vminus1 = elmer.Boundary(sim, 'Vminus1', [physical['interface_bottom1'][1]])
boundary_vminus1.data['Potential'] = 0.0
boundary_vplus2 = elmer.Boundary(sim, 'Vplus2', [physical['interface_top2'][1]])
boundary_vplus2.data['Potential'] = 1.0
boundary_vplus2.data['Save Scalars'] = True
boundary_vminus2 = elmer.Boundary(sim, 'Vminus2', [physical['interface_bottom2'][1]])
boundary_vminus2.data['Potential'] = 0.0
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = sim_dir if sim_dir else Path(tmpdir)
sim.write_startinfo(tmpdir)
sim.write_sif(tmpdir)
# Convert mesh from gmsh to elemer formats. Also scale it from 1 unit = 1 mm to 1 unit = 1 m (SI units)
elmer_grid(mesh_file.absolute(), 'mesh', cwd=tmpdir, scale=[1e-3, 1e-3, 1e-3],
stdout_log=(tmpdir / 'ElmerGrid_stdout.log'),
stderr_log=(tmpdir / 'ElmerGrid_stderr.log'))
solver_stdout, solver_stderr = (tmpdir / 'ElmerSolver_stdout.log'), (tmpdir / 'ElmerSolver_stderr.log')
res = elmer_solver(tmpdir,
stdout_log=solver_stdout,
stderr_log=solver_stderr)
P, R, U_mag = None, None, None
solver_error = False
for l in res.stdout.splitlines():
if (m := re.fullmatch(r'StatCurrentSolve:\s*Total Heating Power\s*:\s*([0-9.+-Ee]+)\s*', l)):
P = float(m.group(1))
elif (m := re.fullmatch(r'StatCurrentSolve:\s*Effective Resistance\s*:\s*([0-9.+-Ee]+)\s*', l)):
R = float(m.group(1))
elif (m := re.fullmatch(r'MagnetoDynamicsCalcFields:\s*ElectroMagnetic Field Energy\s*:\s*([0-9.+-Ee]+)\s*', l)):
U_mag = float(m.group(1))
elif re.fullmatch(r'IterSolve: Linear iteration did not converge to tolerance', l):
solver_error = True
if solver_error:
raise click.ClickException(f'Error: One of the solvers did not converge. See log files for details:\n{solver_stdout.absolute()}\n{solver_stderr.absolute()}')
elif P is None or R is None or U_mag is None:
raise click.ClickException(f'Error during solver execution. Electrical parameters could not be calculated. See log files for details:\n{solver_stdout.absolute()}\n{solver_stderr.absolute()}')
V = math.sqrt(P*R)
I = math.sqrt(P/R)
Lm = (U_mag - 2*reference_field) / ((I/2)**2)
assert math.isclose(V, 1.0, abs_tol=1e-3)
print(f'Mutual inductance calucated from field: {format_si(Lm, "H")}')
@cli.command()
@click.option('-d', '--sim-dir', type=click.Path(dir_okay=True, file_okay=False, path_type=Path))
@click.argument('mesh_file', type=click.Path(dir_okay=False, path_type=Path))
def self_capacitance(mesh_file, sim_dir):
physical = dict(enumerate_mesh_bodies(mesh_file))
if sim_dir is not None:
sim_dir = Path(sim_dir)
sim_dir.mkdir(exist_ok=True)
sim = elmer.load_simulation('3D_steady', 'self_capacitance_sim.yml')
mesh_dir = '.'
mesh_fn = 'mesh'
sim.header['Mesh DB'] = f'"{mesh_dir}" "{mesh_fn}"'
sim.constants.update({
'Permittivity of Vacuum': str(constants.epsilon_0),
'Gravity(4)': f'0 -1 0 {constants.g}',
'Boltzmann Constant': str(constants.Boltzmann),
'Unit Charge': str(constants.elementary_charge)})
air = elmer.load_material('air', sim, 'coil_mag_materials.yml')
fr4 = elmer.load_material('fr4', sim, 'coil_mag_materials.yml')
copper = elmer.load_material('copper', sim, 'coil_mag_materials.yml')
solver_current = elmer.load_solver('StaticCurrent', sim, 'self_capacitance_solvers.yml')
solver_estat = elmer.load_solver('Electrostatics', sim, 'self_capacitance_solvers.yml')
copper_eqn = elmer.Equation(sim, 'copperEqn', [solver_current, solver_estat])
air_eqn = elmer.Equation(sim, 'airEqn', [solver_estat])
bdy_trace = elmer.Body(sim, 'trace', [physical['trace'][1]])
bdy_trace.material = copper
bdy_trace.equation = copper_eqn
bdy_sub = elmer.Body(sim, 'substrate', [physical['substrate'][1]])
bdy_sub.material = fr4
bdy_sub.equation = air_eqn
bdy_ab = elmer.Body(sim, 'airbox', [physical['airbox'][1]])
bdy_ab.material = air
bdy_ab.equation = air_eqn
bdy_if_top = elmer.Body(sim, 'interface_top', [physical['interface_top'][1]])
bdy_if_top.material = copper
bdy_if_top.equation = copper_eqn
bdy_if_bottom = elmer.Body(sim, 'interface_bottom', [physical['interface_bottom'][1]])
bdy_if_bottom.material = copper
bdy_if_bottom.equation = copper_eqn
potential_force = elmer.BodyForce(sim, 'electric_potential', {'Potential': 'Equals "PotentialStat"'})
bdy_trace.body_force = potential_force
# boundaries
boundary_airbox = elmer.Boundary(sim, 'FarField', [physical['airbox_surface'][1]])
boundary_airbox.data['Electric Infinity BC'] = 'True'
boundary_vplus = elmer.Boundary(sim, 'Vplus', [physical['interface_top'][1]])
boundary_vplus.data['PotentialStat'] = 'Real 1.0'
boundary_vplus.data['Save Scalars'] = True
boundary_vminus = elmer.Boundary(sim, 'Vminus', [physical['interface_bottom'][1]])
boundary_vminus.data['PotentialStat'] = 'Real 0.0'
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = sim_dir if sim_dir else Path(tmpdir)
sim.write_startinfo(tmpdir)
sim.write_sif(tmpdir)
# Convert mesh from gmsh to elemer formats. Also scale it from 1 unit = 1 mm to 1 unit = 1 m (SI units)
elmer_grid(mesh_file.absolute(), 'mesh', cwd=tmpdir, scale=[1e-3, 1e-3, 1e-3],
stdout_log=(tmpdir / 'ElmerGrid_stdout.log'),
stderr_log=(tmpdir / 'ElmerGrid_stderr.log'))
solver_stdout, solver_stderr = (tmpdir / 'ElmerSolver_stdout.log'), (tmpdir / 'ElmerSolver_stderr.log')
res = elmer_solver(tmpdir,
stdout_log=solver_stdout,
stderr_log=solver_stderr)
C, U_elec = None, None
solver_error = False
for l in res.stdout.splitlines():
if (m := re.fullmatch(r'StatElecSolve:\s*Tot. Electric Energy\s*:\s*([0-9.+-Ee]+)\s*', l)):
U_elec = float(m.group(1))
elif (m := re.fullmatch(r'StatElecSolve:\s*Capacitance\s*:\s*([0-9.+-Ee]+)\s*', l)):
C = float(m.group(1))
elif re.fullmatch(r'IterSolve: Linear iteration did not converge to tolerance', l):
solver_error = True
if solver_error:
raise click.ClickException(f'Error: One of the solvers did not converge. See log files for details:\n{solver_stdout.absolute()}\n{solver_stderr.absolute()}')
elif C is None or U_elec is None:
raise click.ClickException(f'Error during solver execution. Electrical parameters could not be calculated. See log files for details:\n{solver_stdout.absolute()}\n{solver_stderr.absolute()}')
print(f'Total electric field energy: {format_si(U_elec, "J")}')
print(f'Total parasitic capacitance: {format_si(C, "F")}')
@cli.command()
@click.option('-d', '--sim-dir', type=click.Path(dir_okay=True, file_okay=False, path_type=Path))
@click.option('--capacitance-matrix-file', type=click.Path(dir_okay=False, exists=True))
@click.option('--total-inductance', type=float, required=True, help='Total inductance in Henry')
@click.option('--total-resistance', type=float, required=True, help='Total resistance in Ohm')
@click.option('--plot-out', type=click.Path(dir_okay=False, writable=True), help='Optional SVG plot output file')
def resonance(sim_dir, capacitance_matrix_file, total_inductance, total_resistance, plot_out):
import PySpice.Unit
from PySpice.Spice.Library import SpiceLibrary
from PySpice.Spice.Netlist import Circuit
from PySpice.Plot.BodeDiagram import bode_diagram
import scipy.signal
from matplotlib import pyplot as plt
capacitance_matrix = np.loadtxt(capacitance_matrix_file)
num_elements = capacitance_matrix.shape[0]
circ = Circuit('LC ladder parasitic sim')
inputs = 'Vplus', circ.gnd
coil_in = 'coil_in'
Rtest = circ.R('Rtest', inputs[0], coil_in, 50@PySpice.Unit.u_Ohm)
intermediate_nodes = [f'intermediate{i}' for i in range(num_elements-1)]
inductor_nodes = [(a, b) for a, b in zip([coil_in, *intermediate_nodes], [*intermediate_nodes, inputs[1]])]
inductor_midpoints = [f'midpoint{i}' for i in range(num_elements)]
circ.SinusoidalVoltageSource('input', inputs[0], inputs[1], amplitude=1@PySpice.Unit.u_V)
for i, ((a, b), m) in enumerate(zip(inductor_nodes, inductor_midpoints)):
L = total_inductance / num_elements / 2
R = total_resistance / num_elements / 2
circ.L(f'L{i}A', a, f'R{i}A1', L@PySpice.Unit.u_H)
circ.R(f'R{i}A', f'R{i}A1', m, R@PySpice.Unit.u_Ohm)
circ.R(f'R{i}B', m, f'R{i}B1', R@PySpice.Unit.u_Ohm)
circ.L(f'L{i}B', f'R{i}B1', b, L@PySpice.Unit.u_H)
for i in range(num_elements):
for j in range(i):
circ.C(f'C{i}_{j}', inductor_midpoints[i], inductor_midpoints[j], capacitance_matrix[i, j]@PySpice.Unit.u_F)
sim = circ.simulator(temperature=25, nominal_temperature=25)
ana = sim.ac(start_frequency=10@PySpice.Unit.u_kHz, stop_frequency=1000@PySpice.Unit.u_MHz, number_of_points=1000, variation='dec')
figure, axs = plt.subplots(2, figsize=(20, 10), sharex=True)
freq = ana.frequency
gain = 20*np.log10(np.absolute(ana.coil_in))
peaks, peak_props = scipy.signal.find_peaks(-gain, height=20)
for peak in peaks[:3]:
print(f'Resonance at {float(freq[peak])/1e6:.3f} MHz')
if plot_out:
plt.title("Bode Diagram of a Low-Pass RC Filter")
bode_diagram(axes=axs,
frequency=freq,
gain=gain,
phase=np.angle(ana.coil_in, deg=False),
linestyle='-',
)
for peak in peaks[:3]:
for ax in axs:
ax.axvline(float(freq[peak]), color='red', alpha=0.5)
plt.tight_layout()
plt.savefig(plot_out)
if __name__ == '__main__':
cli()

Wyświetl plik

@ -1,38 +0,0 @@
air:
Density: 1.1885 # 20°C
Electric Conductivity: 0.0
Heat Capacity: 1006.4 # 20°C
Heat Conductivity: 0.025873 # 20°C
Relative Permeability: 1
Relative Permittivity: 1
ro4003c:
Density: 1790 # 23°C
Relative Permeability: 1
Relative Permittivity: 3.55
fr4:
Density: 1850 # 23°C
Relative Permeability: 1
Relative Permittivity: 4.4
Heat Conductivity: 0.81 # in-plane
ideal:
Relative Permittivity: 1
copper:
Density: 8960.0 # 0°C
Electric Conductivity: 59600000 # 20°C
Emissivity: 0.012 # 327°C
Heat Capacity: 415.0 # 200°C
Heat Conductivity: 401.0 # 0°C
Relative Permeability: 1
Relative Permittivity: 1
steel_1.4541:
Density: 7900.0 # 20°C
Electric Conductivity: 1370
Emissivity: 0.111 # 200°C
Heat Capacity: 470.0 # 20°C
Heat Conductivity: 15.0 # 20°C
Relative Permeability: 1
Relative Permittivity: 1
water:
Density: 1000.0
Heat Capacity: 4182.0
Heat Conductivity: 0.6

Wyświetl plik

@ -1,50 +0,0 @@
2D_steady:
Max Output Level: 4
Coordinate System: Cartesian 2D
Simulation Type: Steady state
Steady State Max Iterations: 10
3D_steady:
Max Output Level: 5
Coordinate System: Cartesian
Coordinate Mapping(3): 1 2 3
Simulation Type: Steady state
Steady State Max Iterations: 1
Output Intervals: 1
Timestepping Method: BDF
BDF Order: 1
Solver Input File: case.sif
Post File: case.vtu
Output File: case.result
2D_transient:
Max Output Level: 4
Coordinate System: Cartesian 2D
Simulation Type: Transient
Steady State Max Iterations: 10
Output File: case.result
Output Intervals: 10
Timestep Sizes: 0.1
Timestep Intervals: 100
Timestepping Method: BDF
BDF Order: 1
axi-symmetric_steady:
Max Output Level: 4
Coordinate System: Axi Symmetric
Simulation Type: Steady state
Steady State Max Iterations: 10
Output File: case.result
axi-symmetric_transient:
Max Output Level: 4
Coordinate System: Axi Symmetric
Simulation Type: Transient
Steady State Max Iterations: 10
Output File: case.result
Output Intervals: 10
Timestep Sizes: 0.1
Timestep Intervals: 100
Timestepping Method: BDF
BDF Order: 1

Wyświetl plik

@ -1,203 +0,0 @@
Electrostatics_Capacitance:
Equation: Electrostatics
Calculate Electric Field: True
Calculate Capacitance Matrix: True
Capacitance Matrix Filename: capacitance.txt
Procedure: '"StatElecSolve" "StatElecSolver"'
Variable: Potential
Calculate Electric Energy: True
Exec Solver: Always
Stabilize: True
Bubbles: False
Lumped Mass Matrix: False
Optimize Bandwidth: True
Steady State Convergence Tolerance: 1.0e-5
Nonlinear System Convergence Tolerance: 1.0e-7
Nonlinear System Max Iterations: 20
Nonlinear System Newton After Iterations: 3
Nonlinear System Newton After Tolerance: 1.0e-3
Nonlinear System Relaxation Factor: 1
Linear System Solver: Iterative
Linear System Iterative Method: BiCGStab
Linear System Max Iterations: 500
Linear System Convergence Tolerance: 1.0e-10
BiCGstabl polynomial degree: 2
Linear System Preconditioning: ILU0
Linear System ILUT Tolerance: 1.0e-3
Linear System Abort Not Converged: False
Linear System Residual Output: 10
Linear System Precondition Recompute: 1
Electrostatics:
Equation: Electrostatics
Calculate Electric Field: True
Procedure: '"StatElecSolve" "StatElecSolver"'
Variable: Potential
Calculate Electric Energy: True
Exec Solver: Always
Stabilize: True
Bubbles: False
Lumped Mass Matrix: False
Optimize Bandwidth: True
Steady State Convergence Tolerance: 1.0e-5
Nonlinear System Convergence Tolerance: 1.0e-7
Nonlinear System Max Iterations: 20
Nonlinear System Newton After Iterations: 3
Nonlinear System Newton After Tolerance: 1.0e-3
Nonlinear System Relaxation Factor: 1
Linear System Solver: Iterative
Linear System Iterative Method: BiCGStab
Linear System Max Iterations: 500
Linear System Convergence Tolerance: 1.0e-10
BiCGstabl polynomial degree: 2
Linear System Preconditioning: ILU0
Linear System ILUT Tolerance: 1.0e-3
Linear System Abort Not Converged: False
Linear System Residual Output: 10
Linear System Precondition Recompute: 1
ThermoElectricSolver:
Equation: ThermoElectric
Procedure: '"ThermoElectricSolver" "ThermoElectricSolver"'
Variable: POT[Temperature:1 Potential:1]
Element: '"p:1"'
Calculate Loads: True
Exec Solver: Always
Nonlinear System Convergence Tolerance: 1.0e-6
Nonlinear System Max Iterations: 100
Nonlinear System Newton After Iterations : 1
Nonlinear System Newton After Tolerance: 1e-9
Linear System Solver: '"Iterative"'
Linear System Iterative Method: BicgstabL
Bicgstabl Polynomial Degree: 2
Linear System Max Iterations: 200
Linear System Residual Output: 40
Linear System Preconditioning: Ilu
Linear System Convergence Tolerance: 1e-8
Steady State Convergence Tolerance: 1e-6
HeatSolver:
Equation: HeatSolver
Procedure: '"HeatSolve" "HeatSolver"'
Variable: '"Temperature"'
Variable Dofs: 1
Calculate Loads: True
Exec Solver: Always
Nonlinear System Convergence Tolerance: 1.0e-6
Nonlinear System Max Iterations: 1000
Nonlinear System Relaxation Factor: 0.7
Steady State Convergence Tolerance: 1.0e-6
Stabilize: True # Necessary in convection-dominated systems
Optimize Bandwidth: True
Linear System Solver: Iterative
Linear System Iterative Method: BiCGStab
Linear System Max Iterations: 1000
Linear System Preconditioning: ILU
Linear System Precondition Recompute: 1
Linear System Convergence Tolerance: 1.0e-8
Linear System Abort Not Converged: True
Linear System Residual Output: 1
Smart Heater Control After Tolerance: 1.0e-4
MagnetoDynamics2DHarmonic:
Equation: MagnetoDynamics2DHarmonic
Procedure: '"MagnetoDynamics2D" "MagnetoDynamics2DHarmonic"'
Variable: 'Potential[Potential Re:1 Potential Im:1]'
Variable Dofs: 2
Exec Solver: Always
Nonlinear System Convergence Tolerance: 1.0e-6
Nonlinear System Max Iterations: 1000
Nonlinear System Relaxation Factor: 0.7
Steady State Convergence Tolerance: 1.0e-6
Optimize Bandwidth: True
Linear System Solver: Iterative
Linear System Iterative Method: BiCGStab
Linear System Max Iterations: 1000
Linear System Preconditioning: ILU
Linear System Precondition Recompute: 1
Linear System Convergence Tolerance: 1.0e-8
Linear System Abort Not Converged: True
Linear System Residual Output: 1
MagnetoDynamicsCalcFields:
Equation: MagnetoDynamicsCalcFields
Procedure: '"MagnetoDynamics" "MagnetoDynamicsCalcFields"'
Potential Variable: Potential
Angular Frequency: 8.48e4
Calculate Joule Heating: True
Calculate Magnetic Field Strength: True
Calculate Electric Field: True
Exec Solver: Always
Calculate Nodal Fields: Logical False
Calculate Elemental Fields: Logical True
StatMagSolver:
Equation: StatMagSolver
Procedure: '"StatMagSolve" "StatMagSolver"'
Variable: Potential
Variable DOFs: 2
Calculate Joule Heating: 'Logical True'
Calculate Magnetic Flux: 'Logical True'
Exec Solver: Always
Nonlinear System Convergence Tolerance: 1.0e-6
Nonlinear System Max Iterations: 1000
Nonlinear System Relaxation Factor: 0.7
Steady State Convergence Tolerance: 1.0e-6
Optimize Bandwidth: True
Linear System Solver: Iterative
Linear System Iterative Method: BiCGStab
Linear System Max Iterations: 1000
Linear System Preconditioning: ILU
Linear System Precondition Recompute: 1
Linear System Convergence Tolerance: 1.0e-8
Linear System Abort Not Converged: True
Linear System Residual Output: 1
SaveMaterials:
Exec Solver: 'After timestep'
Procedure: 'File "SaveData" "SaveMaterials"'
Parameter 1: 'String "Heat Conductivity"'
ResultOutputSolver:
Exec Solver: 'After timestep'
Equation: ResultOutputSolver
Procedure: '"ResultOutputSolve" "ResultOutputSolver"'
VTU Format: True
Save Geometry Ids: 'Logical True'
FluxSolver:
Exec Solver: 'After timestep'
Equation: 'Flux Solver'
Procedure: '"FluxSolver" "FluxSolver"'
Calculate Grad: 'Logical True'
Calculate Flux: 'Logical True'
Target Variable: 'String "Temperature"'
Flux Coefficient: 'String "Heat Conductivity"'
Exec Solver: Always
Nonlinear System Convergence Tolerance: 1.0e-6
Nonlinear System Max Iterations: 1000
Nonlinear System Relaxation Factor: 0.7
Steady State Convergence Tolerance: 1.0e-6
Optimize Bandwidth: True
Linear System Solver: Iterative
Linear System Iterative Method: BiCGStab
Linear System Max Iterations: 1000
Linear System Preconditioning: ILU
Linear System Precondition Recompute: 1
Linear System Convergence Tolerance: 1.0e-8
Linear System Abort Not Converged: True
Linear System Residual Output: 1
SaveScalars:
Exec Solver: 'After timestep'
Equation: SaveScalars
Procedure: '"SaveData" "SaveScalars"'
Filename: '"boundary_scalars.dat"'
Output Directory: './results'
Operator 1: 'boundary sum'
Variable 1: 'Temperature Loads'
Operator 2: 'diffusive flux'
Variable 2: Temperature
Coefficient 2: 'Heat Conductivity'
SaveLine:
Exec Solver: 'After timestep'
Equation: '"SaveLine"'
Procedure: '"SaveData" "SaveLine"'
Filename: '"boundary_lines.dat"'
Output Directory: './results'
Variable 1: Temperature Loads
SteadyPhaseChange:
Equation: SteadyPhaseChange
Variable: '"Phase Surface"'
Procedure: '"SteadyPhaseChange" "SteadyPhaseChange"'
Internal Mesh Movement: 'Logical True'

Wyświetl plik

@ -1,483 +0,0 @@
#!/usr/bin/env python3
import math
import hashlib
import re
import itertools
import datetime
import tempfile
import subprocess
import sqlite3
import json
from pathlib import Path
import tqdm
import gerbonara.cad.kicad.pcb as pcb
import gerbonara.cad.kicad.footprints as fp
import gerbonara.cad.primitives as cad_pr
import gerbonara.cad.kicad.graphical_primitives as kc_gr
cols = 5
rows = 5
coil_specs = [
{'n': 1, 's': True, 't': 1, 'c': 0.20, 'w': 5.00, 'd': 3.00, 'v': 5.00},
{'n': 2, 's': True, 't': 1, 'c': 0.20, 'w': 3.00, 'd': 1.50, 'v': 3.00},
{'n': 3, 's': True, 't': 1, 'c': 0.20, 'w': 1.50, 'd': 1.20, 'v': 2.00},
{'n': 5, 's': True, 't': 1, 'c': 0.20, 'w': 0.80, 'd': 0.40, 'v': 0.80},
{'n': 10, 's': True, 't': 1, 'c': 0.20, 'w': 0.50, 'd': 0.30, 'v': 0.60},
{'n': 25, 's': True, 't': 1, 'c': 0.15, 'w': 0.25, 'd': 0.30, 'v': 0.60},
{'n': 1, 's': False, 't': 3, 'c': 0.20, 'w': 5.00, 'd': 3.00, 'v': 5.00},
{'n': 2, 's': False, 't': 1, 'c': 0.20, 'w': 3.00, 'd': 1.50, 'v': 3.00},
{'n': 3, 's': False, 't': 1, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 2.00},
{'n': 5, 's': False, 't': 1, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 0.80},
{'n': 10, 's': False, 't': 1, 'c': 0.20, 'w': 1.50, 'd': 0.80, 'v': 0.60},
{'n': 25, 's': False, 't': 1, 'c': 0.15, 'w': 0.50, 'd': 0.30, 'v': 0.60},
{'n': 1, 's': False, 't': 4, 'c': 0.20, 'w': 5.00, 'd': 3.00, 'v': 5.00},
{'n': 2, 's': False, 't': 3, 'c': 0.20, 'w': 3.00, 'd': 1.50, 'v': 3.00},
{'n': 3, 's': False, 't': 4, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 2.00},
{'n': 5, 's': False, 't': 3, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 0.80},
{'n': 10, 's': False, 't': 3, 'c': 0.20, 'w': 1.50, 'd': 0.80, 'v': 0.60},
{'n': 25, 's': False, 't': 3, 'c': 0.15, 'w': 0.50, 'd': 0.30, 'v': 0.60},
{'n': 1, 's': False, 't': 5, 'c': 0.20, 'w': 5.00, 'd': 3.00, 'v': 5.00},
{'n': 2, 's': False, 't': 5, 'c': 0.20, 'w': 3.00, 'd': 1.50, 'v': 3.00},
{'n': 3, 's': False, 't': 4, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 2.00},
{'n': 5, 's': False, 't': 7, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 0.80},
{'n': 10, 's': False, 't': 7, 'c': 0.20, 'w': 1.50, 'd': 0.80, 'v': 0.60},
{'n': 25, 's': False, 't': 13, 'c': 0.15, 'w': 0.50, 'd': 0.30, 'v': 0.60},
{'n': 25, 's': False, 't': 37, 'c': 0.15, 'w': 0.50, 'd': 0.30, 'v': 0.60},
]
cachedir = Path('/tmp/coil_test_cache')
version_string = 'v1.0'
coil_border = 7 # mm
cut_gap = 8 # mm
tooling_border = 10 # mm
vscore_extra = 10 # mm
mouse_bite_width = 8 # mm
mouse_bite_yoff = 0.175
mouse_bite_hole_dia = 0.7
mouse_bite_hole_spacing = 0.7
hole_offset = 5
hole_dia = 3.2
coil_dia = 35 # mm
coil_inner_dia = 15 # mm
board_thickness = 0.80 # mm
pad_offset = 2 # mm
pad_dia = 2.0 # mm
pad_length = 3.5 # mm
pad_drill = 1.1 # mm
pad_pitch = 2.54 # mm
join_trace_w = 0.150 # mm
do_v_cuts = False
do_mouse_bites = False
do_cut_gaps = False
db = sqlite3.connect('coil_parameters.sqlite3')
db.execute('CREATE TABLE IF NOT EXISTS runs (run_id INTEGER PRIMARY KEY, timestamp TEXT, version TEXT)')
db.execute('CREATE TABLE IF NOT EXISTS coils (coil_id INTEGER PRIMARY KEY, run_id INTEGER, FOREIGN KEY (run_id) REFERENCES runs(run_id))')
db.execute('CREATE TABLE IF NOT EXISTS results (result_id INTEGER PRIMARY KEY, coil_id INTEGER, key TEXT, value TEXT, FOREIGN KEY (coil_id) REFERENCES coils(coil_id))')
cur = db.cursor()
cur.execute('INSERT INTO runs(timestamp, version) VALUES (datetime("now"), ?)', (version_string,))
run_id = cur.lastrowid
db.commit()
tile_width = tile_height = coil_dia + 2*coil_border
coil_pitch_v = tile_width + cut_gap
coil_pitch_h = tile_height + cut_gap
total_width = coil_pitch_h*cols + 2*tooling_border + cut_gap
total_height = coil_pitch_v*rows + 2*tooling_border + cut_gap
drawing_text_size = 2.0
print(f'Calculated board size: {total_width:.2f} * {total_height:.2f} mm')
print(f'Tile size: {tile_height:.2f} * {tile_height:.2f} mm')
x0, y0 = 100, 100
xy = pcb.XYCoord
b = pcb.Board.empty_board(page=pcb.PageSettings(page_format='A2'))
b.add(kc_gr.Rectangle(xy(x0, y0), xy(x0+total_width, y0+total_height), layer='Edge.Cuts', stroke=pcb.Stroke(width=0.15)))
def do_line(x0, y0, x1, y1, off_x=0, off_y=0):
b.add(kc_gr.Line(xy(x0+off_x, y0+off_y),
xy(x1+off_x, y1+off_y),
layer='Edge.Cuts', stroke=pcb.Stroke(width=0.15)))
if do_v_cuts:
for y in range(rows):
for off_y in [0, tile_height]:
y_pos = y0 + tooling_border + cut_gap + off_y + y*coil_pitch_v
do_line(x0 - vscore_extra, y_pos, x0 + total_width + vscore_extra, y_pos)
b.add(kc_gr.Text(text='V-score',
at=pcb.AtPos(x0 + total_width + vscore_extra + drawing_text_size/2, y_pos, 0),
layer=kc_gr.TextLayer('Edge.Cuts'),
effects=pcb.TextEffect(
font=pcb.FontSpec(size=xy(drawing_text_size, drawing_text_size),
thickness=drawing_text_size/10),
justify=pcb.Justify(h=pcb.Atom.left))))
for x in range(cols):
for off_x in [0, tile_width]:
x_pos = x0 + tooling_border + cut_gap + off_x + x*coil_pitch_h
do_line(x_pos, y0 - vscore_extra, x_pos, y0 + total_height + vscore_extra)
b.add(kc_gr.Text(text='V-score',
at=pcb.AtPos(x_pos, y0 + total_height + vscore_extra + drawing_text_size/2, 90),
layer=kc_gr.TextLayer('Edge.Cuts'),
effects=pcb.TextEffect(
font=pcb.FontSpec(size=xy(drawing_text_size, drawing_text_size),
thickness=drawing_text_size/10),
justify=pcb.Justify(h=pcb.Atom.right))))
def draw_corner(x0, y0, spokes):
right, top, left, bottom = [True if c.lower() in 'y1' else False for c in spokes]
l = (tile_width - mouse_bite_width)/2 - cut_gap/2
if right:
do_line(cut_gap/2, -cut_gap/2, cut_gap/2 + l, -cut_gap/2, x0, y0)
do_line(cut_gap/2, cut_gap/2, cut_gap/2 + l, cut_gap/2, x0, y0)
b.add(kc_gr.Arc(start=xy(x0+cut_gap/2+l, y0-cut_gap/2),
end=xy(x0+cut_gap/2+l, y0+cut_gap/2),
center=xy(x0+cut_gap/2+l, y0),
layer='Edge.Cuts',
stroke=pcb.Stroke(width=0.15)))
else:
do_line(cut_gap/2, -cut_gap/2, cut_gap/2, cut_gap/2, x0, y0)
if left:
do_line(-cut_gap/2, -cut_gap/2, -cut_gap/2 - l, -cut_gap/2, x0, y0)
do_line(-cut_gap/2, cut_gap/2, -cut_gap/2 - l, cut_gap/2, x0, y0)
b.add(kc_gr.Arc(end=xy(x0-cut_gap/2-l, y0-cut_gap/2),
start=xy(x0-cut_gap/2-l, y0+cut_gap/2),
center=xy(x0-cut_gap/2-l, y0),
layer='Edge.Cuts',
stroke=pcb.Stroke(width=0.15)))
else:
do_line(-cut_gap/2, -cut_gap/2, -cut_gap/2, cut_gap/2, x0, y0)
if bottom:
do_line(-cut_gap/2, cut_gap/2, -cut_gap/2, cut_gap/2 + l, x0, y0)
do_line(cut_gap/2, cut_gap/2, cut_gap/2, cut_gap/2 + l, x0, y0)
b.add(kc_gr.Arc(end=xy(x0-cut_gap/2, y0+cut_gap/2+l),
start=xy(x0+cut_gap/2, y0+cut_gap/2+l),
center=xy(x0, y0+cut_gap/2+l),
layer='Edge.Cuts',
stroke=pcb.Stroke(width=0.15)))
else:
do_line(-cut_gap/2, cut_gap/2, cut_gap/2, cut_gap/2, x0, y0)
if top:
do_line(-cut_gap/2, -cut_gap/2, -cut_gap/2, -cut_gap/2 - l, x0, y0)
do_line(cut_gap/2, -cut_gap/2, cut_gap/2, -cut_gap/2 - l, x0, y0)
b.add(kc_gr.Arc(start=xy(x0-cut_gap/2, y0-cut_gap/2-l),
end=xy(x0+cut_gap/2, y0-cut_gap/2-l),
center=xy(x0, y0-cut_gap/2-l),
layer='Edge.Cuts',
stroke=pcb.Stroke(width=0.15)))
else:
do_line(-cut_gap/2, -cut_gap/2, cut_gap/2, -cut_gap/2, x0, y0)
def make_mouse_bite(x, y, rot=0, width=mouse_bite_width, hole_dia=mouse_bite_hole_dia, hole_spacing=mouse_bite_hole_spacing, **kwargs):
pitch = hole_dia + hole_spacing
num_holes = int(math.floor((width - hole_dia) / pitch)) + 1
actual_spacing = (width - num_holes*hole_dia) / (num_holes - 1)
pitch = hole_dia + actual_spacing
f = fp.Footprint(name='mouse_bite', _version=None, generator=None, at=fp.AtPos(x, y, rot), **kwargs)
for i in range(num_holes):
f.pads.append(fp.Pad(
number='1',
type=fp.Atom.np_thru_hole,
shape=fp.Atom.circle,
at=fp.AtPos(-width/2 + i*pitch + hole_dia/2, 0, 0),
size=xy(hole_dia, hole_dia),
drill=fp.Drill(diameter=hole_dia),
footprint=f))
return f
def make_hole(x, y, dia, **kwargs):
f = fp.Footprint(name='hole', _version=None, generator=None, at=fp.AtPos(x, y, 0), **kwargs)
f.pads.append(fp.Pad(
number='1',
type=fp.Atom.np_thru_hole,
shape=fp.Atom.circle,
at=fp.AtPos(0, 0, 0),
size=xy(dia, dia),
drill=fp.Drill(diameter=dia),
footprint=f))
return f
def make_pads(x, y, rot, n, pad_dia, pad_length, drill, pitch, **kwargs):
f = fp.Footprint(name=f'conn_gen_01x{n}', _version=None, generator=None, at=fp.AtPos(x, y, rot), **kwargs)
for i in range(n):
f.pads.append(fp.Pad(
number=str(i+1),
type=fp.Atom.thru_hole,
shape=fp.Atom.oval,
at=fp.AtPos(-pitch*(n-1)/2 + i*pitch, 0, rot),
size=xy(pad_dia, pad_length),
drill=fp.Drill(diameter=drill),
footprint=f))
return f
corner_x0 = x0 + tooling_border + cut_gap/2
corner_y0 = y0 + tooling_border + cut_gap/2
corner_x1 = x0 + total_width - tooling_border - cut_gap/2
corner_y1 = y0 + total_height - tooling_border - cut_gap/2
if do_cut_gaps:
# Corners
draw_corner(corner_x0, corner_y0, 'YNNY')
draw_corner(corner_x0, corner_y1, 'YYNN')
draw_corner(corner_x1, corner_y0, 'NNYY')
draw_corner(corner_x1, corner_y1, 'NYYN')
# Top / bottom T junctions
for x in range(1, cols):
draw_corner(corner_x0 + x*coil_pitch_h, corner_y0, 'YYNY')
draw_corner(corner_x0 + x*coil_pitch_h, corner_y1, 'NYYY')
# Left / right T junctions
for y in range(1, rows):
draw_corner(corner_x0, corner_y0 + y*coil_pitch_v, 'YYNY')
draw_corner(corner_x1, corner_y0 + y*coil_pitch_v, 'NYYY')
# Middle X junctions
for y in range(1, rows):
for x in range(1, cols):
draw_corner(corner_x0 + x*coil_pitch_h, corner_y0 + y*coil_pitch_v, 'YYYY')
else:
for layer in ('F.SilkS', 'B.SilkS'):
for x in range(0, cols+1):
cx = x0 + tooling_border + cut_gap/2 + x*coil_pitch_h
b.add(kc_gr.Line(xy(cx, corner_y0),
xy(cx, corner_y1),
layer=layer, stroke=pcb.Stroke(width=0.15)))
for y in range(0, rows+1):
cy = y0 + tooling_border + cut_gap/2 + y*coil_pitch_v
b.add(kc_gr.Line(xy(corner_x0, cy),
xy(corner_x1, cy),
layer=layer, stroke=pcb.Stroke(width=0.15)))
# Mouse bites
if do_mouse_bites:
for x in range(0, cols):
for y in range(0, rows):
tile_x0 = x0 + tooling_border + cut_gap + x*coil_pitch_h
tile_y0 = y0 + tooling_border + cut_gap + y*coil_pitch_v
b.add(make_mouse_bite(tile_x0 + tile_width/2, tile_y0 - mouse_bite_hole_dia/2, 0))
b.add(make_mouse_bite(tile_x0 + tile_width/2, tile_y0 + tile_height + mouse_bite_hole_dia/2, 0))
b.add(make_mouse_bite(tile_x0 - mouse_bite_hole_dia/2, tile_y0 + tile_height/2, 90))
b.add(make_mouse_bite(tile_x0 + tile_width + mouse_bite_hole_dia/2, tile_y0 + tile_height/2, 90))
# Mounting holes
for x in range(0, cols):
for y in range(0, rows):
tile_x0 = x0 + tooling_border + cut_gap + x*coil_pitch_h + tile_width/2
tile_y0 = y0 + tooling_border + cut_gap + y*coil_pitch_v + tile_height/2
dx = tile_width/2 - hole_offset
dy = tile_height/2 - hole_offset
b.add(make_hole(tile_x0 - dx, tile_y0 - dy, hole_dia))
b.add(make_hole(tile_x0 - dx, tile_y0 + dy, hole_dia))
b.add(make_hole(tile_x0 + dx, tile_y0 - dy, hole_dia))
b.add(make_hole(tile_x0 + dx, tile_y0 + dy, hole_dia))
# border graphics
c = 3
for layer in ['F.SilkS', 'B.SilkS']:
b.add(kc_gr.Rectangle(start=xy(x0, y0), end=xy(x0+c, y0+total_height), layer=layer, stroke=pcb.Stroke(width=0),
fill=kc_gr.FillMode(pcb.Atom.solid)))
b.add(kc_gr.Rectangle(start=xy(x0, y0), end=xy(x0+total_width, y0+c), layer=layer, stroke=pcb.Stroke(width=0),
fill=kc_gr.FillMode(pcb.Atom.solid)))
b.add(kc_gr.Rectangle(start=xy(x0+total_width-c, y0), end=xy(x0+total_width, y0+total_height), layer=layer, stroke=pcb.Stroke(width=0),
fill=kc_gr.FillMode(pcb.Atom.solid)))
b.add(kc_gr.Rectangle(start=xy(x0, y0+total_height-c), end=xy(x0+total_width, y0+total_height), layer=layer, stroke=pcb.Stroke(width=0),
fill=kc_gr.FillMode(pcb.Atom.solid)))
a = 3
timestamp = datetime.datetime.now().strftime('%Y-%m-%d')
b.add(kc_gr.Text(text=f'Planar inductor test panel',
at=pcb.AtPos(x0 + tooling_border + cut_gap/2, y0 + c + 2*a/3),
layer=kc_gr.TextLayer('F.SilkS'),
effects=pcb.TextEffect(
font=pcb.FontSpec(face="Inter Semi Bold",
size=xy(6*a/3, 6*a/3),
thickness=a/5),
justify=pcb.Justify(h=pcb.Atom.left, v=pcb.Atom.top))))
b.add(kc_gr.Text(text=f'{version_string} {timestamp} © 2023 Jan Götte, FG KOM, TU Darmstadt',
at=pcb.AtPos(x0 + total_width - tooling_border - cut_gap/2, y0 + c + 4*a/3),
layer=kc_gr.TextLayer('F.SilkS'),
effects=pcb.TextEffect(
font=pcb.FontSpec(face="Inter Light",
size=xy(a, a),
thickness=a/5),
justify=pcb.Justify(h=pcb.Atom.right, v=pcb.Atom.top))))
for index, ((y, x), spec) in tqdm.tqdm(enumerate(zip(itertools.product(range(rows), range(cols)), coil_specs), start=1)):
pass
with tempfile.NamedTemporaryFile(suffix='.kicad_mod') as f:
tile_x0 = x0 + tooling_border + cut_gap + x*coil_pitch_h + tile_width/2
tile_y0 = y0 + tooling_border + cut_gap + y*coil_pitch_v + tile_height/2
for key, alias in {
'gen.inner_diameter': 'id',
'gen.outer_diameter': 'od',
'gen.trace_width': 'w',
'gen.turns': 'n',
'gen.twists': 't',
'gen.clearance': 'c',
'gen.single_layer': 's',
'gen.via_drill': 'd',
'gen.via_diameter': 'v'}.items():
if alias in spec:
spec[key] = spec.pop(alias)
if 'gen.via_diameter' not in spec:
spec['gen.via_diameter'] = spec['gen.trace_width']
if 'gen.inner_diameter' not in spec:
spec['gen.inner_diameter'] = coil_inner_dia
if 'gen.outer_diameter' not in spec:
spec['gen.outer_diameter'] = coil_dia
args = ['python', '-m', 'twisted_coil_gen_twolayer', '--no-keepout-zone']
for k, v in spec.items():
prefix, _, k = k.partition('.')
if (not isinstance(v, bool) or v) and prefix == 'gen':
args.append('--' + k.replace('_', '-'))
if v is not True:
args.append(str(v))
arg_digest = hashlib.sha3_256(' / '.join(map(str, args)).encode()).hexdigest()
cachedir.mkdir(exist_ok=True)
cache_file = cachedir / f'C-{arg_digest}.kicad_mod'
log_file = cachedir / f'Q-{arg_digest}.kicad_mod'
if not cache_file.is_file():
args.append(cache_file)
try:
res = subprocess.run(args, check=True, capture_output=True, text=True)
log_file.write_text(res.stdout + res.stderr)
except subprocess.CalledProcessError as e:
print(f'Error generating coil with command line {args}, rc={e.returncode}')
print(e.stdout)
print(e.stderr)
coil = fp.Footprint.open_mod(cache_file)
coil.at = fp.AtPos(tile_x0, tile_y0, 0)
b.add(coil)
t = [f'n={spec["gen.turns"]}',
f'{spec["gen.twists"]} twists',
f'w={spec["gen.trace_width"]:.2f}mm']
if spec.get('gen.single_layer'):
t.append('single layer')
spec['gen.board_thickness'] = board_thickness
cur.execute('INSERT INTO coils(run_id) VALUES (?)', (run_id,))
coil_id = cur.lastrowid
for key, value in spec.items():
if isinstance(value, bool):
value = str(value)
db.execute('INSERT INTO results(coil_id, key, value) VALUES (?, ?, ?)', (coil_id, key, value))
for l in log_file.read_text().splitlines():
if (m := re.fullmatch(r'Approximate inductance:\s*([-+.0-9eE]+)\s*µH', l.strip())):
val = float(m.group(1)) * 1e-6
db.execute('INSERT INTO results(coil_id, key, value) VALUES (?, "calculated_approximate_inductance", ?)', (coil_id, val))
if (m := re.fullmatch(r'Approximate track length:\s*([-+.0-9eE]+)\s*mm', l.strip())):
val = float(m.group(1)) * 1e-3
db.execute('INSERT INTO results(coil_id, key, value) VALUES (?, "calculated_trace_length", ?)', (coil_id, val))
if (m := re.fullmatch(r'Approximate resistance:\s*([-+.0-9eE]+)\s*Ω', l.strip())):
val = float(m.group(1))
db.execute('INSERT INTO results(coil_id, key, value) VALUES (?, "calculated_approximate_resistance", ?)', (coil_id, val))
if (m := re.fullmatch(r'Fill factor:\s*([-+.0-9eE]+)', l.strip())):
val = float(m.group(1))
db.execute('INSERT INTO results(coil_id, key, value) VALUES (?, "calculated_fill_factor", ?)', (coil_id, val))
db.commit()
sz = 2
b.add(kc_gr.Text(text='\\n'.join(t),
at=pcb.AtPos(tile_x0, tile_y0),
layer=kc_gr.TextLayer('B.SilkS'),
effects=pcb.TextEffect(
font=pcb.FontSpec(face='Inter Medium',
size=xy(sz, sz),
thickness=sz/5),
justify=pcb.Justify(h=None, v=None, mirror=True))))
b.add(kc_gr.Text(text=f'Tile {index}',
at=pcb.AtPos(tile_x0, tile_y0 - tile_height/2 + sz),
layer=kc_gr.TextLayer('B.SilkS'),
effects=pcb.TextEffect(
font=pcb.FontSpec(face='Inter Semi Bold',
size=xy(sz, sz),
thickness=sz/5),
justify=pcb.Justify(h=None, v=pcb.Atom.top, mirror=True))))
b.add(kc_gr.Text(text=f'{version_string} {timestamp}',
at=pcb.AtPos(tile_x0, tile_y0 - tile_height/2 + sz*2.4),
layer=kc_gr.TextLayer('B.SilkS'),
effects=pcb.TextEffect(
font=pcb.FontSpec(face='Inter Light',
size=xy(sz, sz),
thickness=sz/5),
justify=pcb.Justify(h=None, v=pcb.Atom.top, mirror=True))))
b.add(kc_gr.Text(text=f'{index}',
at=pcb.AtPos(tile_x0, tile_y0 - tile_height/2 + sz),
layer=kc_gr.TextLayer('F.SilkS'),
effects=pcb.TextEffect(
font=pcb.FontSpec(face='Inter Medium',
size=xy(sz, sz),
thickness=sz/5),
justify=pcb.Justify(h=None, v=pcb.Atom.top, mirror=False))))
pads_x0 = tile_x0 + tile_width/2 - pad_offset
pads = make_pads(pads_x0, tile_y0, 270, 2, pad_dia, pad_length, pad_drill, pad_pitch)
b.add(pads)
w = min(spec.get('gen.trace_width', pad_dia), pad_dia)
wx, wy, _r, _f = pads.pad(2).abs_pos
w2 = (wx - pad_length/2, wy)
wx, wy, _r, _f = pads.pad(1).abs_pos
w1 = (wx - pad_length/2, wy)
b.add(cad_pr.Trace(w, coil.pad(1), pads.pad(1), waypoints=[w1], orientation=['ccw'], side='top'))
b.add(cad_pr.Trace(w, coil.pad(2), pads.pad(2), waypoints=[w2], orientation=['cw'], side='bottom'))
k = 3
for layer in ['F.SilkS', 'B.SilkS']:
b.add(kc_gr.Rectangle(start=xy(wx-k/2, wy-pad_pitch-k/2), end=xy(wx+k/2, wy-pad_pitch), layer=layer, stroke=pcb.Stroke(width=0),
fill=kc_gr.FillMode(pcb.Atom.solid)))
b.write('coil_test_board.kicad_pcb')

Wyświetl plik

@ -1,173 +0,0 @@
Header
CHECK KEYWORDS "Warn"
Mesh DB "." "mesh"
End
Simulation
Mesh Levels = 1
Max Output Level = 7
Coordinate System = Cartesian
Coordinate Mapping(3) = 1 2 3
Simulation Type = Steady state
Steady State Max Iterations = 1
Output Intervals = 1
Timestepping Method = BDF
Simulation Timing = True
BDF Order = 1
Solver Input File = case.sif
Post File = case.vtu
Output File = case.result
End
Constants
Stefan Boltzmann = 5.6704e-08
Permittivity of Vacuum = 8.8541878128e-12
Gravity(4) = 0 -1 0 9.80665
Boltzmann Constant = 1.380649e-23
Unit Charge = 1.602176634e-19
End
! airEqn
Equation 1
Active Solvers(1) = 2
End
! copperEqn
Equation 2
Active Solvers(1) = 1 2
End
Solver 1
Equation = Static Current Conduction
Variable = Potential
Variable DOFs = 1
Procedure = "StatCurrentSolve" "StatCurrentSolver"
Calculate Volume Current = True
Calculate Joule Heating = False
Optimize Bandwidth = True
Nonlinear System Max Iterations = 1
Linear System Solver = Iterative
Linear System Iterative Method = CG
Linear System Max Iterations = 10000
Linear System Convergence Tolerance = 1e-10
Linear System Preconditioning = ILU3
Linear System ILUT Tolerance = 0.001
Linear System Abort Not Converged = False
Linear System Residual Output = 20
Linear System Precondition Recompute = 1
End
Solver 2
Equation = Electrostatics
Procedure = "StatElecSolve" "StatElecSolver"
Variable = PotentialStat
Calculate Electric Field = True
Calculate Electric Energy = True
Steady State Convergence Tolerance = 1e-05
Nonlinear System Convergence Tolerance = 1e-07
Nonlinear System Max Iterations = 20
Nonlinear System Newton After Iterations = 3
Nonlinear System Newton After Tolerance = 0.001
Nonlinear System Relaxation Factor = 1
Linear System Solver = Iterative
Linear System Iterative Method = BiCGStab
Linear System Max Iterations = 500
Linear System Convergence Tolerance = 1e-10
BiCGstabl polynomial degree = 2
Linear System Preconditioning = none
Linear System ILUT Tolerance = 0.001
Linear System Abort Not Converged = False
Linear System Residual Output = 10
End
! air
Material 1
Density = 1.1885
Electric Conductivity = 0.0
Heat Capacity = 1006.4
Heat Conductivity = 0.025873
Relative Permeability = 1
Relative Permittivity = 1
End
! ro4003c
Material 2
Density = 1790
Electric Conductivity = 0.0
Relative Permeability = 1
Relative Permittivity = 3.55
End
! copper
Material 3
Density = 8960.0
Electric Conductivity = 59600000
Emissivity = 0.012
Heat Capacity = 415.0
Heat Conductivity = 401.0
Relative Permeability = 1
End
! trace
Body 1
Target Bodies(1) = 3
Equation = 2 ! copperEqn
Material = 3 ! copper
Body Force = 1 ! electric_potential
End
! substrate
Body 2
Target Bodies(1) = 1
Equation = 1 ! airEqn
Material = 2 ! ro4003c
End
! airbox
Body 3
Target Bodies(1) = 2
Equation = 1 ! airEqn
Material = 1 ! air
Body Force = 1
End
! interface_top
Body 4
Target Bodies(1) = 4
Material = 3 ! copper
End
! interface_bottom
Body 5
Target Bodies(1) = 5
Material = 3 ! copper
End
! FarField
Boundary Condition 1
Target Boundaries(1) = 6
Electric Infinity BC = True
End
! Vplus
Boundary Condition 2
Target Boundaries(1) = 4
Potential = 1.0
Save Scalars = True
End
! Vminus
Boundary Condition 3
Target Boundaries(1) = 5
Potential = 0.0
End
! electric_potential
Body Force 1
PotentialStat = Equals "Potential"
End

Wyświetl plik

@ -1,154 +0,0 @@
Header
CHECK KEYWORDS "Warn"
Mesh DB "." "mesh"
End
Simulation
Mesh Levels = 1
Max Output Level = 7
Coordinate System = Cartesian
Coordinate Mapping(3) = 1 2 3
Simulation Type = Steady state
Steady State Max Iterations = 1
Output Intervals = 1
Timestepping Method = BDF
Simulation Timing = True
BDF Order = 1
Solver Input File = case.sif
Post File = case.vtu
Output File = case.result
End
Constants
Stefan Boltzmann = 5.6704e-08
Permittivity of Vacuum = 8.8541878128e-12
Gravity(4) = 0 -1 0 9.80665
Boltzmann Constant = 1.380649e-23
Unit Charge = 1.602176634e-19
End
! airEqn
Equation 1
Active Solvers(1) = 1 ! Static_Current_Conduction, Magneto_Dynamics, Magneto_Dynamics_Calculations,
End
! Static_Current_Conduction
Solver 1
Equation = Electrostatics
Procedure = "StatElecSolve" "StatElecSolver"
Variable = Potential
Calculate Electric Field = True
Calculate Electric Energy = True
Exec Solver = Always
Stabilize = True
Bubbles = False
Lumped Mass Matrix = False
Optimize Bandwidth = True
Steady State Convergence Tolerance = 1e-05
Nonlinear System Convergence Tolerance = 1e-07
Nonlinear System Max Iterations = 20
Nonlinear System Newton After Iterations = 3
Nonlinear System Newton After Tolerance = 0.001
Nonlinear System Relaxation Factor = 1
Linear System Solver = Iterative
Linear System Iterative Method = BiCGStab
Linear System Max Iterations = 500
Linear System Convergence Tolerance = 1e-10
BiCGstabl polynomial degree = 2
Linear System Preconditioning = ILU0
Linear System ILUT Tolerance = 0.001
Linear System Abort Not Converged = False
Linear System Residual Output = 10
Linear System Precondition Recompute = 1
End
! air
Material 1
Density = 1.1885
Electric Conductivity = 0.0
Heat Capacity = 1006.4
Heat Conductivity = 0.025873
Relative Permeability = 1
Relative Permittivity = 1
End
! ro4003c
Material 2
Density = 1790
Electric Conductivity = 0.0
Relative Permeability = 1
Relative Permittivity = 3.55
End
! copper
Material 3
Density = 8960.0
Electric Conductivity = 59600000
Emissivity = 0.012
Heat Capacity = 415.0
Heat Conductivity = 401.0
Relative Permeability = 1
End
! trace
Body 1
Target Bodies(1) = 3
Material = 3 ! copper
Body Force = 1 ! electric_potential
End
! substrate
Body 2
Target Bodies(1) = 1
Equation = 1 ! airEqn
Material = 2 ! ro4003c
End
! airbox
Body 3
Target Bodies(1) = 2
Equation = 1 ! airEqn
Material = 1 ! air
End
! interface_top
Body 4
Target Bodies(1) = 4
Material = 3 ! copper
End
! interface_bottom
Body 5
Target Bodies(1) = 5
Material = 3 ! copper
End
! FarField
Boundary Condition 1
Target Boundaries(1) = 6
Electric Infinity BC = True
End
! Vplus
Boundary Condition 2
Target Boundaries(1) = 4
Potential = 1.0
Save Scalars = True
End
! Vminus
Boundary Condition 3
Target Boundaries(1) = 5
Potential = 0.0
End
! electric_potential
Body Force 1
Electric Potential = Equals "Potential"
End

Wyświetl plik

@ -1,282 +0,0 @@
#!/usr/bin/env python3
import threading
import queue
import itertools
import pathlib
import tempfile
import sys
import sqlite3
import time
import math
import json
import subprocess
import tqdm
import click
from tabulate import tabulate
def mesh_args(db, coil_id, mesh_type, mesh_file, outfile):
mesh_type = {'split': '--mesh-split-out', 'normal': '--mesh-out', 'mutual': '--mesh-mutual-out'}[mesh_type]
rows = db.execute('SELECT key, value FROM results WHERE coil_id=?', (coil_id,)).fetchall()
args = ['python', '-m', 'twisted_coil_gen_twolayer', mesh_type, mesh_file, '--pcb']
for k, v in rows:
prefix, _, k = k.partition('.')
if v != 'False' and prefix == 'gen':
args.append('--' + k.replace('_', '-'))
if v != 'True':
args.append(str(v))
args.append(outfile)
return args
def get_mesh_file(db, mesh_dir, run_id, coil_id, mesh_type):
db.execute('CREATE TABLE IF NOT EXISTS meshes(coil_id INTEGER, mesh_type TEXT, error INTEGER, filename TEXT, timestamp TEXT DEFAULT current_timestamp, FOREIGN KEY (coil_id) REFERENCES coils(coil_id))')
row = db.execute('SELECT * FROM meshes WHERE coil_id=? AND mesh_type=? ORDER BY timestamp DESC LIMIT 1', (coil_id, mesh_type)).fetchone()
if row is not None:
mesh_file = mesh_dir / row['filename']
if mesh_file.is_file():
return mesh_file
timestamp = time.strftime('%Y-%m-%d_%H-%M-%S')
return mesh_dir / f'mesh-{run_id}-{coil_id}-{mesh_type}-{timestamp}.msh'
def ensure_mesh(db, mesh_dir, log_dir, run_id, coil_id, mesh_type):
mesh_file = get_mesh_file(db, mesh_dir, run_id, coil_id, mesh_type)
if mesh_file.is_file():
return mesh_file
db.execute('INSERT INTO meshes(coil_id, mesh_type, error, filename) VALUES (?, ?, 0, ?)', (coil_id, mesh_type, mesh_file.name))
db.commit()
mesh_file.parent.mkdir(exist_ok=True)
with tempfile.NamedTemporaryFile(suffix='.kicad_pcb') as f:
args = mesh_args(db, coil_id, mesh_type, mesh_file, f.name)
tqdm.tqdm.write(' '.join(map(str, args)))
logfile = log_dir / mesh_file.with_suffix('.log').name
logfile.parent.mkdir(exist_ok=True)
try:
res = subprocess.run(args, check=True, capture_output=True, text=True)
logfile.write_text(res.stdout + res.stderr)
except subprocess.CalledProcessError as e:
print('Mesh generation failed with exit code {e.returncode}', file=sys.stderr)
logfile.write_text(e.stdout + e.stderr)
print(e.stdout + e.stderr)
raise
return mesh_file
@click.group()
@click.option('-d', '--database', default='coil_parameters.sqlite3')
@click.pass_context
def cli(ctx, database):
ctx.ensure_object(dict)
def connect():
db = sqlite3.connect(database)
db.row_factory = sqlite3.Row
return db
ctx.obj['db_connect'] = connect
@cli.command()
@click.pass_context
def list_runs(ctx):
for row in ctx.obj['db_connect']().execute('SELECT * FROM runs ORDER BY timestamp').fetchall():
print(row['run_id'], row['timestamp'], row['version'])
@cli.command()
@click.pass_context
def list_runs(ctx):
for row in ctx.obj['db_connect']().execute('SELECT * FROM runs ORDER BY timestamp').fetchall():
print(row['run_id'], row['timestamp'], row['version'])
@cli.command()
@click.option('-r', '--run-id')
@click.option('-m', '--mesh-dir', default='meshes')
@click.pass_context
def list_coils(ctx, run_id, mesh_dir):
db = ctx.obj['db_connect']()
if run_id is None:
run_id, = db.execute('SELECT run_id FROM runs ORDER BY timestamp DESC LIMIT 1').fetchone()
timestamp, = db.execute('SELECT timestamp FROM runs WHERE run_id=?', (run_id,)).fetchone()
mesh_dir = pathlib.Path(mesh_dir)
print(f'Listing meshes for run {run_id} at {timestamp}')
print()
keys = {'gen.turns': 'N',
'gen.twists': 'T',
'gen.single_layer': '1L',
'gen.inner_diameter': 'ID[mm]',
'gen.outer_diameter': 'OD[mm]',
'calculated_fill_factor': 'Fill factor',
'calculated_approximate_inductance': 'L [µH]',
'calculated_trace_length': 'track len [mm]',
'calculated_approximate_resistance': 'R [mΩ]'}
out = []
for row in db.execute('SELECT *, MAX(meshes.timestamp) FROM coils LEFT JOIN meshes ON coils.coil_id=meshes.coil_id WHERE run_id=? GROUP BY coils.coil_id, mesh_type ORDER BY meshes.timestamp', (run_id,)).fetchall():
if row['timestamp']:
if row['error']:
state = 'ERROR'
elif not (mesh_dir / row['filename']).is_file():
state = 'NOT FOUND'
else:
state = 'SUCCESS'
else:
state = 'NOT RUN'
params = dict(db.execute('SELECT key, value FROM results WHERE coil_id=?', (row['coil_id'],)).fetchall())
if 'calculated_approximate_inductance' in params:
params['calculated_approximate_inductance'] = f'{float(params["calculated_approximate_inductance"])*1e6:.02f}'
if 'calculated_trace_length' in params:
params['calculated_trace_length'] = f'{float(params["calculated_trace_length"])*1e3:.03f}'
if 'calculated_approximate_resistance' in params:
params['calculated_approximate_resistance'] = f'{float(params["calculated_approximate_resistance"])*1e3:.03f}'
if 'calculated_fill_factor' in params:
params['calculated_fill_factor'] = f'{float(params["calculated_fill_factor"]):.03f}'
out.append([row['coil_id'], row['mesh_type'], state, row['timestamp']] + [params.get(key, '-') for key in keys])
print(tabulate(out, headers=['coil', 'mesh', 'state', 'time'] + list(keys.values()), disable_numparse=True, stralign='right'))
@cli.command()
@click.argument('coil_id', type=int)
@click.argument('mesh_type', type=click.Choice(['normal', 'split', 'mutual']))
@click.option('--mesh-file', default='/tmp/test.msh')
@click.option('--pcb-file', default='/tmp/test.kicad_pcb')
@click.pass_context
def cmdline(ctx, coil_id, mesh_type, mesh_file, pcb_file):
print(' '.join(mesh_args(ctx.obj['db_connect'](), coil_id, mesh_type, mesh_file, pcb_file)))
@cli.group()
@click.option('-r', '--run-id')
@click.option('-l', '--log-dir', default='logs')
@click.option('-m', '--mesh-dir', default='meshes')
@click.pass_context
def run(ctx, run_id, log_dir, mesh_dir):
if run_id is None:
run_id, = ctx.obj['db_connect']().execute('SELECT run_id FROM runs ORDER BY timestamp DESC LIMIT 1').fetchone()
ctx.obj['run_id'] = run_id
ctx.obj['log_dir'] = pathlib.Path(log_dir)
ctx.obj['mesh_dir'] = pathlib.Path(mesh_dir)
@run.command()
@click.option('-j', '--num-jobs', type=int, default=1, help='Number of jobs to run in parallel')
@click.pass_context
def generate_meshes(ctx, num_jobs):
db = ctx.obj['db_connect']()
rows = [row['coil_id'] for row in db.execute('SELECT coil_id FROM coils WHERE run_id=?', (ctx.obj['run_id'],)).fetchall()]
mesh_types = ['split', 'normal', 'mutual']
params = list(itertools.product(rows, mesh_types))
all_files = {get_mesh_file(db, ctx.obj['mesh_dir'], ctx.obj['run_id'], coil_id, mesh_type): (coil_id, mesh_type) for coil_id, mesh_type in params}
todo = [(coil_id, mesh_type) for f, (coil_id, mesh_type) in all_files.items() if not f.is_file()]
q = queue.Queue()
for elem in todo:
q.put(elem)
tq = tqdm.tqdm(total=len(todo))
def queue_worker():
try:
while True:
coil_id, mesh_type = q.get_nowait()
try:
ensure_mesh(ctx.obj['db_connect'](), ctx.obj['mesh_dir'], ctx.obj['log_dir'], ctx.obj['run_id'], coil_id, mesh_type)
except subprocess.CalledProcessError:
tqdm.tqdm.write(f'Error generating {mesh_type} mesh for {coil_id=}')
tq.update(1)
q.task_done()
except queue.Empty:
pass
tqdm.tqdm.write(f'Found {len(params)-len(todo)} meshes out of a total of {len(params)}.')
tqdm.tqdm.write(f'Processing the remaining {len(todo)} meshes on {num_jobs} workers in parallel.')
threads = []
for i in range(num_jobs):
t = threading.Thread(target=queue_worker, daemon=True)
t.start()
threads.append(t)
q.join()
@run.command()
@click.option('-j', '--num-jobs', type=int, default=1, help='Number of jobs to run in parallel')
@click.pass_context
def self_inductance(ctx, num_jobs):
db = ctx.obj['db_connect']()
q = queue.Queue()
def queue_worker():
try:
while True:
mesh_file, logfile = q.get_nowait()
with tempfile.TemporaryDirectory() as tmpdir:
try:
tqdm.tqdm.write(f'Processing {mesh_file}')
res = subprocess.run(['python', '-m', 'coil_parasitics', 'inductance', '--sim-dir', tmpdir, mesh_file], check=True, capture_output=True)
logfile.write_text(res.stdout+res.stderr)
except subprocess.CalledProcessError as e:
print(f'Error running simulation, rc={e.returncode}')
logfile.write_text(e.stdout+e.stderr)
tq.update(1)
q.task_done()
except queue.Empty:
pass
num_meshes, num_params, num_completed = 0, 0, 0
for coil_id, in db.execute('SELECT coil_id FROM coils WHERE run_id=?', (ctx.obj['run_id'],)).fetchall():
num_params += 1
mesh_file = get_mesh_file(ctx.obj['db_connect'](), ctx.obj['mesh_dir'], ctx.obj['run_id'], coil_id, 'normal')
if mesh_file.is_file():
num_meshes += 1
logfile = ctx.obj['log_dir'] / (mesh_file.stem + '_elmer_self_inductance.log')
if logfile.is_file():
num_completed += 1
else:
q.put((mesh_file, logfile))
tqdm.tqdm.write(f'Found {num_meshes} meshes out of a total of {num_params} with {num_completed} completed simulations.')
tqdm.tqdm.write(f'Processing the remaining {num_meshes-num_completed} simulations on {num_jobs} workers in parallel.')
tq = tqdm.tqdm(total=num_meshes-num_completed)
threads = []
for i in range(num_jobs):
t = threading.Thread(target=queue_worker, daemon=True)
t.start()
threads.append(t)
q.join()
@run.command()
@click.pass_context
def self_capacitance(ctx):
db = ctx.obj['db_connect']()
for coil_id, in tqdm.tqdm(db.execute('SELECT coil_id FROM coils WHERE run_id=?', (ctx.obj['run_id'],)).fetchall()):
mesh_file = get_mesh_file(ctx.obj['db_connect'](), ctx.obj['mesh_dir'], ctx.obj['run_id'], coil_id, 'normal')
if mesh_file.is_file():
logfile = ctx.obj['log_dir'] / (mesh_file.stem + '_elmer_self_capacitance.log')
with tempfile.TemporaryDirectory() as tmpdir:
try:
res = subprocess.run(['python', '-m', 'coil_parasitics', 'self-capacitance', '--sim-dir', tmpdir, mesh_file], check=True, capture_output=True)
logfile.write_text(res.stdout+res.stderr)
except subprocess.CalledProcessError as e:
print(f'Error running simulation, rc={e.returncode}')
logfile.write_text(e.stdout+e.stderr)
if __name__ == '__main__':
cli()

Wyświetl plik

@ -1,295 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
import os
from math import *
from pathlib import Path
from itertools import cycle
from scipy.constants import mu_0
from gerbonara.cad.kicad import pcb as kicad_pcb
from gerbonara.cad.kicad import footprints as kicad_fp
from gerbonara.cad.kicad import graphical_primitives as kicad_gr
from gerbonara.cad.kicad import primitives as kicad_pr
import click
__version__ = '1.0.0'
def point_line_distance(p, l1, l2):
x0, y0 = p
x1, y1 = l1
x2, y2 = l2
# https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
return abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) / sqrt((x2-x1)**2 + (y2-y1)**2)
def line_line_intersection(l1, l2):
p1, p2 = l1
p3, p4 = l2
x1, y1 = p1
x2, y2 = p2
x3, y3 = p3
x4, y4 = p4
# https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
px = ((x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4))/((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4))
py = ((x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4))/((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4))
return px, py
def angle_between_vectors(va, vb):
angle = atan2(vb[1], vb[0]) - atan2(va[1], va[0])
if angle < 0:
angle += 2*pi
return angle
class SVGPath:
def __init__(self, **attrs):
self.d = ''
self.attrs = attrs
def line(self, x, y):
self.d += f'L {x} {y} '
def move(self, x, y):
self.d += f'M {x} {y} '
def arc(self, x, y, r, large, sweep):
self.d += f'A {r} {r} 0 {int(large)} {int(sweep)} {x} {y} '
def close(self):
self.d += 'Z '
def __str__(self):
attrs = ' '.join(f'{key.replace("_", "-")}="{value}"' for key, value in self.attrs.items())
return f'<path {attrs} d="{self.d.rstrip()}"/>'
class SVGCircle:
def __init__(self, r, cx, cy, **attrs):
self.r = r
self.cx, self.cy = cx, cy
self.attrs = attrs
def __str__(self):
attrs = ' '.join(f'{key.replace("_", "-")}="{value}"' for key, value in self.attrs.items())
return f'<circle {attrs} r="{self.r}" cx="{self.cx}" cy="{self.cy}"/>'
def svg_file(fn, stuff, vbw, vbh, vbx=0, vby=0):
with open(fn, 'w') as f:
f.write('<?xml version="1.0" standalone="no"?>\n')
f.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
f.write(f'<svg version="1.1" width="{vbw}mm" height="{vbh}mm" viewBox="{vbx} {vby} {vbw} {vbh}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">>\n')
for foo in stuff:
f.write(str(foo))
f.write('</svg>\n')
@click.command()
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
@click.option('--footprint-name', help="Name for the generated footprint. Default: Output file name sans extension.")
@click.option('--target-layer', default='F.Cu', help="Target KiCad layer for the generated footprint. Default: F.Cu.")
@click.option('--jumper-layer', default='B.Cu', help="KiCad layer for jumper connections. Default: B.Cu.")
@click.option('--turns', type=int, default=5, help='Number of turns')
@click.option('--diameter', type=float, default=50, help='Outer diameter [mm]')
@click.option('--trace-width', type=float, default=0.15)
@click.option('--via-diameter', type=float, default=0.6)
@click.option('--via-drill', type=float, default=0.3)
@click.option('--keepout-zone/--no-keepout-zone', default=True, help='Add a keepout are to the footprint (default: yes)')
@click.option('--keepout-margin', type=float, default=5, help='Margin between outside of coil and keepout area (mm, default: 5)')
@click.option('--twist-width', type=float, default=20, help='Width of twist versus straight coil in percent (0-100, default: 20)')
@click.option('--num-twists', type=int, default=1, help='Number of twists per revolution (default: 1)')
@click.option('--clearance', type=float, default=0.15)
@click.option('--clipboard/--no-clipboard', help='Use clipboard integration (requires wl-clipboard)')
@click.option('--counter-clockwise/--clockwise', help='Direction of generated spiral. Default: clockwise when wound from the inside.')
def generate(outfile, turns, diameter, via_diameter, via_drill, trace_width, clearance, footprint_name, target_layer,
jumper_layer, twist_width, num_twists, clipboard, counter_clockwise, keepout_zone, keepout_margin):
if 'WAYLAND_DISPLAY' in os.environ:
copy, paste, cliputil = ['wl-copy'], ['wl-paste'], 'xclip'
else:
copy, paste, cliputil = ['xclip', '-i', '-sel', 'clipboard'], ['xclip', '-o', '-sel' 'clipboard'], 'wl-clipboard'
out_path = SVGPath(fill='none', stroke='black', stroke_width=trace_width, stroke_linejoin='round', stroke_linecap='round')
jumper_path = SVGPath(fill='none', stroke='gray', stroke_width=trace_width, stroke_linejoin='round', stroke_linecap='round')
svg_stuff = [jumper_path, out_path]
pitch = clearance + trace_width
twist_angle = 2*pi / (turns * num_twists - 1)
twist_width = twist_angle * twist_width/100
via_diameter = max(trace_width, via_diameter)
# See https://coil32.net/pcb-coil.html for details
d_inside = diameter - 2*(pitch*turns - clearance)
d_avg = (diameter + d_inside)/2
phi = (diameter - d_inside) / (diameter + d_inside)
c1, c2, c3, c4 = 1.00, 2.46, 0.00, 0.20
L = mu_0 * turns**2 * d_avg*1e3 * c1 / 2 * (log(c2/phi) + c3*phi + c4*phi**2)
print(f'Outer diameter: {diameter:g} mm')
print(f'Average diameter: {d_avg:g} mm')
print(f'Inner diameter: {d_inside:g} mm')
print(f'Fill factor: {phi:g}')
print(f'Approximate inductance: {L:g} µH')
make_pad = lambda num, x, y: kicad_fp.Pad(
number=str(num),
type=kicad_fp.Atom.smd,
shape=kicad_fp.Atom.circle,
at=kicad_fp.AtPos(x=x, y=y),
size=kicad_fp.XYCoord(x=trace_width, y=trace_width),
layers=[target_layer],
clearance=clearance,
zone_connect=0)
make_line = lambda x1, y1, x2, y2, layer=target_layer: kicad_fp.Line(
start=kicad_fp.XYCoord(x=x1, y=y1),
end=kicad_fp.XYCoord(x=x2, y=y2),
layer=layer,
stroke=kicad_fp.Stroke(width=trace_width))
make_arc = lambda x1, y1, x2, y2, xc, yc, layer=target_layer: kicad_fp.Arc(
start=kicad_fp.XYCoord(x=x1, y=y1),
mid=kicad_fp.XYCoord(x=xc, y=yc),
end=kicad_fp.XYCoord(x=x2, y=y2),
layer=layer,
stroke=kicad_fp.Stroke(width=trace_width))
make_via = lambda x, y: kicad_fp.Pad(number="NC",
type=kicad_fp.Atom.thru_hole,
shape=kicad_fp.Atom.circle,
at=kicad_fp.AtPos(x=x, y=y),
size=kicad_fp.XYCoord(x=via_diameter, y=via_diameter),
drill=kicad_fp.Drill(diameter=via_drill),
layers=[target_layer, jumper_layer],
clearance=clearance,
zone_connect=0)
pads = []
lines = []
arcs = []
for n in range(turns * num_twists - 1):
for k in range(turns):
r = diameter/2 - trace_width/2 - k*pitch
a1 = n*twist_angle + twist_width/2
a2 = a1 + twist_angle - twist_width
x1, y1 = r*cos(a1), r*sin(a1)
out_path.move(x1, y1)
x2, y2 = r*cos(a2), r*sin(a2)
out_path.line(x2, y2)
a3 = (a1 + a2) / 2
xm, ym = r*cos(a3), r*sin(a3)
arcs.append(make_arc(x2, y2, x1, y1, xm, ym))
for k in range(turns-1):
r1 = diameter/2 - trace_width/2 - (k+1)*pitch
r2 = diameter/2 - trace_width/2 - k*pitch
a1 = n*twist_angle - twist_width/2
a2 = a1 + twist_width
x1, y1 = r1*cos(a1), r1*sin(a1)
out_path.move(x1, y1)
x2, y2 = r2*cos(a2), r2*sin(a2)
out_path.line(x2, y2)
a3 = (a1 + a2) / 2
r3 = (r1 + r2) / 2
xm, ym = r3*cos(a3), r3*sin(a3)
arcs.append(make_arc(x2, y2, x1, y1, xm, ym))
rs = diameter/2 - trace_width/2
rv = rs - trace_width/2 + via_diameter/2
a = n*twist_angle - twist_width/2
x1, y1 = rs*cos(a), rs*sin(a)
out_path.move(x1, y1)
xv1, yv1 = rv*cos(a), rv*sin(a)
out_path.line(xv1, yv1)
svg_stuff.append(SVGCircle(via_diameter/2, xv1, yv1, fill='red'))
pads.append(make_via(xv1, yv1))
jumper_path.move(xv1, yv1)
lines.append(make_line(x1, y1, xv1, yv1))
a += twist_width
rs = diameter/2 - trace_width/2 - (turns-1)*pitch
rv = rs + trace_width/2 - via_diameter/2
x1, y1 = rs*cos(a), rs*sin(a)
out_path.move(x1, y1)
xv2, yv2 = rv*cos(a), rv*sin(a)
out_path.line(xv2, yv2)
svg_stuff.append(SVGCircle(via_diameter/2, xv2, yv2, fill='red'))
pads.append(make_via(xv2, yv2))
lines.append(make_line(x1, y1, xv2, yv2))
if n > 0:
jumper_path.line(xv2, yv2)
lines.append(make_line(xv1, yv1, xv2, yv2, jumper_layer))
else:
pads.append(make_pad(1, xv1, yv1))
pads.append(make_pad(2, xv2, yv2))
svg_file('/tmp/test.svg', svg_stuff, 100, 100, -50, -50)
if counter_clockwise:
for p in pads:
p.at.y = -p.at.y
for l in lines:
l.start.y = -l.start.y
l.end.y = -l.end.y
for a in arcs:
a.start.y = -a.start.y
a.end.y = -a.end.y
if footprint_name:
name = footprint_name
elif outfile:
name = outfile.stem,
else:
name = 'generated_coil'
if keepout_zone:
r = diameter/2 + keepout_margin
tol = 0.05 # mm
n = ceil(pi / acos(1 - tol/r))
pts = [(r*cos(a*2*pi/n), r*sin(a*2*pi/n)) for a in range(n)]
zones = [kicad_pr.Zone(layers=['*.Cu'],
hatch=kicad_pr.Hatch(),
filled_areas_thickness=False,
keepout=kicad_pr.ZoneKeepout(copperpour_allowed=False),
polygon=kicad_pr.ZonePolygon(pts=kicad_pr.PointList(xy=[kicad_pr.XYCoord(x=x, y=y) for x, y in pts])))]
else:
zones = []
fp = kicad_fp.Footprint(
name=name,
generator=kicad_fp.Atom('GerbonaraTwistedCoilGenV1'),
layer='F.Cu',
descr=f"{turns} turn {diameter:.2f} mm diameter twisted coil footprint, inductance approximately {L:.6f} µH. Generated by gerbonara'c Twisted Coil generator, version {__version__}.",
clearance=clearance,
zone_connect=0,
lines=lines,
arcs=arcs,
pads=pads,
zones=zones,
)
if clipboard:
try:
print(f'Running {copy[0]}.')
proc = subprocess.Popen(copy, stdin=subprocess.PIPE, text=True)
proc.communicate(fp.serialize())
except FileNotFoundError:
print(f'Error: --clipboard requires the {copy[0]} and {paste[0]} utilities from {cliputil} to be installed.', file=sys.stderr)
elif not outfile:
print(fp.serialize())
else:
fp.write(outfile)
if __name__ == '__main__':
generate()