Trying out some new visualisations

This commit is contained in:
Chris Greening 2022-11-19 08:24:27 +00:00
parent dcc9f8f7ec
commit 3db8c78be0
16 changed files with 2889 additions and 300 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

2
.gitignore vendored
View file

@ -161,3 +161,5 @@ cython_debug/
*.json *.json
*.png *.png
*.csv
*.vol

View file

@ -25,6 +25,10 @@ ln -s ${PWD}/coil_plugin.py ~/Documents/KiCad/6.0/scripting/plugins/coil_plugin.
The project is still very experimental - so there are no guarantees that these will work or do anything useful... The project is still very experimental - so there are no guarantees that these will work or do anything useful...
* 6 circular coils - https://www.pcbway.com/project/shareproject/Proof_of_concept_6_coil_spiral_PCB_stator_23ae7370.html - 6 circular coils - https://www.pcbway.com/project/shareproject/Proof_of_concept_6_coil_spiral_PCB_stator_23ae7370.html
* 6 wedge coils - https://www.pcbway.com/project/shareproject/Proof_of_concept_6_coil_wedge_PCB_stator_c231a379.html - 6 wedge coils - https://www.pcbway.com/project/shareproject/Proof_of_concept_6_coil_wedge_PCB_stator_c231a379.html
* 12 wedge coils - https://www.pcbway.com/project/shareproject/Proof_of_concept_12_coil_wedge_PCB_stator_c54d9374.html - 12 wedge coils - https://www.pcbway.com/project/shareproject/Proof_of_concept_12_coil_wedge_PCB_stator_c54d9374.html
## Building python to take advantage of the M1 Mac Multiprocessors
https://www.reddit.com/r/Python/comments/qog8x3/if_you_are_using_apples_m1_macs_compiling_numpy/

View file

@ -106,7 +106,10 @@
"SCREW_HOLE_RADIUS = STATOR_RADIUS\n", "SCREW_HOLE_RADIUS = STATOR_RADIUS\n",
"\n", "\n",
"# Coil params\n", "# Coil params\n",
"# for custom shape\n",
"TURNS = 16\n", "TURNS = 16\n",
"# for spiral\n",
"# TURNS = 17\n",
"COIL_CENTER_RADIUS = 19.95\n", "COIL_CENTER_RADIUS = 19.95\n",
"COIL_VIA_RADIUS = 20.95" "COIL_VIA_RADIUS = 20.95"
] ]
@ -120,7 +123,12 @@
"# where to put the input pads\n", "# where to put the input pads\n",
"INPUT_PAD_RADIUS = STATOR_RADIUS - (PAD_WIDTH / 2 + VIA_DIAM + TRACK_SPACING)\n", "INPUT_PAD_RADIUS = STATOR_RADIUS - (PAD_WIDTH / 2 + VIA_DIAM + TRACK_SPACING)\n",
"\n", "\n",
"USE_SPIRAL = False\n", "USE_SPIRAL = True\n",
"\n",
"if USE_SPIRAL:\n",
" TURNS = 18\n",
" COIL_VIA_RADIUS = 20.5\n",
" COIL_CENTER_RADIUS = 20.5\n",
"\n", "\n",
"LAYERS = 4" "LAYERS = 4"
] ]
@ -238,18 +246,63 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"def get_spiral(turns, start_radius, thickness, layer=Layer.FRONT):\n",
" points = []\n",
" # create a starting point in the center\n",
" for angle in np.arange(0, turns * 360, 1):\n",
" radius = start_radius + thickness * angle / 360\n",
" if layer == Layer.BACK:\n",
" x = radius * np.cos(np.deg2rad(angle + 180))\n",
" y = radius * np.sin(np.deg2rad(angle + 180))\n",
" points.append((x, -y))\n",
" else:\n",
" x = radius * np.cos(np.deg2rad(angle))\n",
" y = radius * np.sin(np.deg2rad(angle))\n",
" points.append((x, y))\n",
" return points"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if not USE_SPIRAL:\n",
" print(\"Not using spiral\")\n",
" template_f = []\n", " template_f = []\n",
" for i in range(len(template)):\n", " for i in range(len(template)):\n",
" template_f.append(template[len(template) - i - len(template) // 2])\n", " template_f.append(template[len(template) - i - len(template) // 2])\n",
" template_f = flip_x(template_f)\n", " template_f = flip_x(template_f)\n",
" points_f = chaikin(\n", " points_f = chaikin(\n",
" optimize_points(flip_x(get_points(template_f, TURNS, TRACK_SPACING + TRACK_WIDTH))),\n", " optimize_points(\n",
" flip_x(get_points(template_f, TURNS, TRACK_SPACING + TRACK_WIDTH))\n",
" ),\n",
" 2,\n", " 2,\n",
" )\n", " )\n",
" points_b = chaikin(\n", " points_b = chaikin(\n",
" optimize_points(get_points(template, TURNS, TRACK_SPACING + TRACK_WIDTH)), 2\n", " optimize_points(get_points(template, TURNS, TRACK_SPACING + TRACK_WIDTH)), 2\n",
" )\n", " )\n",
"else:\n",
" print(\"Using spiral\")\n",
" points_f = get_spiral(\n",
" TURNS, VIA_DIAM / 2 + TRACK_SPACING, TRACK_SPACING + TRACK_WIDTH, Layer.FRONT\n",
" )\n",
" points_b = get_spiral(\n",
" TURNS, VIA_DIAM / 2 + TRACK_SPACING, TRACK_SPACING + TRACK_WIDTH, Layer.BACK\n",
" )\n",
"\n", "\n",
" points_f = [(0, 0)] + points_f\n",
" points_b = [(0, 0)] + points_b\n",
" print(\"Track points\", len(points_f), len(points_b))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"points_f = [(COIL_VIA_RADIUS - COIL_CENTER_RADIUS, 0)] + points_f\n", "points_f = [(COIL_VIA_RADIUS - COIL_CENTER_RADIUS, 0)] + points_f\n",
"points_b = [(COIL_VIA_RADIUS - COIL_CENTER_RADIUS, 0)] + points_b\n", "points_b = [(COIL_VIA_RADIUS - COIL_CENTER_RADIUS, 0)] + points_b\n",
"\n", "\n",
@ -269,40 +322,75 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# write the coil out in a format that can be simulated\n", "# write the coil out in a format that can be simulated\n",
"# shift the coils aronnd to make connections a bit easier\n", "# rotate the points by 90 degrees so that the x axis is horizontal\n",
"COIL_ROTATION = -360 / 12\n", "pf = rotate(points_f, 90)\n",
"pb = rotate(points_b, 90)\n",
"fname = \"simulations/coils/coil_12_custom\"\n",
"if USE_SPIRAL:\n",
" fname = \"simulations/coils/coil_12_spiral\"\n",
"\n", "\n",
"for i in range(12):\n", "# write the coil out in a format that can be simulated\n",
" angle = i * 360 / 12 + COIL_ROTATION\n", "p_f = rotate(points_f, 90)\n",
" p_f = scale(translate(rotate(points_f, angle), COIL_CENTER_RADIUS, angle), 0.1)\n", "p_b = rotate(points_b, 90)\n",
" p_b = scale(translate(rotate(points_b, angle), COIL_CENTER_RADIUS, angle), 0.1)\n",
"\n", "\n",
" # we'll have positive current through the A coils, and negative current through the B coils\n", "with open(fname + \".csv\", \"w\") as f:\n",
" current = 0\n", " for point in p_f[::-1]:\n",
" if i % 3 == 0:\n", " f.write(f\"{point[0]},{point[1]},0,0.5\\n\")\n",
" current = 0.5\n",
" elif i % 3 == 1:\n",
" current = -0.5\n",
"\n", "\n",
"# two layer board\n", "# two layer board\n",
" with open(f\"simulations/coil-2-{i}.txt\", \"wt\") as f:\n", "with open(fname + \"-2-layer.csv\", \"wt\") as f:\n",
" for point in p_f[::-1]:\n", " for point in p_f[::-1]:\n",
" f.write(f\"{point[0]},{point[1]},0, {current}\\n\")\n", " f.write(f\"{point[0]},{point[1]},0,0.5\\n\")\n",
" for point in p_b:\n", " for point in p_b:\n",
" f.write(f\"{point[0]},{point[1]},0-0.62,{current}\\n\")\n", " f.write(f\"{point[0]},{point[1]},0-0.062,0.5\\n\")\n",
"\n", "\n",
" # four layer board\n", "# all four layer board\n",
" with open(f\"simulations/coil-4-top-{i}.txt\", \"wt\") as f:\n", "with open(fname + \"-4-layer.csv\", \"wt\") as f:\n",
" for point in p_f[::-1]:\n", " for point in p_f[::-1]:\n",
" f.write(f\"{point[0]},{point[1]},0,{current}\\n\")\n", " f.write(f\"{point[0]},{point[1]},0,0.5\\n\")\n",
" for point in p_b:\n", " for point in p_b:\n",
" f.write(f\"{point[0]},{point[1]},0-0.11,{current}\\n\")\n", " f.write(f\"{point[0]},{point[1]},0-0.011,0.5\\n\")\n",
" for point in p_f[::-1]:\n",
" f.write(f\"{point[0]},{point[1]},0-(0.011+0.04),0.5\\n\")\n",
" for point in p_b:\n",
" f.write(f\"{point[0]},{point[1]},0-(0.011+0.011+0.04),0.5\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# write the coil out in a format that can be simulated\n",
"# shift the coils aronnd to make connections a bit easier\n",
"pf = rotate(points_f, 90)\n",
"pb = rotate(points_b, 90)\n",
"fname = \"simulations/coils/coil_12_custom\"\n",
"if USE_SPIRAL:\n",
" fname = \"simulations/coils/coil_12_spiral\"\n",
"\n", "\n",
" with open(f\"simulations/coil-4-bottom-{i}.txt\", \"wt\") as f:\n", "with open(fname + \".csv\", \"w\") as f:\n",
" for point in p_f[::-1]:\n", " for point in pf:\n",
" f.write(f\"{point[0]},{point[1]},0-(0.11+0.4),{current}\\n\")\n", " f.write(f\"{point[0]/10},{point[1]/10},0,0.5\\n\")\n",
" for point in p_b:\n", "\n",
" f.write(f\"{point[0]},{point[1]},0-(0.11+0.11+0.4),{current}\\n\")" "# two layer board\n",
"with open(fname + \"-2-layer.csv\", \"wt\") as f:\n",
" for point in pf[::-1]:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0,0.5\\n\")\n",
" for point in pb:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0-0.062,0.5\\n\")\n",
"\n",
"# all four layer board\n",
"with open(fname + \"-4-layer.csv\", \"wt\") as f:\n",
" for point in pf[::-1]:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0,0.5\\n\")\n",
" for point in pb:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0-0.011,0.5\\n\")\n",
" for point in pf[::-1]:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0-(0.011+0.04),0.5\\n\")\n",
" for point in pb:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0-(0.011+0.011+0.04),0.5\\n\")"
] ]
}, },
{ {
@ -538,35 +626,35 @@
"\n", "\n",
"outer_cuts = (\n", "outer_cuts = (\n",
" draw_arc(-45 + nibble_angle_size / 2, 45 - nibble_angle_size / 2, STATOR_RADIUS, 5)\n", " draw_arc(-45 + nibble_angle_size / 2, 45 - nibble_angle_size / 2, STATOR_RADIUS, 5)\n",
" # + translate(\n", " + translate(\n",
" # rotate(draw_arc(1, 179, SCREW_HOLE_DRILL_DIAM / 2, 5)[::-1], 135),\n", " rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5)[::-1], 135),\n",
" # STATOR_RADIUS,\n", " STATOR_RADIUS,\n",
" # 45,\n", " 45,\n",
" # )\n", " )\n",
" + draw_arc(\n", " + draw_arc(\n",
" 45 + nibble_angle_size / 2, 135 - nibble_angle_size / 2, STATOR_RADIUS, 5\n", " 45 + nibble_angle_size / 2, 135 - nibble_angle_size / 2, STATOR_RADIUS, 5\n",
" )\n", " )\n",
" # + translate(\n", " + translate(\n",
" # rotate(draw_arc(1, 179, SCREW_HOLE_DRILL_DIAM / 2, 5), 225)[::-1],\n", " rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5), 225)[::-1],\n",
" # STATOR_RADIUS,\n", " STATOR_RADIUS,\n",
" # 135,\n", " 135,\n",
" # )\n", " )\n",
" + draw_arc(\n", " + draw_arc(\n",
" 135 + nibble_angle_size / 2, 225 - nibble_angle_size / 2, STATOR_RADIUS, 5\n", " 135 + nibble_angle_size / 2, 225 - nibble_angle_size / 2, STATOR_RADIUS, 5\n",
" )\n", " )\n",
" # + translate(\n", " + translate(\n",
" # rotate(draw_arc(1, 179, SCREW_HOLE_DRILL_DIAM / 2, 5), 315)[::-1],\n", " rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5), 315)[::-1],\n",
" # STATOR_RADIUS,\n", " STATOR_RADIUS,\n",
" # 225,\n", " 225,\n",
" # )\n", " )\n",
" + draw_arc(\n", " + draw_arc(\n",
" 225 + nibble_angle_size / 2, 315 - nibble_angle_size / 2, STATOR_RADIUS, 5\n", " 225 + nibble_angle_size / 2, 315 - nibble_angle_size / 2, STATOR_RADIUS, 5\n",
" )\n", " )\n",
" # + translate(\n", " + translate(\n",
" # rotate(draw_arc(1, 179, SCREW_HOLE_DRILL_DIAM / 2, 5), 45)[::-1],\n", " rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5), 45)[::-1],\n",
" # STATOR_RADIUS,\n", " STATOR_RADIUS,\n",
" # 315,\n", " 315,\n",
" # )\n", " )\n",
")\n", ")\n",
"\n", "\n",
"edge_cuts = [\n", "edge_cuts = [\n",

View file

@ -65,6 +65,7 @@
"# PCB Edge size\n", "# PCB Edge size\n",
"STATOR_RADIUS = 25\n", "STATOR_RADIUS = 25\n",
"STATOR_HOLE_RADIUS = 5.5\n", "STATOR_HOLE_RADIUS = 5.5\n",
"HOLE_SPACE = 2\n",
"\n", "\n",
"# where to puth the mounting pins\n", "# where to puth the mounting pins\n",
"SCREW_HOLE_DRILL_DIAM = 2.3 # 2.3mm drill for a 2mm screw\n", "SCREW_HOLE_DRILL_DIAM = 2.3 # 2.3mm drill for a 2mm screw\n",
@ -72,16 +73,41 @@
"\n", "\n",
"HOLE_SPACING = 0.25\n", "HOLE_SPACING = 0.25\n",
"\n", "\n",
"# where to put the input pads\n",
"INPUT_PAD_RADIUS = STATOR_RADIUS - (PAD_WIDTH / 2 + VIA_DIAM + TRACK_SPACING)\n",
"\n",
"# Coil params\n", "# Coil params\n",
"TURNS = 26\n", "TURNS = 26\n",
"COIL_CENTER_RADIUS = 15\n", "COIL_CENTER_RADIUS = 15\n",
"COIL_VIA_RADIUS = 16\n", "COIL_VIA_RADIUS = 15.3"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Large 30 mm version\n",
"\n", "\n",
"# where to place the pins\n", "# PCB Edge size\n",
"# CONNECTION_PINS_RADIUS = 16.5\n", "# STATOR_RADIUS = 30\n",
"\n",
"# # where to puth the mounting pins\n",
"# SCREW_HOLE_DRILL_DIAM = 2.3 # 2.3mm drill for a 2mm screw\n",
"# SCREW_HOLE_RADIUS = STATOR_RADIUS\n",
"\n",
"# # Coil params\n",
"# TURNS = 31\n",
"# COIL_CENTER_RADIUS = 19\n",
"# COIL_VIA_RADIUS = 19.3"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# where to put the input pads\n",
"INPUT_PAD_RADIUS = STATOR_RADIUS - (PAD_WIDTH / 2 + VIA_DIAM + TRACK_SPACING)\n",
"\n", "\n",
"USE_SPIRAL = False\n", "USE_SPIRAL = False\n",
"\n", "\n",
@ -218,8 +244,8 @@
" optimize_points(get_points(template, TURNS, TRACK_SPACING + TRACK_WIDTH)), 2\n", " optimize_points(get_points(template, TURNS, TRACK_SPACING + TRACK_WIDTH)), 2\n",
" )\n", " )\n",
"\n", "\n",
" points_f = [(0, 0)] + points_f\n", " points_f = [(COIL_VIA_RADIUS - COIL_CENTER_RADIUS, 0)] + points_f\n",
" points_b = [(0, 0)] + points_b\n", " points_b = [(COIL_VIA_RADIUS - COIL_CENTER_RADIUS, 0)] + points_b\n",
"\n", "\n",
" df = pd.DataFrame(points_f, columns=[\"x\", \"y\"])\n", " df = pd.DataFrame(points_f, columns=[\"x\", \"y\"])\n",
" ax = df.plot.line(x=\"x\", y=\"y\", color=\"blue\")\n", " ax = df.plot.line(x=\"x\", y=\"y\", color=\"blue\")\n",
@ -237,27 +263,7 @@
"execution_count": null, "execution_count": null,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": []
"# write the coil out in a format that can be simulated\n",
"\n",
"# two layer board\n",
"with open(\"simulations/coil-2.txt\", \"wt\") as f:\n",
" for point in points_f[::-1]:\n",
" f.write(f\"{point[0]},{point[1]},0,0.5\\n\")\n",
" for point in points_b:\n",
" f.write(f\"{point[0]},{point[1]},0-0.62,0.5\\n\")\n",
"\n",
"# four layer board\n",
"with open(\"simulations/coil-4.txt\", \"wt\") as f:\n",
" for point in points_f[::-1]:\n",
" f.write(f\"{point[0]},{point[1]},0,0.5\\n\")\n",
" for point in points_b:\n",
" f.write(f\"{point[0]},{point[1]},0-0.11,0.5\\n\")\n",
" for point in points_f[::-1]:\n",
" f.write(f\"{point[0]},{point[1]},0-(0.11+0.4),0.5\\n\")\n",
" for point in points_b:\n",
" f.write(f\"{point[0]},{point[1]},0-(0.11+0.11+0.4),0.5\\n\")"
]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
@ -309,6 +315,50 @@
" print(\"Using template\")" " print(\"Using template\")"
] ]
}, },
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Write out coils for simulation"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# write the coil out in a format that can be simulated\n",
"# rotate the points by 90 degrees so that the x axis is horizontal\n",
"pf = rotate(points_f, 90)\n",
"pb = rotate(points_b, 90)\n",
"fname = \"simulations/coils/coil_6_custom\"\n",
"if USE_SPIRAL:\n",
" fname = \"simulations/coils/coil_6_spiral\"\n",
"\n",
"with open(fname + \".csv\", \"w\") as f:\n",
" for point in pf:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0,0.5\\n\")\n",
"\n",
"# two layer board\n",
"with open(fname + \"-2-layer.csv\", \"wt\") as f:\n",
" for point in pf[::-1]:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0,0.5\\n\")\n",
" for point in pb:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0-0.062,0.5\\n\")\n",
"\n",
"# all four layer board\n",
"with open(fname + \"-4-layer.csv\", \"wt\") as f:\n",
" for point in pf[::-1]:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0,0.5\\n\")\n",
" for point in pb:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0-0.011,0.5\\n\")\n",
" for point in pf[::-1]:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0-(0.011+0.04),0.5\\n\")\n",
" for point in pb:\n",
" f.write(f\"{point[0]/10},{point[1]/10},0-(0.011+0.011+0.04),0.5\\n\")"
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
@ -403,12 +453,12 @@
"tracks_b.append(coil_C_opp_b)\n", "tracks_b.append(coil_C_opp_b)\n",
"\n", "\n",
"# connect the front and back coils together\n", "# connect the front and back coils together\n",
"vias.append(create_via(get_arc_point(angle_A, COIL_CENTER_RADIUS)))\n", "vias.append(create_via(get_arc_point(angle_A, COIL_VIA_RADIUS)))\n",
"vias.append(create_via(get_arc_point(angle_B, COIL_CENTER_RADIUS)))\n", "vias.append(create_via(get_arc_point(angle_B, COIL_VIA_RADIUS)))\n",
"vias.append(create_via(get_arc_point(angle_C, COIL_CENTER_RADIUS)))\n", "vias.append(create_via(get_arc_point(angle_C, COIL_VIA_RADIUS)))\n",
"vias.append(create_via(get_arc_point(angle_A_opp, COIL_CENTER_RADIUS)))\n", "vias.append(create_via(get_arc_point(angle_A_opp, COIL_VIA_RADIUS)))\n",
"vias.append(create_via(get_arc_point(angle_B_opp, COIL_CENTER_RADIUS)))\n", "vias.append(create_via(get_arc_point(angle_B_opp, COIL_VIA_RADIUS)))\n",
"vias.append(create_via(get_arc_point(angle_C_opp, COIL_CENTER_RADIUS)))\n", "vias.append(create_via(get_arc_point(angle_C_opp, COIL_VIA_RADIUS)))\n",
"\n", "\n",
"# connect the front copper opposite coils together\n", "# connect the front copper opposite coils together\n",
"common_connection_radius = SCREW_HOLE_RADIUS - (\n", "common_connection_radius = SCREW_HOLE_RADIUS - (\n",
@ -426,7 +476,7 @@
"# vias.append(create_via(get_arc_point(angle_C_opp, common_connection_radius)))\n", "# vias.append(create_via(get_arc_point(angle_C_opp, common_connection_radius)))\n",
"\n", "\n",
"# wires for connecting to opposite coils\n", "# wires for connecting to opposite coils\n",
"connection_radius1 = STATOR_HOLE_RADIUS + (2 * TRACK_SPACING)\n", "connection_radius1 = STATOR_HOLE_RADIUS + (2 * TRACK_SPACING + HOLE_SPACING)\n",
"connection_radius2 = connection_radius1 + (2 * TRACK_SPACING + VIA_DIAM / 2)\n", "connection_radius2 = connection_radius1 + (2 * TRACK_SPACING + VIA_DIAM / 2)\n",
"\n", "\n",
"# draw a 45 degree line from each coil at connection radius 1\n", "# draw a 45 degree line from each coil at connection radius 1\n",
@ -556,7 +606,7 @@
"outer_cuts = (\n", "outer_cuts = (\n",
" draw_arc(-45 + nibble_angle_size / 2, 45 - nibble_angle_size / 2, STATOR_RADIUS, 5)\n", " draw_arc(-45 + nibble_angle_size / 2, 45 - nibble_angle_size / 2, STATOR_RADIUS, 5)\n",
" + translate(\n", " + translate(\n",
" rotate(draw_arc(1, 179, SCREW_HOLE_DRILL_DIAM / 2, 5)[::-1], 135),\n", " rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5)[::-1], 135),\n",
" STATOR_RADIUS,\n", " STATOR_RADIUS,\n",
" 45,\n", " 45,\n",
" )\n", " )\n",
@ -564,7 +614,7 @@
" 45 + nibble_angle_size / 2, 135 - nibble_angle_size / 2, STATOR_RADIUS, 5\n", " 45 + nibble_angle_size / 2, 135 - nibble_angle_size / 2, STATOR_RADIUS, 5\n",
" )\n", " )\n",
" + translate(\n", " + translate(\n",
" rotate(draw_arc(1, 179, SCREW_HOLE_DRILL_DIAM / 2, 5), 225)[::-1],\n", " rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5), 225)[::-1],\n",
" STATOR_RADIUS,\n", " STATOR_RADIUS,\n",
" 135,\n", " 135,\n",
" )\n", " )\n",
@ -572,7 +622,7 @@
" 135 + nibble_angle_size / 2, 225 - nibble_angle_size / 2, STATOR_RADIUS, 5\n", " 135 + nibble_angle_size / 2, 225 - nibble_angle_size / 2, STATOR_RADIUS, 5\n",
" )\n", " )\n",
" + translate(\n", " + translate(\n",
" rotate(draw_arc(1, 179, SCREW_HOLE_DRILL_DIAM / 2, 5), 315)[::-1],\n", " rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5), 315)[::-1],\n",
" STATOR_RADIUS,\n", " STATOR_RADIUS,\n",
" 225,\n", " 225,\n",
" )\n", " )\n",
@ -580,7 +630,7 @@
" 225 + nibble_angle_size / 2, 315 - nibble_angle_size / 2, STATOR_RADIUS, 5\n", " 225 + nibble_angle_size / 2, 315 - nibble_angle_size / 2, STATOR_RADIUS, 5\n",
" )\n", " )\n",
" + translate(\n", " + translate(\n",
" rotate(draw_arc(1, 179, SCREW_HOLE_DRILL_DIAM / 2, 5), 45)[::-1],\n", " rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5), 45)[::-1],\n",
" STATOR_RADIUS,\n", " STATOR_RADIUS,\n",
" 315,\n", " 315,\n",
" )\n", " )\n",
@ -593,7 +643,7 @@
"\n", "\n",
"# dump out the json version\n", "# dump out the json version\n",
"json_result = dump_json(\n", "json_result = dump_json(\n",
" filename=\"coils_6.json\",\n", " filename=f\"coils_6_{STATOR_RADIUS}.json\",\n",
" track_width=TRACK_WIDTH,\n", " track_width=TRACK_WIDTH,\n",
" pin_diam=PIN_DIAM,\n", " pin_diam=PIN_DIAM,\n",
" pin_drill=PIN_DRILL,\n", " pin_drill=PIN_DRILL,\n",

579
coil_generator-radial.ipynb Normal file
View file

@ -0,0 +1,579 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"\n",
"import matplotlib.pyplot as plt\n",
"from skspatial.objects import LineSegment, Line, Vector\n",
"\n",
"# some helper functions\n",
"from helpers import (\n",
" get_arc_point,\n",
" draw_arc,\n",
" rotate,\n",
" translate,\n",
" flip_y,\n",
" flip_x,\n",
" optimize_points,\n",
" chaikin,\n",
" rotate_point,\n",
" scale,\n",
")\n",
"from pcb_json import (\n",
" dump_json,\n",
" plot_json,\n",
" create_via,\n",
" create_pad,\n",
" create_pin,\n",
" create_track,\n",
" create_silk,\n",
" create_silk,\n",
" create_mounting_hole,\n",
")\n",
"\n",
"from enum import Enum\n",
"\n",
"Layer = Enum(\"Layer\", \"FRONT BACK\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Track width and spacing\n",
"TRACK_WIDTH = 0.127\n",
"TRACK_SPACING = 0.127\n",
"\n",
"# via defaults\n",
"VIA_DIAM = 0.8\n",
"VIA_DRILL = 0.4\n",
"\n",
"# this is for a 1.27mm pitch pin\n",
"PIN_DIAM = 1.0\n",
"PIN_DRILL = 0.65\n",
"\n",
"# this is for the PCB connector - see https://www.farnell.com/datasheets/2003059.pdf\n",
"PAD_WIDTH = 3\n",
"PAD_HEIGHT = 2\n",
"PAD_PITCH = 2.5\n",
"\n",
"STATOR_HOLE_RADIUS = 5.5\n",
"HOLE_SPACING = 0.25"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Standard 25 mm version\n",
"\n",
"# PCB Edge size\n",
"STATOR_RADIUS = 25\n",
"STATOR_HOLE_RADIUS = 5.5\n",
"\n",
"# where to puth the mounting pins\n",
"SCREW_HOLE_DRILL_DIAM = 2.3 # 2.3mm drill for a 2mm screw\n",
"SCREW_HOLE_RADIUS = STATOR_RADIUS\n",
"\n",
"# Coil params\n",
"TURNS = 12\n",
"COIL_CENTER_RADIUS = 16\n",
"COIL_VIA_RADIUS = 17"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Large 30 mm version\n",
"\n",
"# PCB Edge size\n",
"STATOR_RADIUS = 30\n",
"\n",
"# where to puth the mounting pins\n",
"SCREW_HOLE_DRILL_DIAM = 2.3 # 2.3mm drill for a 2mm screw\n",
"SCREW_HOLE_RADIUS = STATOR_RADIUS\n",
"\n",
"# Coil params\n",
"TURNS = 16\n",
"COIL_CENTER_RADIUS = 19.95\n",
"COIL_VIA_RADIUS = 20.95"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# where to put the input pads\n",
"INPUT_PAD_RADIUS = STATOR_RADIUS - (PAD_WIDTH / 2 + VIA_DIAM + TRACK_SPACING)\n",
"\n",
"USE_SPIRAL = False\n",
"\n",
"LAYERS = 4"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Radial coil generator"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def get_points(spacing, inner_radius, outer_radius, start_angle, end_angle):\n",
" # first calculate the angle step size from the spacing and the inner_radius\n",
" spacing_angle = np.rad2deg(np.arctan2(spacing, inner_radius))\n",
" print(spacing_angle)\n",
" # now calculate the points be iterating from start_angle to end_angle with the spacing_angle\n",
" points = []\n",
" for angle in np.arange(start_angle, end_angle, spacing_angle * 2):\n",
" points.append(get_arc_point(angle, inner_radius))\n",
" points.append(get_arc_point(angle, outer_radius))\n",
" points.append(\n",
" (\n",
" get_arc_point(angle, outer_radius)[0],\n",
" get_arc_point(angle, outer_radius)[1] + spacing,\n",
" )\n",
" )\n",
" points.append(get_arc_point(angle + spacing_angle, inner_radius))\n",
" return points"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"points_f = get_points(\n",
" TRACK_SPACING + TRACK_WIDTH,\n",
" (COIL_CENTER_RADIUS - 10),\n",
" COIL_CENTER_RADIUS + 10,\n",
" -15,\n",
" 15,\n",
")\n",
"\n",
"df = pd.DataFrame(points_f, columns=[\"x\", \"y\"])\n",
"ax = df.plot.line(x=\"x\", y=\"y\", color=\"blue\")\n",
"ax.axis(\"equal\")\n",
"\n",
"# calculat the total length of the track to compute the resistance\n",
"total_length_front = 0\n",
"for i in range(len(points_f) - 1):\n",
" total_length_front += np.linalg.norm(\n",
" np.array(points_f[i + 1]) - np.array(points_f[i])\n",
" )\n",
"print(\"Total length front\", total_length_front)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"spacing = TRACK_SPACING + TRACK_WIDTH\n",
"inner_radius = COIL_CENTER_RADIUS - 10\n",
"outer_radius = COIL_CENTER_RADIUS + 10\n",
"start_angle = -15\n",
"end_angle = 15\n",
"# first calculate the angle step size from the spacing and the inner_radius\n",
"spacing_angle = np.rad2deg(np.arctan2(spacing, inner_radius))\n",
"# now calculate the points be iterating from start_angle to end_angle with the spacing_angle\n",
"i = 0\n",
"count = (end_angle - start_angle) // spacing_angle\n",
"print(spacing_angle, count)\n",
"# for angle in np.arange(start_angle, end_angle, spacing_angle):\n",
"for angle in [start_angle, end_angle]:\n",
" i = i + 1\n",
" current = 1\n",
" if i == 2:\n",
" current = -1\n",
" with open(f\"simulations/radial_{i}.csv\", \"w\") as f:\n",
" points = [\n",
" get_arc_point(angle, inner_radius),\n",
" get_arc_point(angle, outer_radius),\n",
" ]\n",
" points = translate(rotate(points, 90), -22.5, 90)\n",
" for point in points:\n",
" f.write(f\"{point[0]}/10,{point[1]}/10,0,{current}\\n\")\n",
"\n",
"\n",
"# # write the coil out in a format that can be simulated\n",
"p_f = translate(rotate(points_f, 90), -22.5, 90)\n",
"p_b = translate(rotate(points_f, 90), -22.5, 90)\n",
"\n",
"# df = pd.DataFrame(p_f, columns=[\"x\", \"y\"])\n",
"# ax = df.plot.line(x=\"x\", y=\"y\", color=\"blue\")\n",
"# ax.axis(\"equal\")\n",
"\n",
"with open(\"simulations/coils/coil_rad.csv\", \"w\") as f:\n",
" for point in p_f:\n",
" f.write(f\"{point[0]},{point[1]},0,0.5\\n\")\n",
"\n",
"# two layer board\n",
"with open(\"simulations/coils/coil-rad-2-layer.csv\", \"wt\") as f:\n",
" for point in p_f:\n",
" f.write(f\"{point[0]},{point[1]},0,0.5\\n\")\n",
" for point in p_b:\n",
" f.write(f\"{point[0]},{point[1]},{0-0.062},0.5\\n\")\n",
"\n",
"# all four layer board\n",
"with open(\"simulations/coils/coil-rad-4-layer.csv\", \"wt\") as f:\n",
" for point in p_f:\n",
" f.write(f\"{point[0]},{point[1]},0,0.5\\n\")\n",
" for point in p_b:\n",
" f.write(f\"{point[0]},{point[1]},{0-0.011},0.5\\n\")\n",
" for point in p_f:\n",
" f.write(f\"{point[0]},{point[1]},{0-(0.011+0.04)},0.5\\n\")\n",
" for point in p_b:\n",
" f.write(f\"{point[0]},{point[1]},{0-(0.011+0.011+0.04)},0.5\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Generate PCB Layout"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# calculat the total length of the track to compute the resistance\n",
"total_length_front = 0\n",
"for i in range(len(points_f) - 1):\n",
" total_length_front += np.linalg.norm(\n",
" np.array(points_f[i + 1]) - np.array(points_f[i])\n",
" )\n",
"print(\"Total length front\", total_length_front)\n",
"\n",
"total_length_back = 0\n",
"for i in range(len(points_b) - 1):\n",
" total_length_back += np.linalg.norm(\n",
" np.array(points_b[i + 1]) - np.array(points_b[i])\n",
" )\n",
"print(\"Total length back\", total_length_back)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"vias = []\n",
"tracks_f = []\n",
"tracks_b = []\n",
"pads = []\n",
"pins = []\n",
"mounting_holes = []\n",
"silk = []\n",
"\n",
"\n",
"# shift the coils aronnd to make connections a bit easier\n",
"COIL_ROTATION = -360 / 12\n",
"\n",
"coil_angles = []\n",
"for i in range(12):\n",
" angle = i * 360 / 12 + COIL_ROTATION\n",
" coil_angles.append(angle)\n",
"\n",
"# the main coils\n",
"coil_labels = [\"A\", \"B\", \"C\"]\n",
"coils_f = []\n",
"coils_b = []\n",
"for i in range(12):\n",
" angle = coil_angles[i]\n",
" if (i // 3) % 2 == 0:\n",
" coil_A_f = translate(rotate(points_f, angle), COIL_CENTER_RADIUS, angle)\n",
" coil_A_b = translate(rotate(points_b, angle), COIL_CENTER_RADIUS, angle)\n",
" else:\n",
" # slightly nudge the coils so that they don't overlap when flipped\n",
" coil_A_f = translate(rotate(flip_y(points_f), angle), COIL_CENTER_RADIUS, angle)\n",
" coil_A_b = translate(rotate(flip_y(points_b), angle), COIL_CENTER_RADIUS, angle)\n",
" # keep track of the coils\n",
" coils_f.append(coil_A_f)\n",
" coils_b.append(coil_A_b)\n",
"\n",
" tracks_f.append(coil_A_f)\n",
" tracks_b.append(coil_A_b)\n",
" vias.append(create_via(get_arc_point(angle, COIL_VIA_RADIUS)))\n",
" silk.append(\n",
" create_silk(get_arc_point(angle, COIL_CENTER_RADIUS), coil_labels[i % 3])\n",
" )\n",
"\n",
"# raidus for connecting the bottoms of the coils together\n",
"connection_radius1 = STATOR_HOLE_RADIUS + 3 * TRACK_SPACING\n",
"\n",
"# create tracks to link the A coils around the center\n",
"connection_via_radius_A = connection_radius1 + 3 * TRACK_SPACING + VIA_DIAM / 2\n",
"coil_A1_A2_inner = (\n",
" [get_arc_point(coil_angles[0], connection_via_radius_A)]\n",
" + draw_arc(COIL_ROTATION, coil_angles[3], connection_radius1)\n",
" + [get_arc_point(coil_angles[3], connection_via_radius_A)]\n",
")\n",
"tracks_f.append(coil_A1_A2_inner)\n",
"coil_A3_A4_inner = (\n",
" [get_arc_point(coil_angles[6], connection_via_radius_A)]\n",
" + draw_arc(coil_angles[6], coil_angles[9], connection_radius1)\n",
" + [get_arc_point(coil_angles[9], connection_via_radius_A)]\n",
")\n",
"tracks_f.append(coil_A3_A4_inner)\n",
"# connect up the bottoms of the A coils\n",
"coils_b[0].append(coil_A1_A2_inner[0])\n",
"coils_b[3].append(coil_A1_A2_inner[-1])\n",
"coils_b[6].append(coil_A3_A4_inner[0])\n",
"coils_b[9].append(coil_A3_A4_inner[-1])\n",
"# add the vias to stitch them together\n",
"vias.append(create_via(coil_A1_A2_inner[0]))\n",
"vias.append(create_via(coil_A1_A2_inner[-1]))\n",
"vias.append(create_via(coil_A3_A4_inner[0]))\n",
"vias.append(create_via(coil_A3_A4_inner[-1]))\n",
"\n",
"# create tracks to link the B coils around the center - this can all be done on the bottom layer\n",
"coil_B1_B2_inner = draw_arc(coil_angles[1], coil_angles[4], connection_radius1)\n",
"tracks_b.append(coil_B1_B2_inner)\n",
"coil_B3_B4_inner = draw_arc(coil_angles[7], coil_angles[10], connection_radius1)\n",
"tracks_b.append(coil_B3_B4_inner)\n",
"# connect up the bottoms of the A coils\n",
"coils_b[1].append(coil_B1_B2_inner[0])\n",
"coils_b[4].append(coil_B1_B2_inner[-1])\n",
"coils_b[7].append(coil_B3_B4_inner[0])\n",
"coils_b[10].append(coil_B3_B4_inner[-1])\n",
"\n",
"# create tracks to link the C coils around the center\n",
"connection_via_radius_C = connection_via_radius_A + 3 * TRACK_SPACING + VIA_DIAM / 2\n",
"coil_C1_C2_inner = draw_arc(coil_angles[2], coil_angles[5], connection_via_radius_C)\n",
"tracks_f.append(coil_C1_C2_inner)\n",
"coil_C3_C4_inner = draw_arc(coil_angles[8], coil_angles[11], connection_via_radius_C)\n",
"tracks_f.append(coil_C3_C4_inner)\n",
"# connect up the bottoms of the B coils\n",
"coils_b[2].append(coil_C1_C2_inner[0])\n",
"coils_b[5].append(coil_C1_C2_inner[-1])\n",
"coils_b[8].append(coil_C3_C4_inner[0])\n",
"coils_b[11].append(coil_C3_C4_inner[-1])\n",
"# add the vias to stitch them together\n",
"vias.append(create_via(coil_C1_C2_inner[0]))\n",
"vias.append(create_via(coil_C1_C2_inner[-1]))\n",
"vias.append(create_via(coil_C3_C4_inner[0]))\n",
"vias.append(create_via(coil_C3_C4_inner[-1]))\n",
"\n",
"# connect the last three coils together\n",
"common_connection_radius = SCREW_HOLE_RADIUS - (SCREW_HOLE_DRILL_DIAM / 2 + 0.5)\n",
"tracks_f.append(draw_arc(coil_angles[9], coil_angles[11], common_connection_radius))\n",
"coils_f[9].append(get_arc_point(coil_angles[9], common_connection_radius))\n",
"coils_f[10].append(get_arc_point(coil_angles[10], common_connection_radius))\n",
"coils_f[11].append(get_arc_point(coil_angles[11], common_connection_radius))\n",
"\n",
"# connect the outer A coils together\n",
"outer_connection_radius_A = SCREW_HOLE_RADIUS - (SCREW_HOLE_DRILL_DIAM / 2 + 0.5)\n",
"tracks_f.append(draw_arc(coil_angles[3], coil_angles[6], outer_connection_radius_A))\n",
"coils_f[3].append(get_arc_point(coil_angles[3], outer_connection_radius_A))\n",
"coils_f[6].append(get_arc_point(coil_angles[6], outer_connection_radius_A))\n",
"\n",
"# connect the outer B coils together\n",
"outer_connection_radius_B = outer_connection_radius_A - TRACK_SPACING - VIA_DIAM / 2\n",
"tracks_b.append(\n",
" [get_arc_point(coil_angles[4], outer_connection_radius_B)]\n",
" + draw_arc(coil_angles[4], coil_angles[7], outer_connection_radius_A)\n",
" + [get_arc_point(coil_angles[7], outer_connection_radius_B)]\n",
")\n",
"coils_f[4].append(get_arc_point(coil_angles[4], outer_connection_radius_B))\n",
"coils_f[7].append(get_arc_point(coil_angles[7], outer_connection_radius_B))\n",
"vias.append(\n",
" create_via(get_arc_point(4 * 360 / 12 + COIL_ROTATION, outer_connection_radius_B))\n",
")\n",
"vias.append(\n",
" create_via(get_arc_point(7 * 360 / 12 + COIL_ROTATION, outer_connection_radius_B))\n",
")\n",
"\n",
"# connect the outer C coilds together\n",
"outer_connection_radius_C = outer_connection_radius_B - TRACK_SPACING - VIA_DIAM / 2\n",
"tracks_b.append(\n",
" draw_arc(\n",
" 5 * 360 / 12 + COIL_ROTATION,\n",
" 8 * 360 / 12 + COIL_ROTATION,\n",
" outer_connection_radius_C,\n",
" )\n",
")\n",
"coils_f[5].append(\n",
" get_arc_point(5 * 360 / 12 + COIL_ROTATION, outer_connection_radius_C)\n",
")\n",
"coils_f[8].append(\n",
" get_arc_point(8 * 360 / 12 + COIL_ROTATION, outer_connection_radius_C)\n",
")\n",
"vias.append(\n",
" create_via(get_arc_point(5 * 360 / 12 + COIL_ROTATION, outer_connection_radius_C))\n",
")\n",
"vias.append(\n",
" create_via(get_arc_point(8 * 360 / 12 + COIL_ROTATION, outer_connection_radius_C))\n",
")\n",
"\n",
"# create the pads for connecting the inputs to the coils\n",
"silk.append(\n",
" create_silk((INPUT_PAD_RADIUS - PAD_HEIGHT - 2.5, PAD_PITCH), \"C\", \"b\", 2.5, -900)\n",
")\n",
"silk.append(create_silk((INPUT_PAD_RADIUS - PAD_HEIGHT - 2.5, 0), \"B\", \"b\", 2.5, -900))\n",
"silk.append(\n",
" create_silk((INPUT_PAD_RADIUS - PAD_HEIGHT - 2.5, -PAD_PITCH), \"A\", \"b\", 2.5, -900)\n",
")\n",
"\n",
"pads.append(create_pad((INPUT_PAD_RADIUS, -PAD_PITCH), PAD_WIDTH, PAD_HEIGHT, \"b\"))\n",
"pads.append(create_pad((INPUT_PAD_RADIUS, 0), PAD_WIDTH, PAD_HEIGHT, \"b\"))\n",
"pads.append(create_pad((INPUT_PAD_RADIUS, PAD_PITCH), PAD_WIDTH, PAD_HEIGHT, \"b\"))\n",
"\n",
"# connect coil A to the top pad\n",
"pad_connection_point_x = INPUT_PAD_RADIUS\n",
"pad_angle = np.rad2deg(np.arcsin(PAD_PITCH / pad_connection_point_x))\n",
"coils_f[0].append(get_arc_point(coil_angles[0], pad_connection_point_x))\n",
"vias.append(create_via(get_arc_point(coil_angles[0], pad_connection_point_x)))\n",
"# connect coil B to the middle pad\n",
"coils_f[1].append((pad_connection_point_x + PAD_WIDTH / 2 + VIA_DIAM / 2, 0))\n",
"vias.append(create_via(((pad_connection_point_x + PAD_WIDTH / 2 + VIA_DIAM / 2, 0))))\n",
"# connect coil C to the bottom pad\n",
"coils_f[2].append(get_arc_point(coil_angles[2], pad_connection_point_x))\n",
"vias.append(create_via(get_arc_point(coil_angles[2], pad_connection_point_x)))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# if we are doing four layers then duplicate the front and back layers on (front and inner1), (inner2 and back)\n",
"tracks_in1 = []\n",
"tracks_in2 = []\n",
"if LAYERS == 4:\n",
" tracks_in1 = tracks_b.copy()\n",
" tracks_in2 = tracks_f.copy()\n",
"\n",
"# these final bits of wiring up to the input pads don't need to be duplicated\n",
"tracks_b.append(\n",
" [(pad_connection_point_x + PAD_WIDTH / 2, 0), (pad_connection_point_x, 0)]\n",
")\n",
"tracks_b.append(draw_arc(coil_angles[0], -pad_angle, pad_connection_point_x, 1))\n",
"tracks_b.append(draw_arc(coil_angles[2], pad_angle, pad_connection_point_x, 1))\n",
"\n",
"nibble_angle_size = 360 * SCREW_HOLE_DRILL_DIAM / (2 * np.pi * STATOR_RADIUS)\n",
"\n",
"outer_cuts = (\n",
" draw_arc(-45 + nibble_angle_size / 2, 45 - nibble_angle_size / 2, STATOR_RADIUS, 5)\n",
" + translate(\n",
" rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5)[::-1], 135),\n",
" STATOR_RADIUS,\n",
" 45,\n",
" )\n",
" + draw_arc(\n",
" 45 + nibble_angle_size / 2, 135 - nibble_angle_size / 2, STATOR_RADIUS, 5\n",
" )\n",
" + translate(\n",
" rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5), 225)[::-1],\n",
" STATOR_RADIUS,\n",
" 135,\n",
" )\n",
" + draw_arc(\n",
" 135 + nibble_angle_size / 2, 225 - nibble_angle_size / 2, STATOR_RADIUS, 5\n",
" )\n",
" + translate(\n",
" rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5), 315)[::-1],\n",
" STATOR_RADIUS,\n",
" 225,\n",
" )\n",
" + draw_arc(\n",
" 225 + nibble_angle_size / 2, 315 - nibble_angle_size / 2, STATOR_RADIUS, 5\n",
" )\n",
" + translate(\n",
" rotate(draw_arc(5, 175, SCREW_HOLE_DRILL_DIAM / 2, 5), 45)[::-1],\n",
" STATOR_RADIUS,\n",
" 315,\n",
" )\n",
")\n",
"\n",
"edge_cuts = [\n",
" outer_cuts,\n",
" draw_arc(0, 360, STATOR_HOLE_RADIUS, 1),\n",
"]\n",
"\n",
"# dump out the json version\n",
"json_result = dump_json(\n",
" filename=f\"coils_12_{STATOR_RADIUS}mm.json\",\n",
" track_width=TRACK_WIDTH,\n",
" pin_diam=PIN_DIAM,\n",
" pin_drill=PIN_DRILL,\n",
" via_diam=VIA_DIAM,\n",
" via_drill=VIA_DRILL,\n",
" vias=vias,\n",
" pins=pins,\n",
" pads=pads,\n",
" silk=silk,\n",
" tracks_f=tracks_f,\n",
" tracks_in1=tracks_in1,\n",
" tracks_in2=tracks_in2,\n",
" tracks_b=tracks_b,\n",
" mounting_holes=mounting_holes,\n",
" edge_cuts=edge_cuts,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# plot the json\n",
"plot_json(json_result)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.10.7 ('venv': venv)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.7"
},
"vscode": {
"interpreter": {
"hash": "1ce20143987840b9786ebb5907032c9c3a8efacbb887dbb0ebc4934f2ad26cb3"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View file

@ -188,5 +188,34 @@ class CoilPlugin(pcbnew.ActionPlugin):
ec.SetPolyPoints(v) ec.SetPolyPoints(v)
board.Add(ec) board.Add(ec)
# put it on the solder mask as well - who knows why...
for edge_cut in coil_data["edgeCuts"]:
ec = pcbnew.PCB_SHAPE(board)
ec.SetShape(pcbnew.SHAPE_T_POLY)
ec.SetFilled(False)
ec.SetLayer(pcbnew.F_Mask)
ec.SetWidth(int(0.1 * pcbnew.IU_PER_MM))
v = pcbnew.wxPoint_Vector()
for point in edge_cut:
x = point["x"] + CENTER_X
y = point["y"] + CENTER_Y
v.append(pcbnew.wxPointMM(x, y))
ec.SetPolyPoints(v)
board.Add(ec)
for edge_cut in coil_data["edgeCuts"]:
ec = pcbnew.PCB_SHAPE(board)
ec.SetShape(pcbnew.SHAPE_T_POLY)
ec.SetFilled(False)
ec.SetLayer(pcbnew.B_Mask)
ec.SetWidth(int(0.1 * pcbnew.IU_PER_MM))
v = pcbnew.wxPoint_Vector()
for point in edge_cut:
x = point["x"] + CENTER_X
y = point["y"] + CENTER_Y
v.append(pcbnew.wxPointMM(x, y))
ec.SetPolyPoints(v)
board.Add(ec)
CoilPlugin().register() # Instantiate and register to Pcbnew]) CoilPlugin().register() # Instantiate and register to Pcbnew])

View file

@ -4,3 +4,9 @@ numpy
pandas pandas
scikit-spatial scikit-spatial
pre-commit pre-commit
tqdm
plotly
magpylib
pyvista
mayavi
PySide

BIN
simulations/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -1,4 +1,4 @@
''' """
Biot-Savart Magnetic Field Calculator v4.3 Biot-Savart Magnetic Field Calculator v4.3
Mingde Yin Mingde Yin
Ryan Zazo Ryan Zazo
@ -6,7 +6,7 @@ Ryan Zazo
All lengths are in cm, B-field is in G All lengths are in cm, B-field is in G
from - https://github.com/vuthalab/biot-savart from - https://github.com/vuthalab/biot-savart
''' """
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@ -14,17 +14,18 @@ from mpl_toolkits.mplot3d import Axes3D
import matplotlib.cm as cm import matplotlib.cm as cm
import matplotlib.ticker as ticker import matplotlib.ticker as ticker
''' """
Feature Wishlist: Feature Wishlist:
improve plot_coil with different colors for different values of current improve plot_coil with different colors for different values of current
accelerate integrator to use meshgrids directly instead of a layer of for loop accelerate integrator to use meshgrids directly instead of a layer of for loop
get parse_coil to use vectorized function instead of for loop get parse_coil to use vectorized function instead of for loop
''' """
def parse_coil(filename): def parse_coil(filename):
''' """
Parses 4 column CSV into x,y,z,I slices for coil. Parses 4 column CSV into x,y,z,I slices for coil.
Each (x,y,z,I) entry defines a vertex on the coil. Each (x,y,z,I) entry defines a vertex on the coil.
@ -35,28 +36,41 @@ def parse_coil(filename):
- There are 2 amps of current running between points 1 and 2 - There are 2 amps of current running between points 1 and 2
- There are 3 amps of current running between points 2 and 3 - There are 3 amps of current running between points 2 and 3
- The last bit of current is functionally useless. - The last bit of current is functionally useless.
''' """
with open(filename, "r") as f: return np.array([[eval(i) for i in line.split(",")] for line in f.read().splitlines()]).T with open(filename, "r") as f:
return np.array(
[[eval(i) for i in line.split(",")] for line in f.read().splitlines()]
).T
def slice_coil(coil, steplength): def slice_coil(coil, steplength):
''' """
Slices a coil into pieces of size steplength. Slices a coil into pieces of size steplength.
If the coil is already sliced into pieces smaller than that, this does nothing. If the coil is already sliced into pieces smaller than that, this does nothing.
''' """
def interpolate_points(p1, p2, parts): def interpolate_points(p1, p2, parts):
''' """
Produces a series of linearly spaced points between two given points in R3+I Produces a series of linearly spaced points between two given points in R3+I
Linearly interpolates X,Y,Z; but keeps I the SAME Linearly interpolates X,Y,Z; but keeps I the SAME
i.e. (0, 2, 1, 3), (3, 4, 2, 5), parts = 2: i.e. (0, 2, 1, 3), (3, 4, 2, 5), parts = 2:
(0, 2, 1, 3), (1.5, 3, 1.5, 3), (3, 4, 2, 5) (0, 2, 1, 3), (1.5, 3, 1.5, 3), (3, 4, 2, 5)
''' """
return np.column_stack((np.linspace(p1[0], p2[0], parts+1), np.linspace(p1[1], p2[1], parts+1), return np.column_stack(
np.linspace(p1[2], p2[2], parts+1), p1[3] * np.ones((parts+1)))) (
np.linspace(p1[0], p2[0], parts + 1),
np.linspace(p1[1], p2[1], parts + 1),
np.linspace(p1[2], p2[2], parts + 1),
p1[3] * np.ones((parts + 1)),
)
)
newcoil = np.zeros((1, 4)) # fill column with dummy data, we will remove this later. newcoil = np.zeros(
(1, 4)
) # fill column with dummy data, we will remove this later.
segment_starts = coil[:, :-1] segment_starts = coil[:, :-1]
segment_ends = coil[:, 1:] segment_ends = coil[:, 1:]
@ -72,17 +86,21 @@ def slice_coil(coil, steplength):
# determine how many steps we must chop each segment into # determine how many steps we must chop each segment into
for i in range(segments.shape[1]): for i in range(segments.shape[1]):
newrows = interpolate_points(segment_starts[:,i], segment_ends[:,i], stepnumbers[i]) newrows = interpolate_points(
segment_starts[:, i], segment_ends[:, i], stepnumbers[i]
)
# set of new interpolated points to feed in # set of new interpolated points to feed in
newcoil = np.vstack((newcoil, newrows)) newcoil = np.vstack((newcoil, newrows))
if newcoil.shape[0] %2 != 0: newcoil = np.vstack((newcoil, newcoil[-1,:])) if newcoil.shape[0] % 2 != 0:
newcoil = np.vstack((newcoil, newcoil[-1, :]))
## Force the coil to have an even number of segments, for Richardson Extrapolation to work ## Force the coil to have an even number of segments, for Richardson Extrapolation to work
return newcoil[1:, :].T # return non-dummy columns return newcoil[1:, :].T # return non-dummy columns
def calculate_field(coil, x, y, z): def calculate_field(coil, x, y, z):
''' """
Calculates magnetic field vector as a result of some position and current x, y, z, I Calculates magnetic field vector as a result of some position and current x, y, z, I
[In the same coordinate system as the coil] [In the same coordinate system as the coil]
@ -90,15 +108,15 @@ def calculate_field(coil, x, y, z):
x, y, z: position in cm x, y, z: position in cm
Output B-field is a 3-D vector in units of G Output B-field is a 3-D vector in units of G
''' """
FACTOR = 0.1 # = mu_0 / 4pi when lengths are in cm, and B-field is in G FACTOR = 0.1 # = mu_0 / 4pi when lengths are in cm, and B-field is in G
def bs_integrate(start, end): def bs_integrate(start, end):
''' """
Produces tiny segment of magnetic field vector (dB) using the midpoint approximation over some interval Produces tiny segment of magnetic field vector (dB) using the midpoint approximation over some interval
TODO for future optimization: Get this to work with meshgrids directly TODO for future optimization: Get this to work with meshgrids directly
''' """
dl = (end - start).T dl = (end - start).T
mid = (start + end) / 2 mid = (start + end) / 2
position = np.array((x - mid[0], y - mid[1], z - mid[2])).T position = np.array((x - mid[0], y - mid[1], z - mid[2])).T
@ -106,7 +124,11 @@ def calculate_field(coil, x, y, z):
mag = np.sqrt((x - mid[0]) ** 2 + (y - mid[1]) ** 2 + (z - mid[2]) ** 2) mag = np.sqrt((x - mid[0]) ** 2 + (y - mid[1]) ** 2 + (z - mid[2]) ** 2)
# magnitude of the relative position vector # magnitude of the relative position vector
return start[3] * np.cross(dl[:3], position) / np.array((mag ** 3, mag ** 3, mag ** 3)).T return (
start[3]
* np.cross(dl[:3], position)
/ np.array((mag**3, mag**3, mag**3)).T
)
# Apply the Biot-Savart Law to get the differential magnetic field # Apply the Biot-Savart Law to get the differential magnetic field
# current flowing in this segment is represented by start[3] # current flowing in this segment is represented by start[3]
@ -115,77 +137,118 @@ def calculate_field(coil, x, y, z):
# midpoint integration with 1 layer of Richardson Extrapolation # midpoint integration with 1 layer of Richardson Extrapolation
starts, mids, ends = coil[:, :-1:2], coil[:, 1::2], coil[:, 2::2] starts, mids, ends = coil[:, :-1:2], coil[:, 1::2], coil[:, 2::2]
for start, mid, end in np.nditer([starts, mids, ends], flags=['external_loop'], order='F'): for start, mid, end in np.nditer(
[starts, mids, ends], flags=["external_loop"], order="F"
):
# use numpy fast indexing # use numpy fast indexing
fullpart = bs_integrate(start, end) # stage 1 richardson fullpart = bs_integrate(start, end) # stage 1 richardson
halfpart = bs_integrate(start, mid) + bs_integrate(mid, end) # stage 2 richardson halfpart = bs_integrate(start, mid) + bs_integrate(
mid, end
) # stage 2 richardson
B += 4/3 * halfpart - 1/3 * fullpart # richardson extrapolated midpoint rule B += (
4 / 3 * halfpart - 1 / 3 * fullpart
) # richardson extrapolated midpoint rule
return (
B * FACTOR
) # return SUM of all components as 3 (x,y,z) meshgrids for (Bx, By, Bz) component when evaluated using produce_target_volume
return B * FACTOR # return SUM of all components as 3 (x,y,z) meshgrids for (Bx, By, Bz) component when evaluated using produce_target_volume
def produce_target_volume(coil, box_size, start_point, vol_resolution): def produce_target_volume(coil, box_size, start_point, vol_resolution):
''' """
Generates a set of field vector values for each tuple (x, y, z) in the box. Generates a set of field vector values for each tuple (x, y, z) in the box.
Coil: Input Coil Positions in format specified above, already sub-divided into small pieces Coil: Input Coil Positions in format specified above, already sub-divided into small pieces
box_size: (x, y, z) dimensions of the box in cm box_size: (x, y, z) dimensions of the box in cm
start_point: (x, y, z) = (0, 0, 0) = bottom left corner position of the box start_point: (x, y, z) = (0, 0, 0) = bottom left corner position of the box
vol_resolution: Spatial resolution (in cm) vol_resolution: Spatial resolution (in cm)
''' """
x = np.linspace(start_point[0], box_size[0] + start_point[0],int(box_size[0]/vol_resolution)+1) x = np.linspace(
y = np.linspace(start_point[1], box_size[1] + start_point[1],int(box_size[1]/vol_resolution)+1) start_point[0],
z = np.linspace(start_point[2], box_size[2] + start_point[2],int(box_size[2]/vol_resolution)+1) box_size[0] + start_point[0],
int(box_size[0] / vol_resolution) + 1,
)
y = np.linspace(
start_point[1],
box_size[1] + start_point[1],
int(box_size[1] / vol_resolution) + 1,
)
z = np.linspace(
start_point[2],
box_size[2] + start_point[2],
int(box_size[2] / vol_resolution) + 1,
)
# Generate points at regular spacing, incl. end points # Generate points at regular spacing, incl. end points
Z, Y, X = np.meshgrid(z, y, x, indexing='ij') Z, Y, X = np.meshgrid(z, y, x, indexing="ij")
# NOTE: Requires axes to be flipped in order for meshgrid to have the correct dimensional order # NOTE: Requires axes to be flipped in order for meshgrid to have the correct dimensional order
return calculate_field(coil, X, Y, Z) return calculate_field(coil, X, Y, Z)
def get_field_vector(targetVolume, position, start_point, volume_resolution): def get_field_vector(targetVolume, position, start_point, volume_resolution):
''' """
Returns the B vector [Bx, By, Bz] components in a generated Target Volume at a given position tuple (x, y, z) in a coordinate system Returns the B vector [Bx, By, Bz] components in a generated Target Volume at a given position tuple (x, y, z) in a coordinate system
start_point: (x, y, z) = (0, 0, 0) = bottom left corner position of the box start_point: (x, y, z) = (0, 0, 0) = bottom left corner position of the box
volume_resolution: Division of volumetric meshgrid (generate a point every volume_resolution cm) volume_resolution: Division of volumetric meshgrid (generate a point every volume_resolution cm)
''' """
relativePosition = ((np.array(position) - np.array(start_point)) / volume_resolution).astype(int) relativePosition = (
(np.array(position) - np.array(start_point)) / volume_resolution
).astype(int)
# adjust to the meshgrid's system # adjust to the meshgrid's system
if (relativePosition < 0).any(): return ("ERROR: Out of bounds! (negative indices)") if (relativePosition < 0).any():
return "ERROR: Out of bounds! (negative indices)"
try: return targetVolume[relativePosition[0], relativePosition[1], relativePosition[2], :] try:
except: return ("ERROR: Out of bounds!") return targetVolume[
relativePosition[0], relativePosition[1], relativePosition[2], :
]
except:
return "ERROR: Out of bounds!"
# basic error checking to see if you actually got a correct input/output # basic error checking to see if you actually got a correct input/output
'''
"""
- If you are indexing a targetvolume meshgrid on your own, remember to account for the offset (starting point), and spatial resolution - If you are indexing a targetvolume meshgrid on your own, remember to account for the offset (starting point), and spatial resolution
- You will need an index like <relativePosition = ((np.array(position) - np.array(start_point)) / volume_resolution).astype(int)> - You will need an index like <relativePosition = ((np.array(position) - np.array(start_point)) / volume_resolution).astype(int)>
''' """
def write_target_volume(input_filename,output_filename, box_size, start_point,
coil_resolution=1, volume_resolution=1): def write_target_volume(
''' input_filename,
output_filename,
box_size,
start_point,
coil_resolution=1,
volume_resolution=1,
):
"""
Takes a coil specified in input_filename, generates a target volume, and saves the generated target volume to output_filename. Takes a coil specified in input_filename, generates a target volume, and saves the generated target volume to output_filename.
box_size: (x, y, z) dimensions of the box in cm box_size: (x, y, z) dimensions of the box in cm
start_point: (x, y, z) = (0, 0, 0) = bottom left corner position of the box AKA the offset start_point: (x, y, z) = (0, 0, 0) = bottom left corner position of the box AKA the offset
coil_resolution: How long each coil subsegment should be coil_resolution: How long each coil subsegment should be
volume_resolution: Division of volumetric meshgrid (generate a point every volume_resolution cm) volume_resolution: Division of volumetric meshgrid (generate a point every volume_resolution cm)
''' """
coil = parse_coil(input_filename) coil = parse_coil(input_filename)
chopped = slice_coil(coil, coil_resolution) chopped = slice_coil(coil, coil_resolution)
targetVolume = produce_target_volume(chopped, box_size, start_point, volume_resolution) targetVolume = produce_target_volume(
chopped, box_size, start_point, volume_resolution
)
with open(output_filename, "wb") as f: np.save(f, targetVolume) with open(output_filename, "wb") as f:
np.save(f, targetVolume)
# stored in standard numpy pickle form # stored in standard numpy pickle form
def read_target_volume(filename): def read_target_volume(filename):
''' """
Takes the name of a saved target volume and loads the B vector meshgrid. Takes the name of a saved target volume and loads the B vector meshgrid.
Returns None if not found. Returns None if not found.
''' """
targetVolume = None targetVolume = None
try: try:
@ -195,10 +258,20 @@ def read_target_volume(filename):
except: except:
pass pass
## plotting routines ## plotting routines
def plot_fields(Bfields,box_size,start_point,vol_resolution,which_plane='z',level=0,num_contours=50):
''' def plot_fields(
Bfields,
box_size,
start_point,
vol_resolution,
which_plane="z",
level=0,
num_contours=50,
):
"""
Plots the set of Bfields in the given region, at the specified resolutions. Plots the set of Bfields in the given region, at the specified resolutions.
Bfields: A 4D array of the Bfield. Bfields: A 4D array of the Bfield.
@ -209,22 +282,34 @@ def plot_fields(Bfields,box_size,start_point,vol_resolution,which_plane='z',leve
level : The "height" of the plane. For instance the Z = 5 plane would have a level of 5 level : The "height" of the plane. For instance the Z = 5 plane would have a level of 5
num_contours: THe amount of contours on the contour plot. num_contours: THe amount of contours on the contour plot.
''' """
# filled contour plot of Bx, By, and Bz on a chosen slice plane # filled contour plot of Bx, By, and Bz on a chosen slice plane
X = np.linspace(start_point[0], box_size[0] + start_point[0],int(box_size[0]/vol_resolution)+1) X = np.linspace(
Y = np.linspace(start_point[1], box_size[1] + start_point[1],int(box_size[1]/vol_resolution)+1) start_point[0],
Z = np.linspace(start_point[2], box_size[2] + start_point[2],int(box_size[2]/vol_resolution)+1) box_size[0] + start_point[0],
int(box_size[0] / vol_resolution) + 1,
)
Y = np.linspace(
start_point[1],
box_size[1] + start_point[1],
int(box_size[1] / vol_resolution) + 1,
)
Z = np.linspace(
start_point[2],
box_size[2] + start_point[2],
int(box_size[2] / vol_resolution) + 1,
)
print(Z) print(Z)
if which_plane=='x': if which_plane == "x":
converted_level = np.where(X >= level) converted_level = np.where(X >= level)
B_sliced = [Bfields[converted_level[0][0], :, :, i].T for i in range(3)] B_sliced = [Bfields[converted_level[0][0], :, :, i].T for i in range(3)]
x_label, y_label = "y", "z" x_label, y_label = "y", "z"
x_array, y_array = Y, Z x_array, y_array = Y, Z
elif which_plane=='y': elif which_plane == "y":
converted_level = np.where(Y >= level) converted_level = np.where(Y >= level)
B_sliced = [Bfields[:, converted_level[0][0], :, i].T for i in range(3)] B_sliced = [Bfields[:, converted_level[0][0], :, i].T for i in range(3)]
x_label, y_label = "x", "z" x_label, y_label = "x", "z"
@ -238,34 +323,42 @@ def plot_fields(Bfields,box_size,start_point,vol_resolution,which_plane='z',leve
Bmin, Bmax = np.amin(B_sliced), np.amax(B_sliced) Bmin, Bmax = np.amin(B_sliced), np.amax(B_sliced)
component_labels = ['x','y','z'] component_labels = ["x", "y", "z"]
fig, axes = plt.subplots(nrows=4, ncols=1, figsize=(10, 40)) fig, axes = plt.subplots(nrows=4, ncols=1, figsize=(10, 40))
axes[0].set_ylabel(y_label + " (cm)") axes[0].set_ylabel(y_label + " (cm)")
for i in range(3): for i in range(3):
contours = axes[i].contourf(x_array,y_array,B_sliced[i], contours = axes[i].contourf(
vmin=Bmin,vmax=Bmax, x_array,
cmap=cm.magma,levels=num_contours) y_array,
B_sliced[i],
vmin=Bmin,
vmax=Bmax,
cmap=cm.magma,
levels=num_contours,
)
axes[i].set_xlabel(x_label + " (cm)") axes[i].set_xlabel(x_label + " (cm)")
axes[i].set_title("$\mathcal{B}$" + "$_{}$".format(component_labels[i])) axes[i].set_title("$\mathcal{B}$" + "$_{}$".format(component_labels[i]))
axes[3].set_aspect(100) axes[3].set_aspect(100)
fig.colorbar(contours,cax=axes[3],extend='both') fig.colorbar(contours, cax=axes[3], extend="both")
plt.tight_layout() plt.tight_layout()
plt.show() plt.show()
return plt
def plot_coil(*input_filenames): def plot_coil(*input_filenames):
''' """
Plots one or more coils in space. Plots one or more coils in space.
input_filenames: Name of the files containing the coils. input_filenames: Name of the files containing the coils.
Should be formatted appropriately. Should be formatted appropriately.
''' """
fig = plt.figure() fig = plt.figure()
tick_spacing = 2 tick_spacing = 2
ax = fig.add_subplot(111, projection='3d') ax = fig.add_subplot(111, projection="3d")
ax.set_xlabel("$x$ (cm)") ax.set_xlabel("$x$ (cm)")
ax.set_ylabel("$y$ (cm)") ax.set_ylabel("$y$ (cm)")
ax.set_zlabel("$z$ (cm)") ax.set_zlabel("$z$ (cm)")
@ -273,29 +366,61 @@ def plot_coil(*input_filenames):
for input_filename in input_filenames: for input_filename in input_filenames:
coil_points = np.array(parse_coil(input_filename)) coil_points = np.array(parse_coil(input_filename))
ax.plot3D(coil_points[0,:],coil_points[1,:],coil_points[2,:],lw=2) ax.plot3D(
coil_points[0, :], coil_points[1, :], coil_points[2, :], lw=2, color="red"
)
for axis in [ax.xaxis, ax.yaxis, ax.zaxis]: for axis in [ax.xaxis, ax.yaxis, ax.zaxis]:
axis.set_major_locator(ticker.MultipleLocator(tick_spacing)) axis.set_major_locator(ticker.MultipleLocator(tick_spacing))
plt.tight_layout() plt.tight_layout()
# set the size of the plot
fig.set_size_inches(10, 10)
plt.show()
# save the plot
fig.savefig("coil.png", dpi=300)
def plot_coil2(coil_points):
"""
Plots one or more coils in space.
input_filenames: Name of the files containing the coils.
Should be formatted appropriately.
"""
fig = plt.figure()
tick_spacing = 2
ax = fig.add_subplot(111, projection="3d")
ax.set_xlabel("$x$ (cm)")
ax.set_ylabel("$y$ (cm)")
ax.set_zlabel("$z$ (cm)")
coil_points = np.array(coil_points)
ax.plot3D(
coil_points[0, :], coil_points[1, :], coil_points[2, :], lw=2, color="red"
)
for axis in [ax.xaxis, ax.yaxis, ax.zaxis]:
axis.set_major_locator(ticker.MultipleLocator(tick_spacing))
plt.tight_layout()
# set the size of the plot
fig.set_size_inches(5, 5)
plt.show() plt.show()
def create_B_x_rectangle(name, p0=[-21.59, -38.1, -21.59, 1], L=76.20, W=43.18): def create_B_x_rectangle(name, p0=[-21.59, -38.1, -21.59, 1], L=76.20, W=43.18):
''' """
Creates a rectangle of the Y-Z plane that produces a B_x field. Creates a rectangle of the Y-Z plane that produces a B_x field.
name: filename to output to. Should be a .txt file. name: filename to output to. Should be a .txt file.
p0: [x0,y0,z0,Current] Starting point of the rectangle. p0: [x0,y0,z0,Current] Starting point of the rectangle.
L: Length (on Z) L: Length (on Z)
W: Width (on y) W: Width (on y)
''' """
f = open(name, "w") f = open(name, "w")
p1 = [p0[0], p0[1] + W, p0[2], p0[3]] p1 = [p0[0], p0[1] + W, p0[2], p0[3]]
p2 = [p0[0], p0[1] + W, p0[2] + L, p0[3]] p2 = [p0[0], p0[1] + W, p0[2] + L, p0[3]]
p3 = [p0[0], p0[1], p0[2] + L, p0[3]] p3 = [p0[0], p0[1], p0[2] + L, p0[3]]
line = str(p0) line = str(p0)
line = line[1 : len(line) - 1] + "\n" line = line[1 : len(line) - 1] + "\n"
f.write(line) f.write(line)
@ -320,14 +445,14 @@ def create_B_x_rectangle(name,p0=[-21.59,-38.1,-21.59,1],L = 76.20,W= 43.18):
def create_B_y_rectangle(name, p0=[-21.59, -38.1, -21.59, 1], L=76.20, D=43.18): def create_B_y_rectangle(name, p0=[-21.59, -38.1, -21.59, 1], L=76.20, D=43.18):
''' """
Creates a rectangle of the X-Z plane that produces a B_y field. Creates a rectangle of the X-Z plane that produces a B_y field.
name: filename to output to. Should be a .txt file. name: filename to output to. Should be a .txt file.
p0: [x0,y0,z0,Current] Starting point of the rectangle. p0: [x0,y0,z0,Current] Starting point of the rectangle.
L: Length (on Z) L: Length (on Z)
D: Depth (on X) D: Depth (on X)
''' """
f = open(name, "w") f = open(name, "w")
p1 = [p0[0], p0[1], p0[2] + L, p0[3]] p1 = [p0[0], p0[1], p0[2] + L, p0[3]]
@ -355,15 +480,16 @@ def create_B_y_rectangle(name,p0=[-21.59,-38.1,-21.59,1], L = 76.20, D= 43.18):
f.write(line) f.write(line)
f.close() f.close()
def create_B_z_rectangle(name, p0=[-26.67, -26.67, -26.67, 1], H=53.340, DD=53.340): def create_B_z_rectangle(name, p0=[-26.67, -26.67, -26.67, 1], H=53.340, DD=53.340):
''' """
Creates a rectangle of the X-Y plane that produces a B_z field. Creates a rectangle of the X-Y plane that produces a B_z field.
name: filename to output to. Should be a .txt file. name: filename to output to. Should be a .txt file.
p0: [x0,y0,z0,Current] Starting point of the rectangle. p0: [x0,y0,z0,Current] Starting point of the rectangle.
H: Height (on Y) H: Height (on Y)
DD: Depth (on X) DD: Depth (on X)
''' """
f = open(name, "w") f = open(name, "w")
p1 = [p0[0] + DD, p0[1], p0[2], p0[3]] p1 = [p0[0] + DD, p0[1], p0[2], p0[3]]
@ -392,8 +518,9 @@ def create_B_z_rectangle(name,p0=[-26.67,-26.67,-26.67,1], H= 53.340, DD = 53.34
f.close() f.close()
def helmholtz_coils(fname1, fname2, numSegments, radius, spacing, current): def helmholtz_coils(fname1, fname2, numSegments, radius, spacing, current):
''' """
Creates a pair of Helmholtz Coils that are parallel to the X-Y plane. Creates a pair of Helmholtz Coils that are parallel to the X-Y plane.
fname1: Name of the file where the first coil will be saved. fname1: Name of the file where the first coil will be saved.
@ -402,24 +529,42 @@ def helmholtz_coils(fname1, fname2, numSegments, radius, spacing, current):
radius: Radius of the coils radius: Radius of the coils
spacing: Spacing between the coils. The first coil will be located at -spacing/2 and the 2nd coil will be located at spacing/2 on the Z plane spacing: Spacing between the coils. The first coil will be located at -spacing/2 and the 2nd coil will be located at spacing/2 on the Z plane
current: The current that goest through each coil. current: The current that goest through each coil.
''' """
f = open(fname1, "w") f = open(fname1, "w")
line = "" line = ""
for i in range(0, numSegments, 1): for i in range(0, numSegments, 1):
line = str(np.cos(2*np.pi*(i)/(numSegments-1))*radius) + "," + str(np.sin(2*np.pi*(i)/(numSegments-1))*radius) + "," + str(-spacing/2.0) + "," + str(current) + "\n" line = (
str(np.cos(2 * np.pi * (i) / (numSegments - 1)) * radius)
+ ","
+ str(np.sin(2 * np.pi * (i) / (numSegments - 1)) * radius)
+ ","
+ str(-spacing / 2.0)
+ ","
+ str(current)
+ "\n"
)
f.write(line) f.write(line)
f.close() f.close()
f = open(fname2, "w") f = open(fname2, "w")
line = "" line = ""
for i in range(0, numSegments, 1): for i in range(0, numSegments, 1):
line = str(np.cos(2*np.pi*(i)/(numSegments-1))*radius) + "," + str(np.sin(2*np.pi*(i)/(numSegments-1))*radius) + "," + str(spacing/2.0) + "," + str(current) + "\n" line = (
str(np.cos(2 * np.pi * (i) / (numSegments - 1)) * radius)
+ ","
+ str(np.sin(2 * np.pi * (i) / (numSegments - 1)) * radius)
+ ","
+ str(spacing / 2.0)
+ ","
+ str(current)
+ "\n"
)
f.write(line) f.write(line)
f.close() f.close()
def create_Bx_circle(fname, numSegments, radius, spacing, current, center): def create_Bx_circle(fname, numSegments, radius, spacing, current, center):
''' """
Creates a coil on the Y-Z plane that produces a B_x field. Creates a coil on the Y-Z plane that produces a B_x field.
fname: Name of the file where the first coil will be saved. fname: Name of the file where the first coil will be saved.
@ -428,17 +573,26 @@ def create_Bx_circle(fname, numSegments, radius, spacing, current, center):
spacing: Spacing between the coil and the Y-Z plane spacing: Spacing between the coil and the Y-Z plane
current: The current that goest through the coil. current: The current that goest through the coil.
center: (y,z) The center of the coil on the Y-Z plane center: (y,z) The center of the coil on the Y-Z plane
''' """
f = open(fname, "w") f = open(fname, "w")
line = "" line = ""
for i in range(0, numSegments, 1): for i in range(0, numSegments, 1):
line = str(spacing) + "," + str(np.cos(2*np.pi*(i)/(numSegments-1))*radius + center[0]) + "," + str(np.sin(2*np.pi*(i)/(numSegments-1))*radius + center[1]) + "," + str(current) + "\n" line = (
str(spacing)
+ ","
+ str(np.cos(2 * np.pi * (i) / (numSegments - 1)) * radius + center[0])
+ ","
+ str(np.sin(2 * np.pi * (i) / (numSegments - 1)) * radius + center[1])
+ ","
+ str(current)
+ "\n"
)
f.write(line) f.write(line)
f.close() f.close()
def create_By_circle(fname, numSegments, radius, spacing, current, center): def create_By_circle(fname, numSegments, radius, spacing, current, center):
''' """
Creates a coil on the X-Z plane that produces a B_y field. Creates a coil on the X-Z plane that produces a B_y field.
fname: Name of the file where the first coil will be saved. fname: Name of the file where the first coil will be saved.
@ -447,17 +601,26 @@ def create_By_circle(fname, numSegments, radius, spacing, current, center):
spacing: Spacing between the coil and the X-Z plane spacing: Spacing between the coil and the X-Z plane
current: The current that goest through the coil. current: The current that goest through the coil.
center: (x,z) The center of the coil on the X-Z plane center: (x,z) The center of the coil on the X-Z plane
''' """
f = open(fname, "w") f = open(fname, "w")
line = "" line = ""
for i in range(0, numSegments, 1): for i in range(0, numSegments, 1):
line = str(np.cos(2*np.pi*(i)/(numSegments-1))*radius + center[0]) + "," + str(spacing) + "," + str(np.sin(2*np.pi*(i)/(numSegments-1))*radius + center[1]) + "," + str(current) + "\n" line = (
str(np.cos(2 * np.pi * (i) / (numSegments - 1)) * radius + center[0])
+ ","
+ str(spacing)
+ ","
+ str(np.sin(2 * np.pi * (i) / (numSegments - 1)) * radius + center[1])
+ ","
+ str(current)
+ "\n"
)
f.write(line) f.write(line)
f.close() f.close()
def create_Bz_circle(fname, numSegments, radius, spacing, current, center): def create_Bz_circle(fname, numSegments, radius, spacing, current, center):
''' """
Creates a coil on the X-Y plane that produces a B_z field. Creates a coil on the X-Y plane that produces a B_z field.
fname: Name of the file where the first coil will be saved. fname: Name of the file where the first coil will be saved.
@ -466,11 +629,19 @@ def create_Bz_circle(fname, numSegments, radius, spacing, current, center):
spacing: Spacing between the coil and the X-Y plane spacing: Spacing between the coil and the X-Y plane
current: The current that goest through the coil. current: The current that goest through the coil.
center: (x,y) The center of the coil on the X-Y plane center: (x,y) The center of the coil on the X-Y plane
''' """
f = open(fname, "w") f = open(fname, "w")
line = "" line = ""
for i in range(0, numSegments, 1): for i in range(0, numSegments, 1):
line = str(np.cos(2*np.pi*(i)/(numSegments-1))*radius + center[0]) + "," + str(np.sin(2*np.pi*(i)/(numSegments-1))*radius + center[1]) + "," + str(spacing) + "," + str(current) + "\n" line = (
str(np.cos(2 * np.pi * (i) / (numSegments - 1)) * radius + center[0])
+ ","
+ str(np.sin(2 * np.pi * (i) / (numSegments - 1)) * radius + center[1])
+ ","
+ str(spacing)
+ ","
+ str(current)
+ "\n"
)
f.write(line) f.write(line)
f.close() f.close()

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -0,0 +1,359 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import biot_savart_v4_3 as bs\n",
"import matplotlib.pyplot as plt"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# set the size larger\n",
"plt.rcParams[\"figure.figsize\"] = (10, 10)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# COIL_NAME = \"coil_6_spiral\"\n",
"# COIL_NAME = \"coil_6_custom\"\n",
"# COIL_NAME = \"coil_12_spiral\"\n",
"COIL_NAME = \"coil_12_custom\"\n",
"COIL = \"coils/\" + COIL_NAME + \".csv\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"bs.plot_coil(COIL)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"bs.write_target_volume(\n",
" COIL,\n",
" COIL_NAME + \"volume.vol\",\n",
" (4, 4, 4),\n",
" (-2, -2, -2),\n",
" 0.1,\n",
" 0.1,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# read in the results\n",
"total_volume = bs.read_target_volume(COIL_NAME + \"volume.vol\")\n",
"\n",
"total_volume = total_volume\n",
"\n",
"# reads the volume we created\n",
"bs.plot_fields(total_volume, (4, 4, 4), (-2, -2, -2), 0.1, which_plane=\"z\", level=0.1)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import plotly.graph_objects as go\n",
"\n",
"X, Y, Z = np.mgrid[-2:2:41j, -2:2:41j, -2:2:41j]\n",
"# values = np.sqrt(total_volume[:,:,:,0]*total_volume[:,:,:,0] +\n",
"# total_volume[:,:,:,1]*total_volume[:,:,:,1] +\n",
"# total_volume[:,:,:,2]*total_volume[:,:,:,2])\n",
"values = total_volume[:, :, :, 2]\n",
"fig = go.Figure(\n",
" data=go.Volume(\n",
" x=X.flatten(),\n",
" y=Y.flatten(),\n",
" z=Z.flatten(),\n",
" value=np.abs(values.flatten()),\n",
" isomin=0.1,\n",
" isomax=0.5,\n",
" opacity=0.1, # needs to be small to see through all surfaces\n",
" surface_count=17, # needs to be a large number for good volume rendering\n",
" )\n",
")\n",
"\n",
"coil = bs.parse_coil(\"coils/\" + COIL_NAME + \".csv\")\n",
"coil = coil[0:3, :]\n",
"# add the coil as a line plot\n",
"fig.add_trace(\n",
" go.Scatter3d(\n",
" x=coil[0, :],\n",
" y=coil[1, :],\n",
" z=coil[2, :],\n",
" mode=\"lines\",\n",
" line=dict(color=\"red\", width=5),\n",
" )\n",
")\n",
"# make the plotly graph bigger\n",
"fig.update_layout(\n",
" autosize=False,\n",
" width=1400,\n",
" height=1000,\n",
")\n",
"fig.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"slice = [total_volume[:, 21, :, i].T for i in range(3)]\n",
"x_contour = slice[0]\n",
"y_contour = slice[1]\n",
"z_contour = slice[2]\n",
"\n",
"print(x_contour.shape)\n",
"print(y_contour.shape)\n",
"print(z_contour.shape)\n",
"\n",
"x = np.arange(0, x_contour.shape[0], 1)\n",
"y = np.arange(0, z_contour.shape[1], 1)\n",
"xG, yG = np.meshgrid(x, y)\n",
"\n",
"# calclulate the magnitude of the field\n",
"mag = np.sqrt(x_contour**2 + y_contour**2 + z_contour**2)\n",
"\n",
"# color = 2 * np.log(np.hypot(x_contour, z_contour))\n",
"color = np.log(mag)\n",
"stream_plot = plt.streamplot(\n",
" x,\n",
" y,\n",
" x_contour,\n",
" z_contour,\n",
" color=color,\n",
" linewidth=1,\n",
" cmap=plt.cm.Reds,\n",
" density=2,\n",
" arrowstyle=\"->\",\n",
" arrowsize=1.5,\n",
")\n",
"# set the axis to be equal\n",
"plt.axis(\"equal\")\n",
"\n",
"# set the plt size to 5x5\n",
"plt.rcParams[\"figure.figsize\"] = (10, 10)\n",
"\n",
"# save the figure\n",
"plt.savefig(COIL_NAME + \"_field_lines.png\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.cm as cm\n",
"\n",
"# get the total left and right field in the X direction\n",
"slice = [total_volume[:, :, 21, i].T for i in range(3)]\n",
"x_contour = slice[0]\n",
"y_contour = slice[1]\n",
"z_contour = slice[2]\n",
"\n",
"# vmin = np.min([np.min(x_contour), np.min(z_contour), np.min(y_contour)])\n",
"# vmax = np.max([np.max(x_contour), np.max(z_contour), np.max(y_contour)])\n",
"\n",
"vmin = np.min(x_contour)\n",
"vmax = np.max(x_contour)\n",
"\n",
"# -7.970943527635176 7.775784181783158\n",
"vmin = -7.970943527635176\n",
"vmax = 7.775784181783158\n",
"\n",
"print(vmin, vmax)\n",
"\n",
"left_x = np.sum(x_contour[x_contour < 0])\n",
"right_x = np.sum(x_contour[x_contour > 0])\n",
"\n",
"print(\"Left X: \", left_x)\n",
"print(\"Right X: \", right_x)\n",
"\n",
"plt.rcParams[\"figure.figsize\"] = (5, 5)\n",
"\n",
"# make the plot larger\n",
"plt.rcParams[\"figure.figsize\"] = (10, 10)\n",
"\n",
"# plt.imshow(x_contour, cmap='hot', interpolation='nearest')\n",
"plt.contourf(\n",
" x_contour,\n",
" vmin=vmin,\n",
" vmax=vmax,\n",
" cmap=cm.magma,\n",
" levels=50,\n",
")\n",
"\n",
"plt.savefig(COIL_NAME + \"_field_x.png\")\n",
"\n",
"print(vmin, vmax)\n",
"\n",
"# 6 coil custom\n",
"# Left X: -565.6790142743889\n",
"# Right X: 544.1011578804321\n",
"\n",
"# 6 coil spiral\n",
"# Left X: -512.1107132193335\n",
"# Right X: 534.1946450523811\n",
"\n",
"# 12 coil spiral\n",
"# Left X: -218.54696343677543\n",
"# Right X: 233.84835661493196\n",
"\n",
"# 12 coil custom\n",
"# Left X: -361.95894587646944\n",
"# Right X: 347.13174239269904"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# plot the magitude of the slice\n",
"plt.imshow(np.log(mag), cmap=\"hot\", interpolation=\"nearest\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"slice = [total_volume[:, :, 25, i].T for i in range(3)]\n",
"x_contour = slice[0]\n",
"y_contour = slice[1]\n",
"z_contour = slice[2]\n",
"\n",
"mag = np.sqrt(x_contour**2 + y_contour**2 + z_contour**2)\n",
"\n",
"print(np.max(mag), np.min(mag))\n",
"\n",
"# plot a histogram of the magnitude\n",
"# plt.hist(mag.flatten(), bins=10)\n",
"\n",
"plt.hist(z_contour.flatten(), bins=10)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# show the z contour as a 3d surface plot\n",
"fig = plt.figure()\n",
"\n",
"# change the figure size to 16.9\n",
"ax = fig.add_subplot(111, projection=\"3d\")\n",
"\n",
"xG_ = xG[10:30, 10:30]\n",
"yG_ = yG[10:30, 10:30]\n",
"z_contour_ = z_contour[10:30, 10:30]\n",
"\n",
"print(xG.shape, yG.shape, z_contour.shape)\n",
"\n",
"# change the axis limits to 10 to 30\n",
"ax.set_xlim(10, 30)\n",
"ax.set_ylim(10, 30)\n",
"surf = ax.plot_surface(\n",
" xG_, yG_, z_contour_, cmap=cm.plasma, linewidth=0, antialiased=False, alpha=0.5\n",
")\n",
"# add the coil to the plot as a set of lines - the X and Y coordinates of the coil are in the first two elements of the coil array\n",
"coil = bs.parse_coil(\"coils/\" + COIL_NAME + \".csv\")\n",
"coil = coil[0:2, :]\n",
"# need to scale the coil by 10 and shift it by 20, 20\n",
"coil = coil * 10 + 20\n",
"# draw the coil in yellow\n",
"ax.plot(coil[0, :], coil[1, :], 0, color=\"black\")\n",
"\n",
"# hide the axes\n",
"ax.set_axis_off()\n",
"\n",
"# save to png\n",
"plt.savefig(COIL_NAME + \"_field_surface_z.png\")\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# write out the coil as SVG\n",
"coil = bs.parse_coil(\"coils/\" + COIL_NAME + \".csv\")\n",
"coil = coil[0:2, :]\n",
"with open(COIL_NAME + \"_coil.svg\", \"w\") as f:\n",
" # write the header\n",
" f.write('<svg viewBox=\"0 0 400 400\" xmlns=\"http://www.w3.org/2000/svg\">')\n",
" # the X and Y points of the coil the X and Y coordinates of the coil are in the first two elements of the coil array\n",
" # write out the coil as a set of lines\n",
" print(len(coil[0]) - 1)\n",
" # write out the coil as a polyline\n",
" f.write('<polyline points=\"')\n",
" for i in range(0, len(coil[0]), 10):\n",
" f.write(str(coil[0, i] * 100 + 200) + \",\" + str(coil[1, i] * 100 + 200) + \" \")\n",
" f.write('\" style=\"fill:none;stroke:black;stroke-width:1\" />')\n",
" # write the footer\n",
" f.write(\"</svg>\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.10.7 ('venv': venv)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.7"
},
"vscode": {
"interpreter": {
"hash": "1ce20143987840b9786ebb5907032c9c3a8efacbb887dbb0ebc4934f2ad26cb3"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}

111
simulations/helpers.py Normal file
View file

@ -0,0 +1,111 @@
import numpy as np
# get the point on an arc at the given angle
def get_arc_point(angle, radius):
return (
radius * np.cos(np.deg2rad(angle)),
radius * np.sin(np.deg2rad(angle)),
)
# draw an arc
def draw_arc(start_angle, end_angle, radius, step=5):
# make sure start_angle is less then end_angle
if start_angle > end_angle:
start_angle, end_angle = end_angle, start_angle
points = []
for angle in np.arange(start_angle, end_angle, step):
x = radius * np.cos(np.deg2rad(angle))
y = radius * np.sin(np.deg2rad(angle))
points.append((x, y))
if angle != end_angle:
x = radius * np.cos(np.deg2rad(end_angle))
y = radius * np.sin(np.deg2rad(end_angle))
points.append((x, y))
return points
# roate the points by the required angle
def rotate(points, angle):
return [
[
x * np.cos(np.deg2rad(angle)) - y * np.sin(np.deg2rad(angle)),
x * np.sin(np.deg2rad(angle)) + y * np.cos(np.deg2rad(angle)),
]
for x, y in points
]
def scale(points, scale):
return [[x * scale, y * scale] for x, y in points]
# rotate a point
def rotate_point(x, y, angle, ox=0, oy=0):
x -= ox
y -= oy
qx = x * np.cos(np.deg2rad(angle)) - y * np.sin(np.deg2rad(angle))
qy = x * np.sin(np.deg2rad(angle)) + y * np.cos(np.deg2rad(angle))
qx += ox
qy += oy
return qx, qy
# move the points out to the distance at the requited angle
def translate(points, distance, angle):
return [
[
x + distance * np.cos(np.deg2rad(angle)),
y + distance * np.sin(np.deg2rad(angle)),
]
for x, y in points
]
# flip the y coordinate
def flip_y(points):
return [[x, -y] for x, y in points]
def flip_x(points):
return [[-x, y] for x, y in points]
def optimize_points(points):
# follow the line and remove points that are in the same direction as the previous poin
# keep doing this until the direction changes significantly
# this is a very simple optimization that removes a lot of points
# it's not perfect but it's a good start
optimized_points = []
for i in range(len(points)):
if i == 0:
optimized_points.append(points[i])
else:
vector1 = np.array(points[i]) - np.array(points[i - 1])
vector2 = np.array(points[(i + 1) % len(points)]) - np.array(points[i])
length1 = np.linalg.norm(vector1)
length2 = np.linalg.norm(vector2)
if length1 > 0 and length2 > 0:
dot = np.dot(vector1, vector2) / (length1 * length2)
# clamp dot between -1 and 1
dot = max(-1, min(1, dot))
angle = np.arccos(dot)
if angle > np.deg2rad(5):
optimized_points.append(points[i])
print("Optimised from {} to {} points".format(len(points), len(optimized_points)))
return optimized_points
def chaikin(points, iterations):
if iterations == 0:
return points
l = len(points)
smoothed = []
for i in range(l - 1):
x1, y1 = points[i]
x2, y2 = points[i + 1]
smoothed.append([0.95 * x1 + 0.05 * x2, 0.95 * y1 + 0.05 * y2])
smoothed.append([0.05 * x1 + 0.95 * x2, 0.05 * y1 + 0.95 * y2])
smoothed.append(points[l - 1])
return chaikin(smoothed, iterations - 1)

View file

@ -0,0 +1,607 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from mpl_toolkits.mplot3d import Axes3D\n",
"from biot_savart_v4_3 import parse_coil, plot_coil, slice_coil\n",
"from tqdm.notebook import trange, tqdm"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# load up the simple spiral coil\n",
"coil1 = parse_coil(\"coils/coil_12_spiral.csv\")\n",
"plot_coil(\"coils/coil_12_spiral.csv\")\n",
"coil1 = slice_coil(coil1, 1)\n",
"coil1 = coil1.T\n",
"print(coil1.shape)\n",
"\n",
"coil2 = parse_coil(\"coils/coil_12_custom.csv\")\n",
"plot_coil(\"coils/coil_12_custom.csv\")\n",
"coil2 = slice_coil(coil2, 1)\n",
"coil2 = coil2.T\n",
"print(coil2.shape)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Simple Simulation of a dipole magnet"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# magnetic field at a point x,y,z of a dipole magnet with moment m in the z direction\n",
"def B_(x, y, z, m=0.185):\n",
" mu0 = 4 * np.pi * 1e-7\n",
" r = np.sqrt(x**2 + y**2 + z**2)\n",
" return (\n",
" np.array(\n",
" [3 * x * z / r**5, 3 * y * z / r**5, (3 * z**2 / r**5 - 1 / r**3)]\n",
" )\n",
" * m\n",
" * mu0\n",
" )\n",
"\n",
"\n",
"def B(x, y, z, m=0.185, l=0.003, d=0.01):\n",
" d = d * 0.75\n",
" # simulate multiple points in the cylinder and add them together to create a disk of field\n",
" bx = 0\n",
" by = 0\n",
" bz = 0\n",
" for degrees in range(0, 360, 30):\n",
" angle = np.deg2rad(degrees)\n",
" x_, y_, z_ = B_(\n",
" x + d / 2 * np.cos(angle),\n",
" y + d / 2 * np.sin(angle),\n",
" z - l / 2,\n",
" m / (360 / 60),\n",
" )\n",
" bx += x_\n",
" by += y_\n",
" bz += z_\n",
" x_, y_, z_ = B_(\n",
" x + d / 2 * np.cos(angle),\n",
" y + d / 2 * np.sin(angle),\n",
" z + l / 2,\n",
" m / (360 / 60),\n",
" )\n",
" bx += x_\n",
" by += y_\n",
" bz += z_\n",
" return np.array([bx, by, bz])\n",
"\n",
"\n",
"def plot_field_slice(x, y, bx, by, mag, name=\"magnetic_field.png\"):\n",
" # plot the magnetic field\n",
" fig = plt.figure()\n",
" ax = fig.add_subplot(111)\n",
" ax.streamplot(\n",
" x,\n",
" y,\n",
" bx,\n",
" by,\n",
" linewidth=1,\n",
" cmap=plt.cm.inferno,\n",
" density=2,\n",
" arrowstyle=\"->\",\n",
" arrowsize=1.5,\n",
" )\n",
"\n",
" ax.set_xlabel(\"$x$\")\n",
" ax.set_ylabel(\"$y$\")\n",
" ax.set_xlim(-0.1, 0.1)\n",
" ax.set_ylim(-0.1, 0.1)\n",
" ax.set_aspect(\"equal\")\n",
"\n",
" # plot the magniture of the field as an image\n",
" im = ax.imshow(\n",
" mag, extent=[-0.1, 0.1, -0.1, 0.1], origin=\"lower\", cmap=plt.cm.inferno\n",
" )\n",
"\n",
" # draw the magnet\n",
" ax.add_patch(plt.Rectangle((-0.005, -0.0015), 0.01, 0.003, fc=\"w\", ec=\"k\", lw=1))\n",
"\n",
" fig.show()\n",
" # save the figure\n",
" fig.savefig(name)\n",
"\n",
"\n",
"# # calculate the magnetic field at y = 0, over z = -1, 1 and x = -1, 1\n",
"x = np.linspace(-0.1, 0.1, 100)\n",
"z = np.linspace(-0.1, 0.1, 100)\n",
"X, Z = np.meshgrid(x, z)\n",
"Bx, By, Bz = B(X, 0, Z)\n",
"\n",
"print(Bx.shape, By.shape, Bz.shape)\n",
"\n",
"plot_field_slice(\n",
" X,\n",
" Z,\n",
" Bx,\n",
" Bz,\n",
" np.log(np.sqrt(Bx**2 + By**2 + Bz**2)),\n",
" \"magnetic_field_side.png\",\n",
")\n",
"\n",
"# # calculate the magnetic field at z = 1, over y = -1, 1 and x = -1, 1\n",
"x = np.linspace(-0.1, 0.1, 100)\n",
"y = np.linspace(-0.1, 0.1, 100)\n",
"X, Y = np.meshgrid(x, y)\n",
"Bx, By, Bz = B(X, Y, 0.01)\n",
"\n",
"plot_field_slice(\n",
" X,\n",
" Y,\n",
" Bx,\n",
" By,\n",
" np.log(np.sqrt(Bx**2 + By**2 + Bz**2)),\n",
" \"magnetic_field_bottom.png\",\n",
")\n",
"\n",
"# calculate the magnetic field in a 3d volume\n",
"x = np.linspace(-0.1, 0.1, 100)\n",
"y = np.linspace(-0.1, 0.1, 100)\n",
"z = np.linspace(-0.1, 0.1, 100)\n",
"X, Y, Z = np.meshgrid(x, y, z)\n",
"Bx, By, Bz = B(X, Y, Z)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# do a 3d quivwer plot of the magnetic field\n",
"fig = plt.figure()\n",
"ax = fig.add_subplot(111, projection=\"3d\")\n",
"# down sample the results\n",
"ax.quiver(\n",
" X[::10, ::10, ::10],\n",
" Y[::10, ::10, ::10],\n",
" Z[::10, ::10, ::10],\n",
" Bx[::10, ::10, ::10],\n",
" By[::10, ::10, ::10],\n",
" Bz[::10, ::10, ::10],\n",
" length=0.01,\n",
" normalize=True,\n",
")\n",
"ax.set_xlabel(\"$x$\")\n",
"ax.set_ylabel(\"$y$\")\n",
"ax.set_zlabel(\"$z$\")\n",
"ax.set_xlim(-0.1, 0.1)\n",
"ax.set_ylim(-0.1, 0.1)\n",
"ax.set_zlim(-0.1, 0.1)\n",
"ax.set_aspect(\"equal\")\n",
"# make the plot larger\n",
"fig.set_size_inches(10, 10)\n",
"fig.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# use mayavi to plot the magnetic field\n",
"from mayavi import mlab\n",
"\n",
"mlab.figure(bgcolor=(1, 1, 1), size=(800, 800))\n",
"mlab.quiver3d(\n",
" X,\n",
" Y,\n",
" Z,\n",
" Bx,\n",
" By,\n",
" Bz,\n",
" mode=\"2ddash\",\n",
" scale_factor=1,\n",
" scale_mode=\"none\",\n",
" color=(0, 0, 0),\n",
")\n",
"mlab.axes(\n",
" xlabel=\"$x$\", ylabel=\"$y$\", zlabel=\"$z$\", ranges=[-0.1, 0.1, -0.1, 0.1, -0.1, 0.1]\n",
")\n",
"mlab.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# use myavi to plot a contour plot of the magnetic field\n",
"from mayavi import mlab\n",
"\n",
"mlab.figure(bgcolor=(1, 1, 1), size=(800, 800))\n",
"mlab.contour3d(\n",
" X, Y, Z, np.sqrt(Bx**2 + By**2 + Bz**2), contours=20, transparent=True\n",
")\n",
"mlab.axes(\n",
" xlabel=\"$x$\", ylabel=\"$y$\", zlabel=\"$z$\", ranges=[-0.1, 0.1, -0.1, 0.1, -0.1, 0.1]\n",
")\n",
"mlab.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# calculate the force on a wire of length l carrying current I at a point x,y,z with a direction vector d\n",
"def F(p, d, I, l):\n",
" return I * l * np.cross(d, B(p[0], p[1], p[2]))\n",
"\n",
"\n",
"def calculate_forces_on_wire_points(points):\n",
" Fx = []\n",
" Fy = []\n",
" Fz = []\n",
" # calculate the force on each point\n",
" for i in range(len(points)):\n",
" # calculate the direction vector\n",
" dx = points[i][0] - points[(i + 1) % len(points)][0]\n",
" dy = points[i][1] - points[(i + 1) % len(points)][1]\n",
" dz = points[i][2] - points[(i + 1) % len(points)][2]\n",
" d = np.array([dx, dy, dz])\n",
" # get the length of d\n",
" l = np.sqrt(dx**2 + dy**2 + dz**2)\n",
" if l > 0:\n",
" # normalise d\n",
" d = d / l\n",
" # calculate the force\n",
" fx, fy, fz = F(points[i], d, points[i][3], l)\n",
" Fx.append(fx)\n",
" Fy.append(fy)\n",
" Fz.append(fz)\n",
" else:\n",
" Fx.append(0)\n",
" Fy.append(0)\n",
" Fz.append(0)\n",
" return Fx, Fy, Fz\n",
"\n",
"\n",
"# locate the loop of wire directly below the magnet\n",
"x = 0.01\n",
"y = 0\n",
"z = -0.002\n",
"r1 = 0.001\n",
"r2 = 0.01\n",
"\n",
"\n",
"points = coil2.copy()\n",
"# scale the points from mm to m\n",
"# shift the coil to the correct position\n",
"for i in range(len(points)):\n",
" points[i][0] = points[i][0] / 1000 + x\n",
" points[i][1] = points[i][1] / 1000 + y\n",
" points[i][2] = points[i][2] / 1000 + z\n",
"\n",
"\n",
"# plot the points in 2D x,y\n",
"fig = plt.figure()\n",
"ax = fig.add_subplot(111)\n",
"ax.plot([p[0] for p in points], [p[1] for p in points])\n",
"ax.set_xlabel(\"$x$\")\n",
"ax.set_ylabel(\"$y$\")\n",
"ax.set_xlim(-0.01 + x, 0.01 + x)\n",
"ax.set_ylim(-0.01 + y, 0.01 + y)\n",
"ax.set_aspect(\"equal\")\n",
"fig.show()\n",
"\n",
"Fx, Fy, Fz = calculate_forces_on_wire_points(points)\n",
"\n",
"\n",
"# plot the wire along with arrows showing the force\n",
"fig = plt.figure()\n",
"ax = fig.add_subplot(111, projection=\"3d\")\n",
"ax.plot([p[0] for p in points], [p[1] for p in points], [p[2] for p in points])\n",
"\n",
"print(sum(Fx) / 9.8)\n",
"\n",
"# subsample the points and force vectors to make the plot clearer\n",
"points = points[::500]\n",
"Fx = Fx[::500]\n",
"Fy = Fy[::500]\n",
"Fz = Fz[::500]\n",
"\n",
"ax.quiver(\n",
" [p[0] for p in points],\n",
" [p[1] for p in points],\n",
" [p[2] for p in points],\n",
" np.sqrt(Fx),\n",
" 0,\n",
" 0,\n",
" length=0.01,\n",
" normalize=True,\n",
")\n",
"ax.set_xlabel(\"$x$\")\n",
"ax.set_ylabel(\"$y$\")\n",
"ax.set_zlabel(\"$z$\")\n",
"ax.set_xlim(x - 0.02, x + 0.02)\n",
"ax.set_ylim(y - 0.02, y + 0.02)\n",
"ax.set_zlim(z - 0.02, z + 0.02)\n",
"ax.set_aspect(\"equal\")\n",
"\n",
"# change the figure size\n",
"fig.set_size_inches(10, 10)\n",
"\n",
"fig.show()\n",
"# coil2 = 0.0375037573536258\n",
"# coil1 = 0.010254238165764389"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def sweep_coil(coil, X):\n",
" Y = np.zeros(len(X))\n",
" Z = -0.01 * np.ones(len(X))\n",
"\n",
" # loop through the locations and calculate the forces, sum up the force in the X direction for each location\n",
" Fx = []\n",
" Fy = []\n",
" Fz = []\n",
" for p in trange(len(X)):\n",
" points = coil.copy()\n",
" # scale the points from mm to m\n",
" # shift the coil to the correct position\n",
" for i in range(len(points)):\n",
" points[i][0] = points[i][0] / 1000 + X[p]\n",
" points[i][1] = points[i][1] / 1000 + Y[p]\n",
" points[i][2] = points[i][2] / 1000 + Z[p]\n",
" Fx_, Fy_, Fz_ = calculate_forces_on_wire_points(points)\n",
" Fx.append(sum(Fx_))\n",
" Fy.append(sum(Fy_))\n",
" Fz.append(sum(Fz_))\n",
" return Fx, Fy, Fz\n",
"\n",
"\n",
"# sweep the coild from -3cm to 3cm in 0.01m steps\n",
"X = np.linspace(-0.03, 0.03, 100)\n",
"Fx_1_straight, Fy_1_straight, Fz_1_straight = sweep_coil(coil1, X)\n",
"Fx_2_straight, Fy_2_straight, Fz_2_straight = sweep_coil(coil2, X)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# plot the force as a function of x\n",
"plt.plot(X, -np.array(Fx_1_straight) / 9.8, label=\"coil1\", color=\"red\")\n",
"plt.plot(X, np.array(Fx_2_straight) / 9.8, label=\"coil2\", color=\"blue\")\n",
"# plot a dotted line along y = 0\n",
"plt.plot([X[0], X[-1]], [0, 0], \"--\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# plot the force as a function of x\n",
"plt.plot(X, -np.array(Fz_1_straight) / 9.8, label=\"coil1\", color=\"red\")\n",
"plt.plot(X, np.array(Fz_2_straight) / 9.8, label=\"coil2\", color=\"blue\")\n",
"# plot a dotted line along y = 0\n",
"plt.plot([X[0], X[-1]], [0, 0], \"--\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# instead of sweeping horizontally, we'll sweep the coils around a circle\n",
"def sweep_coil_cirlc(coil, coil_center_radius, theta):\n",
" X = coil_center_radius * np.cos(np.deg2rad(theta))\n",
" Y = coil_center_radius * np.sin(np.deg2rad(theta))\n",
" Z = -0.01 * np.ones(100)\n",
"\n",
" # loop through the locations and calculate the forces, sum up the force in the X direction for each location\n",
" Torque = []\n",
" Fx = []\n",
" Fy = []\n",
" Fz = []\n",
" for p in trange(len(theta)):\n",
" angle = np.deg2rad(theta[p]) - np.pi / 2\n",
" x = X[p]\n",
" y = Y[p]\n",
" z = Z[p]\n",
"\n",
" points = coil.copy()\n",
" for i in range(len(points)):\n",
" px = points[i][0] / 1000\n",
" py = points[i][1] / 1000\n",
" pz = points[i][2] / 1000\n",
" # rotate the points so the coil is correctly oriented\n",
" points[i][0] = px * np.cos(angle) - py * np.sin(angle) + x\n",
" points[i][1] = (\n",
" px * np.sin(angle) + py * np.cos(angle) + y - coil_center_radius\n",
" )\n",
" points[i][2] = pz + z\n",
" # feel the force\n",
" Fx_, Fy_, Fz_ = calculate_forces_on_wire_points(points)\n",
" Fx.append(sum(Fx_))\n",
" Fy.append(sum(Fy_))\n",
" Fz.append(sum(Fz_))\n",
" # calculate the torque - which should be 90 degress to the angle\n",
" torque_angle = np.deg2rad(theta[p] - 90)\n",
" Torque.append(sum(Fx_) * np.cos(torque_angle) + sum(Fy_) * np.sin(torque_angle))\n",
"\n",
" return Fx, Fy, Fz, Torque\n",
"\n",
"\n",
"# sweep the coils from -45 to 45 degrees in 1 degree steps\n",
"theta = np.linspace(0, 180, 100)\n",
"Fx_1_curve, Fy_1_curve, Fz_1_curve, Torque_1 = sweep_coil_cirlc(\n",
" coil1, 20.5 / 1000, theta\n",
")\n",
"Fx_2_curve, Fy_2_curve, Fz_2_curve, Torque_2 = sweep_coil_cirlc(\n",
" coil2, 19.5 / 1000, theta\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.plot(\n",
" theta, -np.array(Fx_1_curve) + np.array(Fx_1_curve), label=\"coil1\", color=\"red\"\n",
")\n",
"plt.plot(theta, np.array(Torque_2) - np.array(Fx_2_curve), label=\"coil2\", color=\"blue\")\n",
"# plot a dotted line along y = 0\n",
"plt.plot([theta[0], theta[-1]], [0, 0], \"--\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.plot(theta, -np.array(Torque_1) / 9.8, label=\"coil1\", color=\"red\")\n",
"plt.plot(theta, np.array(Torque_2) / 9.8, label=\"coil2\", color=\"blue\")\n",
"# plot a dotted line along y = 0\n",
"plt.plot([theta[0], theta[-1]], [0, 0], \"--\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# plot arrows for the Fx and Fy components\n",
"X = 19.5 * np.cos(np.deg2rad(theta)) / 1000\n",
"Y = 19.5 * np.sin(np.deg2rad(theta)) / 1000\n",
"plt.quiver(X[::5], Y[::5], Fx_1_curve[::5], Fy_1_curve[::5], color=\"red\")\n",
"# make the axis equal so the arrows are not stretched\n",
"plt.axis(\"equal\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.plot(theta, -np.array(Fz_1_curve) / 9.8, label=\"coil1\", color=\"red\")\n",
"plt.plot(theta, np.array(Fz_2_curve) / 9.8, label=\"coil2\", color=\"blue\")\n",
"# plot a dotted line along y = 0\n",
"plt.plot([theta[0], theta[-1]], [0, 0], \"--\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# instead of sweeping horizontally, we'll sweep the coils around a circle\n",
"def sweep_coil_cirlc(coil, coil_center_radius):\n",
" # sweep the coils from -45 to 45 degrees in 1 degree steps\n",
" theta = np.linspace(0, 180, 20)\n",
" X = coil_center_radius * np.cos(np.deg2rad(theta))\n",
" Y = coil_center_radius * np.sin(np.deg2rad(theta))\n",
" Z = 1 * np.ones(100)\n",
"\n",
" # loop through the locations and calculate the forces, sum up the force in the X direction for each location\n",
" Fx = []\n",
" Fy = []\n",
" Fz = []\n",
" for p in trange(len(theta)):\n",
" angle = np.deg2rad(theta[p]) - np.pi / 2\n",
" x = X[p]\n",
" y = Y[p]\n",
" z = Z[p]\n",
"\n",
" points = coil.copy()\n",
" for i in range(len(points)):\n",
" px = points[i][0] / 1000\n",
" py = points[i][1] / 1000\n",
" pz = points[i][2] / 1000\n",
" # rotate the points so the coil is correctly oriented\n",
" points[i][0] = px * np.cos(angle) - py * np.sin(angle) + x\n",
" points[i][1] = (\n",
" px * np.sin(angle) + py * np.cos(angle) + y - coil_center_radius\n",
" )\n",
" points[i][2] = pz + z\n",
" plt.plot([p[0] for p in points], [p[1] for p in points], linewidth=0.5)\n",
" # add the torque arrow to the plot\n",
" torque_angle = np.deg2rad(theta[p] - 90)\n",
" torque_x1 = x\n",
" torque_y1 = y\n",
" torque_x2 = x + 0.01 * np.cos(torque_angle)\n",
" torque_y2 = y + 0.01 * np.sin(torque_angle)\n",
" plt.arrow(\n",
" torque_x1,\n",
" torque_y1,\n",
" torque_x2 - torque_x1,\n",
" torque_y2 - torque_y1,\n",
" head_width=0.001,\n",
" head_length=0.002,\n",
" fc=\"k\",\n",
" ec=\"k\",\n",
" )\n",
"\n",
"\n",
"sweep_coil_cirlc(coil2, 19.5 / 1000)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.10.7 ('venv': venv)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.7"
},
"vscode": {
"interpreter": {
"hash": "1ce20143987840b9786ebb5907032c9c3a8efacbb887dbb0ebc4934f2ad26cb3"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View file

@ -0,0 +1,525 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from mpl_toolkits.mplot3d import Axes3D\n",
"from biot_savart_v4_3 import parse_coil, plot_coil, slice_coil, plot_coil2\n",
"from tqdm.notebook import trange, tqdm\n",
"from helpers import get_arc_point, draw_arc, rotate, scale, rotate_point, translate"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Track width and spacing\n",
"TRACK_WIDTH = 0.127\n",
"TRACK_SPACING = 0.127\n",
"\n",
"# via defaults\n",
"VIA_DIAM = 0.8\n",
"VIA_DRILL = 0.4\n",
"\n",
"# this is for a 1.27mm pitch pin\n",
"PIN_DIAM = 1.0\n",
"PIN_DRILL = 0.65\n",
"\n",
"# this is for the PCB connector - see https://www.farnell.com/datasheets/2003059.pdf\n",
"PAD_WIDTH = 3\n",
"PAD_HEIGHT = 2\n",
"PAD_PITCH = 2.5\n",
"\n",
"STATOR_HOLE_RADIUS = 5.5\n",
"HOLE_SPACING = 0.25\n",
"\n",
"# PCB Edge size\n",
"STATOR_RADIUS = 30\n",
"\n",
"# where to puth the mounting pins\n",
"SCREW_HOLE_DRILL_DIAM = 2.3 # 2.3mm drill for a 2mm screw\n",
"SCREW_HOLE_RADIUS = STATOR_RADIUS\n",
"\n",
"# Coil params\n",
"TURNS = 16\n",
"COIL_CENTER_RADIUS = 19.95\n",
"COIL_VIA_RADIUS = 20.95\n",
"\n",
"\n",
"def get_points(spacing, inner_radius, outer_radius, start_angle, end_angle):\n",
" # first calculate the angle step size from the spacing and the inner_radius\n",
" spacing_angle = np.rad2deg(np.arctan2(spacing, inner_radius))\n",
" print(spacing_angle)\n",
" # now calculate the points be iterating from start_angle to end_angle with the spacing_angle\n",
" points = []\n",
" for angle in np.arange(start_angle, end_angle, spacing_angle * 2):\n",
" p1 = get_arc_point(angle, inner_radius)\n",
" points.append([p1[0], p1[1], 0, 0.5])\n",
" p2 = get_arc_point(angle + spacing_angle, outer_radius)\n",
" points.append([p2[0], p2[1], 0, 0.5])\n",
" points.append([p2[0], p2[1], -0.8, 0.5])\n",
" points.append([p1[0], p1[1], -0.8, 0.5])\n",
" return points\n",
"\n",
"\n",
"coil1 = get_points(\n",
" TRACK_SPACING + TRACK_WIDTH,\n",
" (COIL_CENTER_RADIUS - 10),\n",
" COIL_CENTER_RADIUS + 10,\n",
" -15,\n",
" 15,\n",
")\n",
"# move all the points in by COIL_CENTER_RADIUS\n",
"for p in coil1:\n",
" p[0] -= COIL_CENTER_RADIUS\n",
"# rotate the points by 90 degrees\n",
"for p in coil1:\n",
" p[0], p[1] = rotate_point(p[0], p[1], 90)\n",
"coil1 = np.array(coil1).T\n",
"plot_coil2(coil1)\n",
"coil1 = slice_coil(coil1, 0.1)\n",
"coil1 = coil1.T\n",
"print(coil1.shape)\n",
"print(len(coil1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Simple Simulation of a dipole magnet"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# magnetic field at a point x,y,z of a dipole magnet with moment m in the z direction\n",
"def B(x, y, z, m=0.185):\n",
" mu0 = 4 * np.pi * 1e-7\n",
" r = np.sqrt(x**2 + y**2 + z**2)\n",
" return (\n",
" np.array(\n",
" [3 * x * z / r**5, 3 * y * z / r**5, (3 * z**2 / r**5 - 1 / r**3)]\n",
" )\n",
" * m\n",
" * mu0\n",
" )\n",
"\n",
"\n",
"def plot_field_slice(x, y, bx, by, mag, name=\"magnetic_field.png\"):\n",
" # plot the magnetic field\n",
" fig = plt.figure()\n",
" ax = fig.add_subplot(111)\n",
" ax.streamplot(\n",
" x,\n",
" y,\n",
" bx,\n",
" by,\n",
" linewidth=1,\n",
" cmap=plt.cm.inferno,\n",
" density=2,\n",
" arrowstyle=\"->\",\n",
" arrowsize=1.5,\n",
" )\n",
"\n",
" ax.set_xlabel(\"$x$\")\n",
" ax.set_ylabel(\"$y$\")\n",
" ax.set_xlim(-0.1, 0.1)\n",
" ax.set_ylim(-0.1, 0.1)\n",
" ax.set_aspect(\"equal\")\n",
"\n",
" # plot the magniture of the field as an image\n",
" im = ax.imshow(\n",
" mag, extent=[-0.1, 0.1, -0.1, 0.1], origin=\"lower\", cmap=plt.cm.inferno\n",
" )\n",
"\n",
" fig.show()\n",
" # save the figure\n",
" fig.savefig(name)\n",
"\n",
"\n",
"# # calculate the magnetic field at y = 0, over z = -1, 1 and x = -1, 1\n",
"x = np.linspace(-0.1, 0.1, 100)\n",
"z = np.linspace(-0.1, 0.1, 100)\n",
"X, Z = np.meshgrid(x, z)\n",
"Bx, By, Bz = B(X, 0, Z)\n",
"\n",
"plot_field_slice(\n",
" X,\n",
" Z,\n",
" Bx,\n",
" Bz,\n",
" np.log(np.sqrt(Bx**2 + By**2 + Bz**2)),\n",
" \"magnetic_field_side.png\",\n",
")\n",
"\n",
"# # calculate the magnetic field at z = 1, over y = -1, 1 and x = -1, 1\n",
"x = np.linspace(-0.1, 0.1, 100)\n",
"y = np.linspace(-0.1, 0.1, 100)\n",
"X, Y = np.meshgrid(x, y)\n",
"Bx, By, Bz = B(X, Y, 0.01)\n",
"\n",
"plot_field_slice(\n",
" X, Y, Bx, By, np.sqrt(Bx**2 + By**2 + Bz**2), \"magnetic_field_bottom.png\"\n",
")\n",
"\n",
"# calculate the magnetic field in a 3d volume\n",
"x = np.linspace(-0.1, 0.1, 100)\n",
"y = np.linspace(-0.1, 0.1, 100)\n",
"z = np.linspace(-0.1, 0.1, 100)\n",
"X, Y, Z = np.meshgrid(x, y, z)\n",
"Bx, By, Bz = B(X, Y, Z)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# calculate the force on a wire of length l carrying current I at a point x,y,z with a direction vector d\n",
"def F(p, d, I, l):\n",
" return I * l * np.cross(d, B(p[0], p[1], p[2]))\n",
"\n",
"\n",
"def calculate_forces_on_wire_points(points):\n",
" Fx = []\n",
" Fy = []\n",
" Fz = []\n",
" # calculate the force on each point\n",
" for i in range(len(points)):\n",
" # calculate the direction vector\n",
" dx = points[i][0] - points[(i + 1) % len(points)][0]\n",
" dy = points[i][1] - points[(i + 1) % len(points)][1]\n",
" dz = points[i][2] - points[(i + 1) % len(points)][2]\n",
" d = np.array([dx, dy, dz])\n",
" # get the length of d\n",
" l = np.sqrt(dx**2 + dy**2 + dz**2)\n",
" if l > 0:\n",
" # normalise d\n",
" d = d / l\n",
" # calculate the force\n",
" fx, fy, fz = F(points[i], d, points[i][3], l)\n",
" Fx.append(fx)\n",
" Fy.append(fy)\n",
" Fz.append(fz)\n",
" else:\n",
" Fx.append(0)\n",
" Fy.append(0)\n",
" Fz.append(0)\n",
" return Fx, Fy, Fz\n",
"\n",
"\n",
"# locate the loop of wire directly below the magnet\n",
"x = 0.01\n",
"y = 0\n",
"z = -0.002\n",
"r1 = 0.001\n",
"r2 = 0.01\n",
"\n",
"\n",
"points = coil1.copy()\n",
"# scale the points from mm to m\n",
"# shift the coil to the correct position\n",
"for i in range(len(points)):\n",
" points[i][0] = points[i][0] / 1000 + x\n",
" points[i][1] = points[i][1] / 1000 + y\n",
" points[i][2] = points[i][2] / 1000 + z\n",
"\n",
"\n",
"# plot the points in 2D x,y\n",
"fig = plt.figure()\n",
"ax = fig.add_subplot(111)\n",
"ax.plot([p[0] for p in points], [p[1] for p in points])\n",
"ax.set_xlabel(\"$x$\")\n",
"ax.set_ylabel(\"$y$\")\n",
"ax.set_xlim(-0.01 + x, 0.01 + x)\n",
"ax.set_ylim(-0.01 + y, 0.01 + y)\n",
"ax.set_aspect(\"equal\")\n",
"fig.show()\n",
"\n",
"Fx, Fy, Fz = calculate_forces_on_wire_points(points)\n",
"\n",
"\n",
"# plot the wire along with arrows showing the force\n",
"fig = plt.figure()\n",
"ax = fig.add_subplot(111, projection=\"3d\")\n",
"ax.plot([p[0] for p in points], [p[1] for p in points], [p[2] for p in points])\n",
"\n",
"print(sum(Fx) / 9.8)\n",
"\n",
"# subsample the points and force vectors to make the plot clearer\n",
"points = points[::50]\n",
"Fx = Fx[::50]\n",
"Fy = Fy[::50]\n",
"Fz = Fz[::50]\n",
"\n",
"ax.quiver(\n",
" [p[0] for p in points],\n",
" [p[1] for p in points],\n",
" [p[2] for p in points],\n",
" np.sqrt(Fx),\n",
" 0,\n",
" 0,\n",
" length=0.01,\n",
" normalize=True,\n",
")\n",
"ax.set_xlabel(\"$x$\")\n",
"ax.set_ylabel(\"$y$\")\n",
"ax.set_zlabel(\"$z$\")\n",
"ax.set_xlim(x - 0.02, x + 0.02)\n",
"ax.set_ylim(y - 0.02, y + 0.02)\n",
"ax.set_zlim(z - 0.02, z + 0.02)\n",
"ax.set_aspect(\"equal\")\n",
"\n",
"# change the figure size\n",
"fig.set_size_inches(10, 10)\n",
"\n",
"fig.show()\n",
"# coil2 = -0.01311082557350831\n",
"# coil1 = 0.0088435517657609"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def sweep_coil(coil, X):\n",
" Y = np.zeros(len(X))\n",
" Z = -0.01 * np.ones(len(X))\n",
"\n",
" # loop through the locations and calculate the forces, sum up the force in the X direction for each location\n",
" Fx = []\n",
" Fy = []\n",
" Fz = []\n",
" for p in trange(len(X)):\n",
" points = coil.copy()\n",
" # scale the points from mm to m\n",
" # shift the coil to the correct position\n",
" for i in range(len(points)):\n",
" points[i][0] = points[i][0] / 1000 + X[p]\n",
" points[i][1] = points[i][1] / 1000 + Y[p]\n",
" points[i][2] = points[i][2] / 1000 + Z[p]\n",
" Fx_, Fy_, Fz_ = calculate_forces_on_wire_points(points)\n",
" Fx.append(sum(Fx_))\n",
" Fy.append(sum(Fy_))\n",
" Fz.append(sum(Fz_))\n",
" return Fx, Fy, Fz\n",
"\n",
"\n",
"# sweep the coild from -3cm to 3cm in 0.01m steps\n",
"X = np.linspace(-0.03, 0.03, 100)\n",
"Fx_1_straight, Fy_1_straight, Fz_1_straight = sweep_coil(coil1, X)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# plot the force as a function of x\n",
"plt.plot(X, -np.array(Fx_1_straight) / 9.8, label=\"coil1\", color=\"red\")\n",
"# plot a dotted line along y = 0\n",
"plt.plot([X[0], X[-1]], [0, 0], \"--\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# plot the force as a function of x\n",
"plt.plot(X, -np.array(Fz_1_straight) / 9.8, label=\"coil1\", color=\"red\")\n",
"# plot a dotted line along y = 0\n",
"plt.plot([X[0], X[-1]], [0, 0], \"--\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# instead of sweeping horizontally, we'll sweep the coils around a circle\n",
"def sweep_coil_cirlc(coil, coil_center_radius, theta):\n",
" X = coil_center_radius * np.cos(np.deg2rad(theta))\n",
" Y = coil_center_radius * np.sin(np.deg2rad(theta))\n",
" Z = -0.01 * np.ones(100)\n",
"\n",
" # loop through the locations and calculate the forces, sum up the force in the X direction for each location\n",
" Torque = []\n",
" Fx = []\n",
" Fy = []\n",
" Fz = []\n",
" for p in trange(len(theta)):\n",
" angle = np.deg2rad(theta[p]) - np.pi / 2\n",
" x = X[p]\n",
" y = Y[p]\n",
" z = Z[p]\n",
"\n",
" points = coil.copy()\n",
" for i in range(len(points)):\n",
" px = points[i][0] / 1000\n",
" py = points[i][1] / 1000\n",
" pz = points[i][2] / 1000\n",
" # rotate the points so the coil is correctly oriented\n",
" points[i][0] = px * np.cos(angle) - py * np.sin(angle) + x\n",
" points[i][1] = (\n",
" px * np.sin(angle) + py * np.cos(angle) + y - coil_center_radius\n",
" )\n",
" points[i][2] = pz + z\n",
" # feel the force\n",
" Fx_, Fy_, Fz_ = calculate_forces_on_wire_points(points)\n",
" Fx.append(sum(Fx_))\n",
" Fy.append(sum(Fy_))\n",
" Fz.append(sum(Fz_))\n",
" # calculate the torque - which should be 90 degress to the angle\n",
" torque_angle = np.deg2rad(theta[p] - 90)\n",
" Torque.append(sum(Fx_) * np.cos(torque_angle) + sum(Fy_) * np.sin(torque_angle))\n",
"\n",
" return Fx, Fy, Fz, Torque\n",
"\n",
"\n",
"# sweep the coils from -45 to 45 degrees in 1 degree steps\n",
"theta = np.linspace(0, 180, 100)\n",
"Fx_1_curve, Fy_1_curve, Fz_1_curve, Torque_1 = sweep_coil_cirlc(\n",
" coil1, 19.5 / 1000, theta\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.plot(theta, -np.array(Torque_1) / 9.8, label=\"coil1\", color=\"red\")\n",
"# plot a dotted line along y = 0\n",
"plt.plot([theta[0], theta[-1]], [0, 0], \"--\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# plot arrows for the Fx and Fy components\n",
"X = 19.5 * np.cos(np.deg2rad(theta)) / 1000\n",
"Y = 19.5 * np.sin(np.deg2rad(theta)) / 1000\n",
"plt.quiver(X[::5], Y[::5], Fx_1_curve[::5], Fy_1_curve[::5], color=\"red\")\n",
"# make the axis equal so the arrows are not stretched\n",
"plt.axis(\"equal\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.plot(theta, -np.array(Fz_1_curve) / 9.8, label=\"coil1\", color=\"red\")\n",
"plt.plot(theta, np.array(Fz_2_curve) / 9.8, label=\"coil2\", color=\"blue\")\n",
"# plot a dotted line along y = 0\n",
"plt.plot([theta[0], theta[-1]], [0, 0], \"--\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# instead of sweeping horizontally, we'll sweep the coils around a circle\n",
"def sweep_coil_cirlc(coil, coil_center_radius):\n",
" # sweep the coils from -45 to 45 degrees in 1 degree steps\n",
" theta = np.linspace(0, 180, 20)\n",
" X = coil_center_radius * np.cos(np.deg2rad(theta))\n",
" Y = coil_center_radius * np.sin(np.deg2rad(theta))\n",
" Z = 1 * np.ones(100)\n",
"\n",
" # loop through the locations and calculate the forces, sum up the force in the X direction for each location\n",
" Fx = []\n",
" Fy = []\n",
" Fz = []\n",
" for p in trange(len(theta)):\n",
" angle = np.deg2rad(theta[p]) - np.pi / 2\n",
" x = X[p]\n",
" y = Y[p]\n",
" z = Z[p]\n",
"\n",
" points = coil.copy()\n",
" for i in range(len(points)):\n",
" px = points[i][0] / 1000\n",
" py = points[i][1] / 1000\n",
" pz = points[i][2] / 1000\n",
" # rotate the points so the coil is correctly oriented\n",
" points[i][0] = px * np.cos(angle) - py * np.sin(angle) + x\n",
" points[i][1] = (\n",
" px * np.sin(angle) + py * np.cos(angle) + y - coil_center_radius\n",
" )\n",
" points[i][2] = pz + z\n",
" plt.plot([p[0] for p in points], [p[1] for p in points], linewidth=0.5)\n",
" # add the torque arrow to the plot\n",
" torque_angle = np.deg2rad(theta[p] - 90)\n",
" torque_x1 = x\n",
" torque_y1 = y\n",
" torque_x2 = x + 0.01 * np.cos(torque_angle)\n",
" torque_y2 = y + 0.01 * np.sin(torque_angle)\n",
" plt.arrow(\n",
" torque_x1,\n",
" torque_y1,\n",
" torque_x2 - torque_x1,\n",
" torque_y2 - torque_y1,\n",
" head_width=0.001,\n",
" head_length=0.002,\n",
" fc=\"k\",\n",
" ec=\"k\",\n",
" )\n",
"\n",
"\n",
"sweep_coil_cirlc(coil2, 19.5 / 1000)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.10.7 ('venv': venv)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.7"
},
"vscode": {
"interpreter": {
"hash": "1ce20143987840b9786ebb5907032c9c3a8efacbb887dbb0ebc4934f2ad26cb3"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View file

@ -7,7 +7,8 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"import numpy as np\n", "import numpy as np\n",
"import biot_savart_v4_3 as bs" "import biot_savart_v4_3 as bs\n",
"import matplotlib.pyplot as plt"
] ]
}, },
{ {
@ -16,7 +17,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"bs.plot_coil(\"coil-4-top-1.txt\")" "bs.plot_coil(\"coil-4-top-1.csv\")"
] ]
}, },
{ {
@ -25,7 +26,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"bs.plot_coil(\"coil-4-bottom-1.txt\")" "bs.plot_coil(\"coil-4-bottom-1.csv\")"
] ]
}, },
{ {
@ -36,7 +37,7 @@
"source": [ "source": [
"for i in range(0, 12):\n", "for i in range(0, 12):\n",
" bs.write_target_volume(\n", " bs.write_target_volume(\n",
" f\"coil-4-top-{i}.txt\",\n", " f\"coil-4-top-{i}.csv\",\n",
" f\"targetvol-top-{i}.vol\",\n", " f\"targetvol-top-{i}.vol\",\n",
" (6, 6, 1),\n", " (6, 6, 1),\n",
" (-3, -3, -0.5),\n", " (-3, -3, -0.5),\n",
@ -44,7 +45,7 @@
" 0.1,\n", " 0.1,\n",
" )\n", " )\n",
" bs.write_target_volume(\n", " bs.write_target_volume(\n",
" f\"coil-4-bottom-{i}.txt\",\n", " f\"coil-4-bottom-{i}.csv\",\n",
" f\"targetvol-bottom-{i}.vol\",\n", " f\"targetvol-bottom-{i}.vol\",\n",
" (6, 6, 1),\n", " (6, 6, 1),\n",
" (-3, -3, -0.5),\n", " (-3, -3, -0.5),\n",
@ -74,6 +75,62 @@
"# reads the volume we created\n", "# reads the volume we created\n",
"bs.plot_fields(volume, (60, 60, 10), (0, 0, 0), 1, which_plane=\"z\", level=6)" "bs.plot_fields(volume, (60, 60, 10), (0, 0, 0), 1, which_plane=\"z\", level=6)"
] ]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib as plt\n",
"\n",
"# get a slice of the volume at z=6\n",
"slice = [volume[50, :, :, i].T for i in range(3)]\n",
"x_contour = slice[0]\n",
"y_contour = slice[1]\n",
"z_contour = slice[2]\n",
"\n",
"print(x_contour.shape)\n",
"print(y_contour.shape)\n",
"print(z_contour.shape)\n",
"\n",
"x = np.arange(0, 61, 1)\n",
"y = np.arange(0, 11, 1)\n",
"xG, yG = np.meshgrid(x, y)\n",
"# do a quiver plot of the x and the z components - we won't worry about y\n",
"plt.pyplot.quiver(xG, yG, y_contour, z_contour)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig = plt.pyplot.figure()\n",
"ax = fig.add_subplot(111)\n",
"\n",
"# Plot the streamlines with an appropriate colormap and arrow style\n",
"# color = 2 * np.log(np.hypot(x_contour, z_contour))\n",
"ax.streamplot(\n",
" x,\n",
" y,\n",
" y_contour,\n",
" z_contour,\n",
" linewidth=1,\n",
" cmap=plt.cm.inferno,\n",
" density=2,\n",
" arrowstyle=\"->\",\n",
" arrowsize=1.5,\n",
")\n",
"\n",
"ax.set_xlabel(\"$x$\")\n",
"ax.set_ylabel(\"$y$\")\n",
"# ax.set_xlim(-2,2)\n",
"# ax.set_ylim(-2,2)\n",
"ax.set_aspect(\"equal\")\n",
"fig.show()"
]
} }
], ],
"metadata": { "metadata": {