# Authors: Mainak Jas <mainakjas@gmail.com>
# Gabriel Motta <gabrielbenmotta@gmail.com>
from pathlib import Path
import numpy as np
import os
import copy
import pyvista as pv
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from bfieldtools.line_conductor import LineConductor
from shapely.geometry import LineString
from shapely.geometry import Point
from .biplanar_coil import get_2D_point_grid
def segment_len(seg):
return (((seg[0][1]-seg[1][1])**2) + ((seg[0][2] - seg[1][2])**2))**.5
def kicad_to_loops(fname, offset, min_len=.1, mult=1000):
"""Load in front, back and via information from kicad file.
Applies scaling, offset, and rejection based on input parameters.
Parameters
----------
fname : string
file path to kicad file.
offset : list of 3 coordinates
initial offset for loaded points.
min_len : float
discard any segment with length smaller than this value.
mult : float
scale points according to this multiplier.
"""
with open(fname, 'r') as fp:
lines = fp.readlines()
if not offset:
offset = [0, 0, 0]
lines = [line.strip(' ').strip('\n') for line in lines]
loops = {'F.Cu': list(), 'B.Cu': list(), 'via': list()}
for layer in ['F.Cu', 'B.Cu']:
lines_layer = [line for line in lines if line.startswith('(segment')
and layer in line]
for line in lines_layer:
parts = line.split('(')
parts = [part.strip('start').strip('end').strip(' ').strip(')')
for part in parts if part.startswith(('start', 'end'))]
xstart, ystart = [float(coord) for coord in parts[0].split(' ')]
xend, yend = [float(coord) for coord in parts[1].split(' ')]
seg = [[offset[0],
xstart / mult + offset[1],
ystart / mult + offset[2]],
[offset[0],
xend / mult + offset[1],
yend / mult + offset[2]]]
if segment_len(seg) > min_len:
loops[layer].append(seg)
via_lines = [line for line in lines if line.startswith('(via')
and layer[0] in line and layer[1] in line]
for line in via_lines:
parts = line.split('(')
parts = [part.strip('at').strip(' ').strip(')') for part in parts
if part.startswith('at')]
x, y = [float(coord) for coord in parts[0].split(' ')]
via = [0, x, y]
loops['via'].append(via)
return loops
def _move_loops(loops, offset):
new_loops = dict()
for layer in ['F.Cu', 'B.Cu']:
new_loops[layer] = list()
for loop in loops[layer]:
for seg in loop:
seg = (np.array(seg) + offset).tolist()
new_loops[layer].append(seg)
return new_loops
def do_intersect(seg1, seg2, tolerance=.02):
line1 = LineString(seg1)
line2 = LineString(seg2)
return not line1.intersection(line2, grid_size=tolerance).is_empty
def dist(pt1, pt2):
return np.linalg.norm(np.array(pt2) - np.array(pt1))
"""
def do_intersect(seg1, seg2, tol=1e-3):
if dist(seg1[0], seg2[0]) < tol:
return True
elif dist(seg1[1], seg2[0]) < tol:
return True
elif dist(seg1[0], seg2[1]) < tol:
return True
elif dist(seg1[1], seg2[1]) < tol:
return True
return False
# a bit less robust to points like:
(start 114.22 274.22) (end 114.22 274.22)
"""
def get_chain(segments, tolerance=.02, verbose=True):
""" Build continous chains from a list of segments
Parameters
----------
segments : list of line segments (2 3D points)
segments to be connected into chains
tolerance : float
how close points need to be to be considered overlapping.
verbose : bool
whether to print progress information.
"""
z_offset = segments[0][0][0]
segments = np.array(segments)[:, :, 1:].tolist() # remove z-coordinate
chains = list()
print(end='')
init_len = len(segments)
while len(segments) > 0:
chain = [segments[0]]
segments.remove(segments[0])
keep_looping = True
while keep_looping:
keep_looping = False
for seg in segments:
if seg in chain:
segments.remove(seg)
keep_looping = True
break
elif do_intersect(seg, chain[-1], tolerance):
chain.append(seg)
segments.remove(seg)
keep_looping = True
break
elif do_intersect(seg, chain[0], tolerance):
chain.insert(0, seg)
segments.remove(seg)
keep_looping = True
break
if verbose:
print(f'\rChains: {len(chains) + 1},' +
f' remaining segments: {len(segments)}/{init_len} ')
loading_bar(1 - len(segments) / init_len)
print("\033[F", end='')
chain = np.array(chain)
chain_3d = np.zeros((chain.shape[0], chain.shape[1], 3))
chain_3d[:, :, 1:] = chain
chain_3d[:, :, 0] = z_offset
chains.append(chain_3d)
if verbose:
print()
print(70 * ' ' + '\r', end='')
return chains
def loading_bar(ratio, length=30, full_char='#', empty_char='-'):
"""Prints a loading bar"""
comp = min(int(ratio * length), length)
loaded = "".join([full_char for _ in range(comp)])
unloaded = "".join([empty_char for _ in range(length - comp)])
print(f'\r[{loaded}{unloaded}] {int(100 * ratio)}%', end='', flush=True)
def link_chains(chain1, chain2, precision):
"""Given two chains, tries to return a linked configuration.
Parameters
----------
chain1 : list of line segments (2 3d points)
one of the chains to be linked.
chain2 : list of line segments (2 3d points)
one of the chains to be linked.
precision : float
how close points need to be to be considered overlapping.
"""
c2_pts = [Point(chain2[-1][0][1:]), Point(chain2[-1][1][1:]),
Point(chain2[0][0][1:]), Point(chain2[0][1][1:])]
def linkable(point, ind):
nonlocal c2_pts
nonlocal precision
return point.dwithin(c2_pts[ind], precision)
for c1_point in chain1[0]:
point = Point(c1_point[1:])
if linkable(point, 0) or linkable(point, 1):
return np.concatenate((chain2, chain1))
elif linkable(point, 2) or linkable(point, 3):
return np.concatenate((np.flip(chain2, 0), chain1))
for c1_point in chain1[-1]:
point = Point(c1_point[1:])
if linkable(point, 0) or linkable(point, 1):
return np.concatenate((chain1, np.flip(chain2, 0)))
elif linkable(point, 2) or linkable(point, 3):
return np.concatenate((chain1, chain2))
return None
def combine_chains(chains, precision=2):
"""Attempts to link together chains.
Parameters
----------
chains : list of line segments (2 3D points)
the chains to be combined.
precision : float
how close segments have to be to count as 'overlapping'
"""
for i in range(len(chains)):
candidate = chains[0]
chains.pop(0)
for _ in range(len(chains)):
for i, chain in enumerate(chains):
new_c = link_chains(candidate, chain, precision)
if new_c is not None:
candidate = new_c
chains.pop(i)
break
chains.append(candidate)
return chains
def combine_while_bypassing(chains, precision):
""" Tries to combine chains while considering beyond the endpoints.
Parameters
----------
chains : list of list of line segements (2 3D points)
chains to be combined.
precision : float
how close points need to be to be considered overlapping.
"""
for i in range(len(chains)):
if len(chains) == 1:
break
candidate = chains[0]
chains.pop(0)
if len(candidate) == 1:
chains.append(candidate)
continue
found = False
for cand in [candidate[1:], candidate[:-1]]:
if found:
break
for i, chain in enumerate(chains):
new_c = link_chains(cand, chain, precision)
if new_c is not None:
print('Bypassing chain endpoint to join chains.')
candidate = new_c
chains.pop(i)
found = True
break
chains.append(candidate)
return chains
def allign_chain(chain):
""" Make all links in a chain point in a single direction, such that the
first point of a link is linked to the second point of the preceeding link.
The given chain is modified in place.
Parameters
----------
chain : array of pairs of points
Array to be alligned.
"""
if len(chain) < 2:
return
i = 1
dists = [0, 0, 0, 0]
dists[0] = Point(chain[i-1][1][1:]).distance(Point(chain[i][0][1:]))
dists[1] = Point(chain[i-1][1][1:]).distance(Point(chain[i][1][1:]))
dists[2] = Point(chain[i-1][0][1:]).distance(Point(chain[i][0][1:]))
dists[3] = Point(chain[i-1][0][1:]).distance(Point(chain[i][1][1:]))
min_ind = dists.index(min(dists))
if min_ind in [2, 3]:
chain[0][0], chain[0][1] = chain[0][1], chain[0][0]
for i in range(1, len(chain)):
dist_norm = Point(chain[i-1][1][1:]).distance(Point(chain[i][0][1:]))
dist_flipped = Point(chain[i-1][1][1:]).distance(Point(chain[i][1][1:]))
if dist_flipped < dist_norm:
chain[i] = np.flip(chain[i], axis=0)
def flip_chain(chain):
""" Flips the orientation of each link in the chain, such that the start
and end points are flipped. This is done in place.
Parameters
----------
chain : array of pairs of points
Array to be flipped.
"""
if len(chain) < 1:
return
for i in range(len(chain)):
chain[i] = np.flip(chain[i], axis=0)
def flip_chains(chain_list):
""" Flips the orientation of each link in the chain, such that the start
and end points are flipped. This is done in place.
Parameters
----------
chain : array of pairs of points
Array to be flipped.
"""
for chain in chain_list:
flip_chain(chain)
def plot_chains_2D(chains):
ax = plt.figure().add_subplot()
loops = dict()
layers = list()
for idx, chain in enumerate(chains):
loops[f'chain{idx}'] = chains[idx]
layers.append(f'chain{idx}')
colors = list(mcolors.TABLEAU_COLORS.keys())
while len(colors) < len(chains):
colors = colors + colors
num = 0
for color, layer in zip(colors, layers):
if layer == 'B.Cu':
continue
loop_array = np.array(loops[layer])
cen = loop_array[:, 0, :]
arrow_dir = loop_array[:, 1, :] - loop_array[:, 0, :]
if layer == 'B.Cu':
arrow_dir *= -1
print(f'Layer {layer}, color {color}')
arrows = ax.quiver(cen[:, 1], cen[:, 2], arrow_dir[:, 1],
arrow_dir[:, 2], color=color,
scale_units='xy', scale=1, angles='xy')
if layer not in ['F.Cu', 'B.Cu']:
ax.quiver(cen[0][1], cen[0][2], arrow_dir[0][1],
arrow_dir[0][2], color='y', scale_units='xy',
scale=1, angles='xy')
ax.quiver(cen[-1][1], cen[-1][2], arrow_dir[-1][1],
arrow_dir[-1][2],
color='r', scale_units='xy', scale=1,
angles='xy')
ax.annotate(f'{num}',(cen[0][1], cen[0][2]))
num += 1
def plot_chains(chains, ax=None, pl=None):
""" Plots chains with arrows as the links.
Parameters
----------
chains : list of arrays of pairs of points
Chains to be plotted.
ax : matplotlib element
matplotlib subplot to use. If none, a new one is created.
"""
# if not ax:
# ax = plt.figure().add_subplot()
if pl is None:
pl = pv.Plotter()
colors = list(mcolors.TABLEAU_COLORS.keys())
while len(colors) < len(chains):
colors = colors + colors
for color, chain in zip(colors, chains):
loop_array = np.array(chain)
cen = loop_array[:, 0, :]
arrow_dir = loop_array[:, 1, :] - loop_array[:, 0, :]
pl.add_arrows(cen, arrow_dir, color=color)
pl.add_arrows(cen[0], arrow_dir[0], color='y', mag=5)
pl.add_arrows(cen[-1], arrow_dir[-1], color='r', mag=5)
# XXX: sphinx_gallery does not like below statement
# pl.add_axes(xlabel='Z', ylabel='X', zlabel='Y')
return pl
[docs]
class PCB:
[docs]
def __init__(self, fname=None, offset=None, loops=None, pcb_dict=None):
"""Class to represent a PCB loaded from KiCAD.
Attributes
----------
loops : dict of list
The keys may be 'via', 'F.Cu', 'B.Cu' for via,
front, and back copper layer.
"""
self.chains = None
if pcb_dict is not None:
self.chains = pcb_dict['chains']
return
if loops is not None:
self.loops = loops
else:
self.loops = kicad_to_loops(fname, offset, mult=1)
self.check = None
[docs]
def get_chains(self):
"""Returns continuous loops in pcb, builds them if needed."""
if self.chains is not None:
return self.chains
def build_chain(segments, tolerances):
chains = get_chain(segments, tolerance=.02)
initial_size = len(chains)
for tol in tolerances:
chains = combine_chains(chains, tol)
chains = [ch for ch in chains if len(ch) > 1 or segment_len(ch[0]) > 1]
return chains, initial_size
tol_iters = [.1, .5, 1, 2]
print('Parsing front...')
if 'F.Cu' in self.loops.keys() and len(self.loops['F.Cu']) > 0:
front_chains, front_size = build_chain(self.loops['F.Cu'], tol_iters)
else:
front_chains = list()
front_size = 0
print('Parsing back...')
if 'B.Cu' in self.loops.keys() and len(self.loops['B.Cu']) > 0:
back_chains, back_size = build_chain(self.loops['B.Cu'], tol_iters)
else:
back_chains = list()
back_size = 0
print('Combining...', end='\r')
chains = front_chains + back_chains
for tol in tol_iters:
chains = combine_chains(chains, tol)
if len(chains) > 1:
chains = combine_while_bypassing(chains, 1)
print(f'Combining: {front_size + back_size} --> {len(chains)}')
for chain in chains:
allign_chain(chain)
self.chains = chains
return self.chains
[docs]
def plot(self, ax=None, pl=None, show=False):
"""Plots PCB
Parameters
----------
ax : matplotlib Axes
The matplotlib axis
pl : pyvista.Plotter object
The plotter object.
"""
chains = self.get_chains()
plot_chains(chains, ax, pl)
if show:
pl.show()
return pl
[docs]
def plot2D(self, ax=None, pl=None):
"""Plots PCB"""
chains = self.get_chains()
plot_chains_2D(chains)
if pl:
pl.show()
return pl
@property
def length(self):
# XXX: todo add repr which prints number of segments
total_len = 0
for chain in self.chains:
for segment in chain:
total_len += dist(segment[0], segment[1])
return total_len
[docs]
def magnetic_field(self, target_points):
"""Calculate total magnetic field from both layers for unit current.
Parameters
----------
target_points : array, shape (n_points, 3)
The target points at which to evaluate the magnetic fields.
Returns
-------
field : array, shape (n_points, 3)
The magnetic field at the target points
"""
field = list()
for chain in self.chains:
line_conductor = LineConductor(chain)
field.append(line_conductor.magnetic_field(target_points))
return sum(field)
[docs]
def adjust_offset(self, offsets):
"""Adjust the offset for a panel.
Parameters
----------
offsets : list of length 3
The x, y, and z offset
"""
for segment in self.chains:
segment[:, :, 0] = offsets[0]
segment[:, :, 1] += offsets[1]
segment[:, :, 2] += offsets[2]
def adjust_scale(self, mult):
for segment in self.chains:
segment[:, :, 1:] /= mult
[docs]
def to_dict(self):
"""Convert to dictionary"""
pcb_dict = dict()
pcb_dict['chains'] = self.chains
return pcb_dict
[docs]
class PCBPanel:
[docs]
def __init__(self, pcb_folder=None, half_names=None, standoff=None,
rearrange=False, panel_dict=None):
"""A Panel is a collection of PCBs used to null one field component.
Parameters
----------
pcb_folder : str
The path to the kicad folder with the PCB design.
half_names : list of str
The names of the halves used for folder naming. E.g.,
['top', 'bott']
standoff : float
The standoff between the two biplanar PCBs. Typically,
this is equal to the length of the PCBs, i.e., 1.4 m.
Attributes
----------
pcbs : dict of PCB
keys of dictionary can be 'left_top', 'left_bott',
'right_top', 'right_bott' etc.
"""
self.pcbs = dict()
self.chains = dict()
if not pcb_folder and not half_names and not standoff:
if panel_dict:
for key, pcb in panel_dict.items():
self.pcbs[key] = PCB(pcb_dict=pcb)
self.chains[key] = self.pcbs[key].chains
return
pcb_folder = Path(pcb_folder)
shift_xs = {'left': -standoff / 2., # in m
'right': standoff / 2.}
for half_name in half_names:
if half_name == 'top' or (rearrange and half_name == 'first'):
shift_y = -0.75
shift_z = 0
elif half_name == 'bott' or (rearrange and half_name == 'second'):
shift_y = -0.75
shift_z = -0.75
elif half_name == 'first':
shift_y = 0
shift_z = -0.75
elif half_name == 'second':
shift_y = -0.75
shift_z = -0.75
fname = pcb_folder / half_name / f'coil_template_{half_name}.kicad_pcb'
pcb = PCB(fname) # offsets in mm
pcb.get_chains()
for dir, shift_x in shift_xs.items():
offset = np.array([shift_x, shift_y, shift_z])
offset_pcb = copy.deepcopy(pcb)
offset_pcb.adjust_scale(1000)
offset_pcb.adjust_offset(offset)
self.pcbs[f'{dir}_{half_name}'] = offset_pcb
self.chains[f'{dir}_{half_name}'] = offset_pcb.get_chains()
@property
def length(self):
"""The total length of the panel."""
total_len = 0
for pcb_name, pcb in self.pcbs.items():
total_len += pcb.length
return total_len
[docs]
def resistance(self, cu_oz=2, trace_width=5):
"""The coil resistance."""
# this formulation for PCB resistance matches the internet calculators
rho = 1.72e-8 # ohm-m at 25C
thickness = cu_oz * 35e-6 # (1 oz cu == 35 um thick)
width = trace_width * 1e-3 # m
resistance = rho * self.length / (width * thickness)
return resistance
[docs]
def adjust_standoff(self, standoff):
"""Adjust the standoff.
Parameters
----------
standoff : float
The new standoff distance (in m).
"""
for pcb_name, pcb in self.pcbs.items():
if 'left' in pcb_name:
shift = -standoff / 2.
else:
shift = standoff / 2.
pcb.adjust_offset(np.array([shift, 0, 0]))
[docs]
def magnetic_field(self, target_points, current):
"""Calculate total magnetic field from panel.
Parameters
----------
current : dict
The current.
"""
field = 0.
for pcb_name, pcb in self.pcbs.items():
dir, half_name = pcb_name.split('_')
this_current = current[dir]
field += pcb.magnetic_field(target_points) * this_current
return field
[docs]
def build_chains(self):
"""Link PCBs into continuous loops."""
self.chains.clear()
for pcb_name, pcb in self.pcbs.items():
print(f'[[ Parsing {pcb_name} ]]')
self.chains[pcb_name] = pcb.get_chains()
print()
[docs]
def plot(self, target_points=None, current=None, pl=None, show=True):
"""Plot the PCBs."""
if pl is None:
pl = pv.Plotter()
for pcb_name, pcb in self.pcbs.items():
pcb.plot(pl=pl, show=False)
if target_points is not None and current is not None:
field = self.magnetic_field(target_points, current)
pl.add_arrows(target_points, field, mag=1e7)
if show:
# pl.remove_scalar_bar()
pl.show()
return pl
[docs]
def plot_profile(self, current, profile_dir='x', field_component='x',
spacing=0.01, min_pos=-0.2, max_pos=0.2, ax=None):
"""Plot line profile for panel.
Parameters
----------
current : dict of floats
current to be applied to the panel.
profile_dir : string ('x', 'y', or 'z')
axis along which the field will plotted.
field_component : string ('x', 'y', or 'z')
axis of field to be plotted.
spacing : float
distance between points
min_pos : float
negative disctance from (0,0) in meters
max_pos : float
positive disctance from (0,0) in meters
ax : Axes
matplotlib axes on which to create the plot
"""
plot_panel_profile(self, current, profile_dir, field_component,
spacing, min_pos, max_pos, ax)
def to_dict(self):
panel_dict = dict()
for key, pcb in self.pcbs.items():
panel_dict[key] = pcb.to_dict()
return panel_dict
[docs]
def plot_panel_profile(panel, current, profile_dir='x', field_component='x',
spacing=0.01, min_pos=-0.2, max_pos=0.2, ax=None):
"""Plot line profile for panel.
Parameters
----------
panel : PCBPanel
panel to plot the field for.
current : dict of floats
current to be applied to the panel.
profile_dir : string ('x', 'y', or 'z')
axis along which the field will plotted.
field_component : string ('x', 'y', or 'z')
axis of field to be plotted.
spacing : float
distance between points
min_pos : float
negative disctance from (0,0) in meters
max_pos : float
positive disctance from (0,0) in meters
ax : Axes
matplotlib axes on which to create the plot
"""
if not ax:
_, ax = plt.subplots()
points = np.arange(min_pos, max_pos, spacing)
n_points = np.shape(points)[0]
profile_ax = dict(x=1, y=2, z=0)[profile_dir]
target_points = np.zeros((n_points, 3))
target_points[:, profile_ax] = points
field = panel.magnetic_field(target_points, current=current)
field_ax = dict(x=1, y=2, z=0)[field_component]
ax.plot(points, field[:, field_ax] * 1e9, 'bo-') # plot in nT
ax.set_ylabel(f'B{field_component} (nT)')
ax.set_xlabel(f'{profile_dir} (m)')
[docs]
def plot_field_colormap(field, grid, axis, ax=None, colorbar=True,
vmin=None, vmax=None):
"""Plot colormap of one field axis along an x-z plane between panels.
Parameters
----------
field : array of 3D points
x,y,z field values to be plotted
grid : array of floats
values of distances to used to get target point grid.
axis : string ('x', 'y', or 'z')
what field component to plot
ax : matplotlib element
matplotlib subplot to use. If none, a new one is created.
colorbar : bool
whether to show a colorbar.
"""
if not ax:
ax = plt.figure().add_subplot()
opt = {'y': 2, 'x': 1, 'z': 0}
vals = dict()
if vmin:
vals['vmin'] = vmin
if vmax:
vals['vmax'] = vmax
cm = ax.pcolormesh(grid, grid,
field[:, opt[axis]].reshape(len(grid), len(grid)), **vals)
ax.set_xlabel('z (m)')
ax.set_ylabel('x (m)')
if colorbar:
fig = ax.get_figure()
fig.colorbar(cm, label=f'B{axis} (nT)')
def plot_field_arrows(target_points, field, ax=None, field_mult=1e7):
"""Plot x, z field components as arrows on a 2D grid
Parameters
----------
target_points : array of 3D points
target points for which fields will be plotted.
field : array of 3D points
x, y, z field values at each target point
ax : matplotlib element
matplotlib subplot to use. If none, a new one is created.
field_mult : float
value by which to multiply field values so they are visible in plot.
"""
if not ax:
ax = plt.figure().add_subplot()
ax.quiver(target_points[:, 0], target_points[:, 1],
field[:, 0] * field_mult, field[:, 1] * field_mult,
scale_units='xy', scale=1)
def check_half_names(pcb_folder):
halves = ['top', 'bott', 'first', 'second']
use_names = list()
for half in halves:
if os.path.isdir(os.path.join(pcb_folder, half)):
use_names.append(half)
return use_names
[docs]
def load_panel(path, standoff=1.4, flip=None, rearrange=False):
"""Loads and returns PCBPanel
Parameters
----------
path : string
path to pcb folder.
standoff : float
distance between the two instances of the board.
flip : list of str | dict
names of which halves of the pcbs to flip. If it is
a dict, the keys are the names of the halves and values
are the chain index.
rearrange : bool
whether to treat 'first/second' halves as 'top/bott'
Returns
-------
panel : PCBPanel
panel object for the given pcb panel.
"""
if os.path.exists(path):
half_names = check_half_names(path)
else:
raise FileExistsError(f'{path} does not exist')
panel = PCBPanel(path, half_names=half_names,
standoff=standoff, rearrange=rearrange)
if flip is None:
flip = dict()
if isinstance(flip, list):
flip = {k: None for k in flip}
for item in flip:
if item not in panel.chains:
raise ValueError(f'Cannot flip {item}, not a valid panel.')
if flip[item] is None:
flip_chains(panel.chains[item])
elif isinstance(flip[item], list):
flip_chains([panel.chains[item][idx] for idx in flip[item]])
return panel
[docs]
def plot_panel(panel, target_size, n_points, current,
axis, title='', show=True):
"""Creates 2d colormap, line profile, and arrow field map for coil.
Parameters
----------
panel : PCBPanel
panel to be plotted.
target_size : float
length of the side of the square/line to be considered when plotting.
n_points : int
number of points per side for the square grid.
current : dict of floats
current to be applied to the panels when computing field.
axis : string ('x', 'y', 'z')
which field axis to plot values for.
title : string
title of the plot
show : bool
If True, show the plot
"""
center = np.array([0, 0, 0])
target_points, grid = get_2D_point_grid(center, n=n_points,
sidelength=target_size)
target_points[:, [0, 2]] = target_points[:, [2, 0]]
field = panel.magnetic_field(target_points, current=current)
fig, ax = plt.subplots(3, layout='constrained')
plot_field_colormap(field, grid, axis, ax[0])
plot_field_arrows(target_points, field, ax[2])
panel.plot_profile(current, profile_dir='z',
field_component=axis, ax=ax[1],
min_pos=-(target_size/2),
max_pos=target_size/2)
if title:
fig.suptitle(title)
if show:
plt.show()
# panel.plot(target_points, current) # 3D
[docs]
def combined_panel_field(panels, currents, target_points):
"""Compute field from multiple sets of panels
Parameters
----------
panels : list of PCBPanels
each panel to compute field for.
currents : list of currents (dict of floats)
currents to be applied to each panel.
target_points : list of points (3d points ndarray)
points where to compute fields.
"""
field = 0.
for panel, current in zip(panels, currents):
field += panel.magnetic_field(target_points, current=current)
return field
[docs]
def plot_combined_panels(panels, currents, target_points, show=True):
"""Plots multiple panels and their combined fields in 3D
Parameters
----------
panels : list of PCBPanels
each panel to be plotted and field to be computed.
currents : list of currents (dict of floats)
currents to be applied to each panel
target_points : list of points (3d points ndarray)
points where to compute fields
"""
pl = pv.Plotter()
for panel in panels:
panel.plot(pl=pl, show=False)
field = combined_panel_field(panels, currents, target_points)
pl.add_arrows(target_points, field, mag=1e7)
pl.remove_scalar_bar()
if show:
pl.show()