-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathspiral_simple_circle.py
173 lines (131 loc) · 5.42 KB
/
spiral_simple_circle.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import math
from scipy import integrate
from scipy import optimize
from pathlib import Path
from configparser import ConfigParser
import unittest
from helper_conversions import *
'''
EXPERIMENTAL
Functions that define a constant trace width circular spiral
'''
# Read configuration
config = ConfigParser()
config.read(Path(__file__).with_name('config.ini'))
config = config['Configuration']
#### ROUND FUNCTIONS ####
def length_of_round_spiral(a: float, b: float, theta: float) -> float:
'''
Returns the length of a circular archimedean spiral.
Proof: https://planetcalc.com/3707/
Parameters:
a (float): The outer radius of the spiral
b (float): The decrease in radius per 1 radian of rotation
theta (float): The number of radians
Returns:
Length (float): The length of the spiral
'''
return integrate.quad(lambda t: math.sqrt((a - b*t)**2 + b**2), 0, theta)[0]
def area_sum_of_round_spiral(a: float, b: float, theta: float) -> float:
'''
Returns the sum of coil areas of a circular archimedean spiral.
All else constant, area-sum is proportional to magnetorquer torque.
Parameters:
a (float): The outer radius of the spiral
b (float): The decrease in radius per 1 radian of rotation
theta (float): The number of radians
Returns:
Area-sum (float): The area-sum of the spiral
'''
return integrate.quad(lambda t: 0.5*(a - b*t)**2, 0, theta)[0]
def spiral(
length, spacing, outer_radius=config.getfloat('OuterRadius')
) -> tuple:
'''
Returns the sum of coil areas of a circular archimedean spiral.
All else constant, area-sum is proportional to magnetorquer torque.
Parameters:
length (float): The length of the spiral's line
spacing (float): The decrease in radius per 1 turn of rotation
outer_radius (float): The outer radius of the spiral
Returns:
num_of_coils (float): The number of coils in the spiral
inner_radius (float): The inner radius of the spiral
area_sum (float): The total area-sum of the spiral
'''
a = outer_radius
b = spacing / (2 * math.pi)
if b == 0: # If b is 0 we have a perfect circle
theta = length / a # Circle arc length formula
else:
# a/b gives theta that results in 0 inner radius
max_theta = a / b
# If user gives a longer length, that won't fit, so return NaN
if length > length_of_round_spiral(a, b, max_theta):
return math.nan, math.nan, math.nan
# Use math to find theta that gives a spiral of the desired length
theta = optimize.brentq(
lambda t: length_of_round_spiral(a, b, t) - length, 0, max_theta)
# Calculate and return other properties from theta
num_of_coils = theta / (2 * math.pi)
inner_radius = a - b*theta
area_sum = area_sum_of_round_spiral(a, b, theta) * 1e-6
return area_sum, inner_radius, num_of_coils
# Returns max trace length physically possible.
# Takes outer radius and a function that defines spacing
def max_trace_length(resistance, outer_layer):
'''
Calculates the maximum length of wire that can fit on the spiral.
Uses binary search and finds highest length that doesn't receive NaN
from spirals.properties_of_square_spiral().
Paramters:
resistance (float - ohms): The intended resistance of the spiral.
outer_layer (bool): States whether spiral is on outer layer of the PCB
since that influences trace thickness
Returns:
max_length (float - mm): Maximum length of wire that can fit on the spiral.
'''
lower = 0
upper = 1e6 # TODO: Algorithmatize this hard coded value
while upper - lower > upper*0.001:
length_guess = (upper + lower)/2
s = spacing_from_length(length_guess, resistance, outer_layer)
if math.isnan(spiral(length_guess, s)[0]):
upper = length_guess
else:
lower = length_guess
max_length = lower
return max_length
def spiral_of_resistance(resistance, outer_layer):
# Dummy function to meet requirements of `optimize.minimize_scalar`
def neg_area_sum_from_length(length):
s = spacing_from_length(length, resistance, outer_layer)
return -spiral(length, s)[0]
max_length = max_trace_length(resistance, outer_layer)
# Finds length that gives maximum area-sum
length = optimize.minimize_scalar(
neg_area_sum_from_length,
bounds=(0, max_length),
method='bounded'
).x
# Calculate data from the optimal length
spacing = spacing_from_length(length, resistance, outer_layer)
optimal = spiral(length, spacing)
# Return coil spacing, number of coils, and area-sum
return *optimal, spacing, length
class TestRoundSpiral(unittest.TestCase):
# Circle with 2 coils.
def test_with_two_coils(self):
result = spiral(4 * math.pi, 0, 1)
self.assertAlmostEqual(result[0], 2 * math.pi * 1e-6)
self.assertAlmostEqual(result[1], 1)
self.assertAlmostEqual(result[2], 2)
# Compare with results received from https://planetcalc.com/9063/
def test_example_1(self):
result = spiral(100, 1, 10)
self.assertAlmostEqual(result[0], 457.73601090254937 * 1e-6)
self.assertAlmostEqual(result[1], 8.25674652261)
# self-calculated.
self.assertAlmostEqual(result[2], 1.74325347739317)
if __name__ == "__main__":
unittest.main()