{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "import matplotlib as plt\n", "import scipy\n", "from skspatial.objects import LineSegment, Line\n", "from enum import Enum" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "VIA_DIAM = 0.8\n", "VIA_DRILL = 0.4\n", "STATOR_HOLE_RADIUS = 4\n", "TRACK_WIDTH = 0.2\n", "TRACK_SPACING = 0.2\n", "TURNS = 11\n", "STATOR_RADIUS = 20\n", "Layer = Enum(\"Layer\", \"FRONT BACK\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "template = [(-1.5, -0.1), (1.5, -2), (2.0, -1), (2.0, 1), (1.5, 2), (-1.5, 0.1)]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# plot the template shape wrapping around to the first point\n", "plt.pyplot.plot(\n", " [x for x, y in template] + [template[0][0]],\n", " [y for x, y in template] + [template[0][1]],\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# this speeds things up dramatically as we only have to compute the line intersections once\n", "# there are probably much faster ways of doing this - we're just doing a brute force search\n", "# for the intersections - consult algorithms from games for inspiration...\n", "def get_template_point_cache(template):\n", " # sweep a line from the origin through 360 degress times the number of turns in 1 degree increments\n", " # and find the intersection points with the template shape\n", " cache = {}\n", " for angle in np.arange(0, 360 + 1, 1):\n", " line = LineSegment(\n", " np.array([0, 0]),\n", " np.array(\n", " [1000 * np.cos(np.deg2rad(angle)), 1000 * np.sin(np.deg2rad(angle))]\n", " ),\n", " )\n", " for i in range(len(template)):\n", " segment = LineSegment(\n", " np.array(template[i]), np.array(template[(i + 1) % len(template)])\n", " )\n", " try:\n", " intersection = line.intersect_line_segment(segment)\n", " if intersection is not None:\n", " cache[angle] = (intersection, segment)\n", " except ValueError:\n", " pass\n", " return cache\n", "\n", "\n", "def get_point(angle, template, layer, spacing, cache):\n", " if layer == Layer.BACK:\n", " angle = angle + 180\n", " intersection, segment = cache[angle % 360]\n", " return intersection, segment\n", "\n", "\n", "# get the points in a coil shape\n", "# Use reverse for bottom layer (basically flips the y coordinate so that the coil goes in the opposite direction)\n", "# Also rotates the endpoints by 90 degress so that the exit point on the bottom layer is to the left hand side\n", "def get_points(template, turns, spacing, layer=Layer.FRONT, cache=None):\n", " if cache is None:\n", " cache = get_template_point_cache(template)\n", " coil_points = []\n", " last_segment = None\n", " for angle in np.arange(0, 360 * turns + 1, 1):\n", " offset = spacing * angle / 360\n", " intersection, segment = get_point(angle, template, layer, spacing, cache)\n", " vector = np.array(segment.point_a) - np.array(segment.point_b)\n", " normal = vector / np.linalg.norm(vector)\n", " # rotate the vector 90 degrees\n", " normal = np.array([-normal[1], normal[0]])\n", " # move the intersection point along the normal vector by the spacing\n", " coil_point = intersection + normal * offset\n", " if layer == Layer.BACK:\n", " coil_points.append((coil_point[0], -coil_point[1], segment))\n", " else:\n", " coil_points.append((coil_point[0], coil_point[1], segment))\n", " # run through the generated coil points and where the line segments change add a point that is the intersection of the previous and next lines\n", " # this prevents any corner cutting\n", " points = []\n", " last_segment = coil_points[0][2]\n", " for i in range(len(coil_points)):\n", " x, y, segment = coil_points[i]\n", " same_segment = (\n", " (last_segment.point_a == segment.point_a).all()\n", " and (last_segment.point_b == segment.point_b).all()\n", " ).all()\n", " if (not same_segment) and i > 2 and i < len(coil_points) - 2:\n", " # create a line from the previous two points\n", " line = Line(\n", " np.array(coil_points[i - 2][0:2]),\n", " np.array(coil_points[i - 2][0:2]) - np.array(coil_points[i - 1][0:2]),\n", " )\n", " # create a line from the next two points\n", " line2 = Line(\n", " np.array(coil_points[i][0:2]),\n", " np.array(coil_points[i][0:2]) - np.array(coil_points[i + 1][0:2]),\n", " )\n", " # find the intersection of the two lines\n", " intersection = line.intersect_line(line2)\n", " # add the intersection point to the list of points\n", " points.append(intersection)\n", " last_segment = segment\n", " points.append((x, y))\n", " return points\n", "\n", "\n", "def optimize_points(points):\n", " # follow the line and remove points that are in the same direction as the previous poin\n", " # keep doing this until the direction changes significantly\n", " # this is a very simple optimization that removes a lot of points\n", " # it's not perfect but it's a good start\n", " optimized_points = []\n", " for i in range(len(points)):\n", " if i == 0:\n", " optimized_points.append(points[i])\n", " else:\n", " vector1 = np.array(points[i]) - np.array(points[i - 1])\n", " vector2 = np.array(points[(i + 1) % len(points)]) - np.array(points[i])\n", " length1 = np.linalg.norm(vector1)\n", " length2 = np.linalg.norm(vector2)\n", " if length1 > 0 and length2 > 0:\n", " dot = np.dot(vector1, vector2) / (length1 * length2)\n", " # clamp dot between -1 and 1\n", " dot = max(-1, min(1, dot))\n", " angle = np.arccos(dot)\n", " if angle > np.deg2rad(0.1):\n", " optimized_points.append(points[i])\n", " print(\"Optimised from {} to {} points\".format(len(points), len(optimized_points)))\n", " return optimized_points\n", "\n", "\n", "def chaikin(points, iterations):\n", " if iterations == 0:\n", " return points\n", " l = len(points)\n", " smoothed = []\n", " for i in range(l - 1):\n", " x1, y1 = points[i]\n", " x2, y2 = points[i + 1]\n", " smoothed.append([0.9 * x1 + 0.1 * x2, 0.9 * y1 + 0.1 * y2])\n", " smoothed.append([0.1 * x1 + 0.9 * x2, 0.1 * y1 + 0.9 * y2])\n", " smoothed.append(points[l - 1])\n", " return chaikin(smoothed, iterations - 1)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "points = get_points(template, 10, TRACK_SPACING + TRACK_WIDTH, Layer.BACK, None)\n", "optimized_points = chaikin(optimize_points(points), 3)\n", "# df = pd.DataFrame(points, columns=['x', 'y'])\n", "# ax = df.plot.line(x='x', y='y', label='Coil A', color='blue')\n", "# ax.axis('equal')\n", "\n", "df_optim = pd.DataFrame(optimized_points, columns=[\"x\", \"y\"])\n", "\n", "ax = df_optim.plot.line(x=\"x\", y=\"y\", label=\"Coil A\", color=\"green\")\n", "ax.axis(\"equal\")\n", "print(len(df_optim), len(df))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cache = get_template_point_cache(template)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "points_f = get_points(template, TURNS, TRACK_SPACING + TRACK_WIDTH, Layer.FRONT, cache)\n", "points_b = get_points(template, TURNS, TRACK_SPACING + TRACK_WIDTH, Layer.BACK, cache)\n", "\n", "points_f = [(0, 0)] + chaikin(optimize_points(points_f), 3)\n", "points_b = [(0, 0)] + chaikin(optimize_points(points_b), 3)\n", "\n", "COIL_CENTER_RADIUS = STATOR_RADIUS / 2 + 1.5\n", "\n", "angle_A = 0\n", "angle_B = 120\n", "angle_C = 240\n", "\n", "# roate the points by the required angle\n", "def rotate(points, angle):\n", " return [\n", " [\n", " x * np.cos(np.deg2rad(angle)) - y * np.sin(np.deg2rad(angle)),\n", " x * np.sin(np.deg2rad(angle)) + y * np.cos(np.deg2rad(angle)),\n", " ]\n", " for x, y in points\n", " ]\n", "\n", "\n", "# move the points out to the distance at the requited angle\n", "def translate(points, distance, angle):\n", " return [\n", " [\n", " x + distance * np.cos(np.deg2rad(angle)),\n", " y + distance * np.sin(np.deg2rad(angle)),\n", " ]\n", " for x, y in points\n", " ]\n", "\n", "\n", "# flip the y coordinate\n", "def flip(points):\n", " return [[x, -y] for x, y in points]\n", "\n", "\n", "# the main coils\n", "coil_A_f = translate(rotate(points_f, angle_A), COIL_CENTER_RADIUS, angle_A)\n", "coil_A_b = translate(rotate(points_b, angle_A), COIL_CENTER_RADIUS, angle_A)\n", "\n", "coil_B_f = translate(rotate(points_f, angle_B), COIL_CENTER_RADIUS, angle_B)\n", "coil_B_b = translate(rotate(points_b, angle_B), COIL_CENTER_RADIUS, angle_B)\n", "\n", "coil_C_f = translate(rotate(points_f, angle_C), COIL_CENTER_RADIUS, angle_C)\n", "coil_C_b = translate(rotate(points_b, angle_C), COIL_CENTER_RADIUS, angle_C)\n", "\n", "# the opposite coils - for more power!\n", "angle_A_opp = angle_A + 180\n", "angle_B_opp = angle_B + 180\n", "angle_C_opp = angle_C + 180\n", "\n", "print(angle_A_opp, angle_B_opp, angle_C_opp)\n", "\n", "coil_A_opp_f = translate(\n", " rotate(flip(points_f), angle_A_opp), COIL_CENTER_RADIUS, angle_A_opp\n", ")\n", "coil_A_opp_b = translate(\n", " rotate(flip(points_b), angle_A_opp), COIL_CENTER_RADIUS, angle_A_opp\n", ")\n", "\n", "coil_B_opp_f = translate(\n", " rotate(flip(points_f), angle_B_opp), COIL_CENTER_RADIUS, angle_B_opp\n", ")\n", "coil_B_opp_b = translate(\n", " rotate(flip(points_b), angle_B_opp), COIL_CENTER_RADIUS, angle_B_opp\n", ")\n", "\n", "coil_C_opp_f = translate(\n", " rotate(flip(points_f), angle_C_opp), COIL_CENTER_RADIUS, angle_C_opp\n", ")\n", "coil_C_opp_b = translate(\n", " rotate(flip(points_b), angle_C_opp), COIL_CENTER_RADIUS, angle_C_opp\n", ")\n", "\n", "# connect the front copper opposite coils together\n", "common_connection_radius = STATOR_RADIUS - (TRACK_WIDTH + TRACK_SPACING)\n", "common_coil_connections_b = [\n", " (\n", " common_connection_radius * np.cos(np.deg2rad(angle)),\n", " common_connection_radius * np.sin(np.deg2rad(angle)),\n", " )\n", " for angle in np.arange(angle_A_opp, angle_C_opp + 5, 5)\n", "]\n", "coil_A_opp_f.append(\n", " (\n", " common_connection_radius * np.cos(np.deg2rad(angle_A_opp)),\n", " common_connection_radius * np.sin(np.deg2rad(angle_A_opp)),\n", " )\n", ")\n", "coil_B_opp_f.append(\n", " (\n", " common_connection_radius * np.cos(np.deg2rad(angle_B_opp)),\n", " common_connection_radius * np.sin(np.deg2rad(angle_B_opp)),\n", " )\n", ")\n", "coil_C_opp_f.append(\n", " (\n", " common_connection_radius * np.cos(np.deg2rad(angle_C_opp)),\n", " common_connection_radius * np.sin(np.deg2rad(angle_C_opp)),\n", " )\n", ")\n", "\n", "# connect coil A to it's opposite\n", "connection_radius1 = STATOR_HOLE_RADIUS + (TRACK_SPACING)\n", "connection_radius2 = connection_radius1 + (TRACK_SPACING + VIA_DIAM / 2)\n", "# draw a 45 degree line from coil A at connection radius 1\n", "# then connect up to connection radius 2\n", "# draw a 45 degree line to the opposite coil\n", "coil_A_b.append(\n", " (\n", " connection_radius1 * np.cos(np.deg2rad(angle_A)),\n", " connection_radius1 * np.sin(np.deg2rad(angle_A)),\n", " )\n", ")\n", "coil_A_opp_b.append(\n", " (\n", " connection_radius2 * np.cos(np.deg2rad(angle_A_opp)),\n", " connection_radius2 * np.sin(np.deg2rad(angle_A_opp)),\n", " )\n", ")\n", "a_connection_b = [\n", " (\n", " connection_radius1 * np.cos(np.deg2rad(angle)),\n", " connection_radius1 * np.sin(np.deg2rad(angle)),\n", " )\n", " for angle in np.arange(angle_A, angle_A + 90 + 5, 5)\n", "]\n", "a_connection_f = [\n", " (\n", " connection_radius2 * np.cos(np.deg2rad(angle)),\n", " connection_radius2 * np.sin(np.deg2rad(angle)),\n", " )\n", " for angle in np.arange(angle_A + 90, angle_A + 180 + 5, 5)\n", "]\n", "a_connection_b.append(a_connection_f[0])\n", "\n", "coil_B_b.append(\n", " (\n", " connection_radius1 * np.cos(np.deg2rad(angle_B)),\n", " connection_radius1 * np.sin(np.deg2rad(angle_B)),\n", " )\n", ")\n", "coil_B_opp_b.append(\n", " (\n", " connection_radius2 * np.cos(np.deg2rad(angle_B_opp)),\n", " connection_radius2 * np.sin(np.deg2rad(angle_B_opp)),\n", " )\n", ")\n", "b_connection_b = [\n", " (\n", " connection_radius1 * np.cos(np.deg2rad(angle)),\n", " connection_radius1 * np.sin(np.deg2rad(angle)),\n", " )\n", " for angle in np.arange(angle_B, angle_B + 90 + 5, 5)\n", "]\n", "b_connection_f = [\n", " (\n", " connection_radius2 * np.cos(np.deg2rad(angle)),\n", " connection_radius2 * np.sin(np.deg2rad(angle)),\n", " )\n", " for angle in np.arange(angle_B + 90, angle_B + 180 + 5, 5)\n", "]\n", "b_connection_b.append(b_connection_f[0])\n", "\n", "coil_C_b.append(\n", " (\n", " connection_radius1 * np.cos(np.deg2rad(angle_C)),\n", " connection_radius1 * np.sin(np.deg2rad(angle_C)),\n", " )\n", ")\n", "coil_C_opp_b.append(\n", " (\n", " connection_radius2 * np.cos(np.deg2rad(angle_C_opp)),\n", " connection_radius2 * np.sin(np.deg2rad(angle_C_opp)),\n", " )\n", ")\n", "c_connection_b = [\n", " (\n", " connection_radius1 * np.cos(np.deg2rad(angle)),\n", " connection_radius1 * np.sin(np.deg2rad(angle)),\n", " )\n", " for angle in np.arange(angle_C, angle_C + 90 + 5, 5)\n", "]\n", "c_connection_f = [\n", " (\n", " connection_radius2 * np.cos(np.deg2rad(angle)),\n", " connection_radius2 * np.sin(np.deg2rad(angle)),\n", " )\n", " for angle in np.arange(angle_C + 90, angle_C + 180 + 5, 5)\n", "]\n", "c_connection_b.append(c_connection_f[0])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def create_track(points):\n", " return [{\"x\": x, \"y\": y} for x, y in points]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# dump out the results to json\n", "json_result = {\n", " \"parameters\": {\n", " \"trackWidth\": TRACK_WIDTH,\n", " \"statorHoleRadius\": STATOR_HOLE_RADIUS,\n", " \"viaDiameter\": VIA_DIAM,\n", " \"viaDrillDiameter\": VIA_DRILL,\n", " },\n", " \"vias\": [\n", " {\n", " \"x\": COIL_CENTER_RADIUS * np.cos(np.deg2rad(angle_A)),\n", " \"y\": COIL_CENTER_RADIUS * np.sin(np.deg2rad(angle_A)),\n", " },\n", " {\n", " \"x\": COIL_CENTER_RADIUS * np.cos(np.deg2rad(angle_B)),\n", " \"y\": COIL_CENTER_RADIUS * np.sin(np.deg2rad(angle_B)),\n", " },\n", " {\n", " \"x\": COIL_CENTER_RADIUS * np.cos(np.deg2rad(angle_C)),\n", " \"y\": COIL_CENTER_RADIUS * np.sin(np.deg2rad(angle_C)),\n", " },\n", " {\n", " \"x\": COIL_CENTER_RADIUS * np.cos(np.deg2rad(angle_A_opp)),\n", " \"y\": COIL_CENTER_RADIUS * np.sin(np.deg2rad(angle_A_opp)),\n", " },\n", " {\n", " \"x\": COIL_CENTER_RADIUS * np.cos(np.deg2rad(angle_B_opp)),\n", " \"y\": COIL_CENTER_RADIUS * np.sin(np.deg2rad(angle_B_opp)),\n", " },\n", " {\n", " \"x\": COIL_CENTER_RADIUS * np.cos(np.deg2rad(angle_C_opp)),\n", " \"y\": COIL_CENTER_RADIUS * np.sin(np.deg2rad(angle_C_opp)),\n", " },\n", " {\n", " \"x\": common_connection_radius * np.cos(np.deg2rad(angle_A_opp)),\n", " \"y\": common_connection_radius * np.sin(np.deg2rad(angle_A_opp)),\n", " },\n", " {\n", " \"x\": common_connection_radius * np.cos(np.deg2rad(angle_B_opp)),\n", " \"y\": common_connection_radius * np.sin(np.deg2rad(angle_B_opp)),\n", " },\n", " {\n", " \"x\": common_connection_radius * np.cos(np.deg2rad(angle_C_opp)),\n", " \"y\": common_connection_radius * np.sin(np.deg2rad(angle_C_opp)),\n", " },\n", " # coil A connections\n", " {\"x\": a_connection_f[0][0], \"y\": a_connection_f[0][1]},\n", " {\"x\": a_connection_f[-1][0], \"y\": a_connection_f[-1][1]},\n", " # coil B connections\n", " {\"x\": b_connection_f[0][0], \"y\": b_connection_f[0][1]},\n", " {\"x\": b_connection_f[-1][0], \"y\": b_connection_f[-1][1]},\n", " # coil C connections\n", " {\"x\": c_connection_f[0][0], \"y\": c_connection_f[0][1]},\n", " {\"x\": c_connection_f[-1][0], \"y\": c_connection_f[-1][1]},\n", " ],\n", " \"silk\": [\n", " {\n", " \"x\": COIL_CENTER_RADIUS * np.cos(np.deg2rad(angle_A)),\n", " \"y\": COIL_CENTER_RADIUS * np.sin(np.deg2rad(angle_A)),\n", " \"text\": \"A\",\n", " },\n", " {\n", " \"x\": COIL_CENTER_RADIUS * np.cos(np.deg2rad(angle_B)),\n", " \"y\": COIL_CENTER_RADIUS * np.sin(np.deg2rad(angle_B)),\n", " \"text\": \"B\",\n", " },\n", " {\n", " \"x\": COIL_CENTER_RADIUS * np.cos(np.deg2rad(angle_C)),\n", " \"y\": COIL_CENTER_RADIUS * np.sin(np.deg2rad(angle_C)),\n", " \"text\": \"C\",\n", " },\n", " ],\n", " \"tracks\": {\n", " \"f\": [\n", " create_track(points)\n", " for points in [\n", " coil_A_f,\n", " coil_A_opp_f,\n", " coil_B_f,\n", " coil_B_opp_f,\n", " coil_C_f,\n", " coil_C_opp_f,\n", " a_connection_f,\n", " b_connection_f,\n", " c_connection_f,\n", " ]\n", " ],\n", " \"b\": [\n", " create_track(points)\n", " for points in [\n", " coil_A_b,\n", " coil_A_opp_b,\n", " coil_B_b,\n", " coil_B_opp_b,\n", " coil_C_b,\n", " coil_C_opp_b,\n", " common_coil_connections_b,\n", " a_connection_b,\n", " b_connection_b,\n", " c_connection_b,\n", " ]\n", " ],\n", " },\n", "}\n", "\n", "import json\n", "\n", "json.dump(json_result, open(\"coil.json\", \"w\"))\n", "\n", "\n", "df = pd.DataFrame(coil_A_f, columns=[\"x\", \"y\"])\n", "ax = df.plot.line(x=\"x\", y=\"y\", label=\"Coil A\", color=\"blue\")\n", "ax.axis(\"equal\")\n", "df = pd.DataFrame(coil_A_b, columns=[\"x\", \"y\"])\n", "ax = df.plot.line(x=\"x\", y=\"y\", label=\"Coil B\", color=\"green\")\n", "ax.axis(\"equal\")\n", "\n", "# plot all three coils on the same graph\n", "# df = pd.DataFrame(coil_A, columns=['x', 'y'])\n", "# ax = df.plot.line(x='x', y='y', label='Coil A', color='blue')\n", "# ax.axis('equal')\n", "# df = pd.DataFrame(coil_B, columns=['x', 'y'])\n", "# df.plot.line(x='x', y='y', ax=ax, label='Coil B', color='green')\n", "# df = pd.DataFrame(coil_C, columns=['x', 'y'])\n", "# df.plot.line(x='x', y='y', ax=ax, label='Coil C', color='red')\n", "\n", "# df = pd.DataFrame(coil_A_opposite, columns=['x', 'y'])\n", "# df.plot.line(x='x', y='y', ax=ax, label='Coil A Opposite', color='blue')\n", "# df = pd.DataFrame(coil_B_opposite, columns=['x', 'y'])\n", "# df.plot.line(x='x', y='y', ax=ax, label='Coil B Opposite', color='green')\n", "# df = pd.DataFrame(coil_C_opposite, columns=['x', 'y'])\n", "# df.plot.line(x='x', y='y', ax=ax, label='Coil C Opposite', color='red')" ] } ], "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 }