petal-qc 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of petal-qc might be problematic. Click here for more details.

Files changed (51) hide show
  1. petal_qc/BTreport/CheckBTtests.py +321 -0
  2. petal_qc/BTreport/__init__.py +0 -0
  3. petal_qc/BTreport/bustapeReport.py +144 -0
  4. petal_qc/__init__.py +14 -0
  5. petal_qc/metrology/Cluster.py +90 -0
  6. petal_qc/metrology/DataFile.py +47 -0
  7. petal_qc/metrology/PetalMetrology.py +327 -0
  8. petal_qc/metrology/__init__.py +0 -0
  9. petal_qc/metrology/all2csv.py +57 -0
  10. petal_qc/metrology/analyze_locking_points.py +597 -0
  11. petal_qc/metrology/cold_noise.py +106 -0
  12. petal_qc/metrology/comparisonTable.py +59 -0
  13. petal_qc/metrology/convert_mitutoyo.py +175 -0
  14. petal_qc/metrology/convert_smartscope.py +145 -0
  15. petal_qc/metrology/coreMetrology.py +402 -0
  16. petal_qc/metrology/data2csv.py +63 -0
  17. petal_qc/metrology/do_Metrology.py +117 -0
  18. petal_qc/metrology/flatness4nigel.py +157 -0
  19. petal_qc/metrology/gtkutils.py +120 -0
  20. petal_qc/metrology/petal_flatness.py +353 -0
  21. petal_qc/metrology/show_data_file.py +118 -0
  22. petal_qc/metrology/testSummary.py +37 -0
  23. petal_qc/metrology/test_paralelism.py +71 -0
  24. petal_qc/thermal/CSVImage.py +69 -0
  25. petal_qc/thermal/DebugPlot.py +76 -0
  26. petal_qc/thermal/IRBFile.py +768 -0
  27. petal_qc/thermal/IRCore.py +110 -0
  28. petal_qc/thermal/IRDataGetter.py +359 -0
  29. petal_qc/thermal/IRPetal.py +1338 -0
  30. petal_qc/thermal/IRPetalParam.py +71 -0
  31. petal_qc/thermal/PetalColorMaps.py +62 -0
  32. petal_qc/thermal/Petal_IR_Analysis.py +142 -0
  33. petal_qc/thermal/PipeFit.py +598 -0
  34. petal_qc/thermal/__init__.py +0 -0
  35. petal_qc/thermal/analyze_IRCore.py +417 -0
  36. petal_qc/thermal/contours.py +378 -0
  37. petal_qc/thermal/create_IRCore.py +185 -0
  38. petal_qc/thermal/pipe_read.py +182 -0
  39. petal_qc/thermal/show_IR_petal.py +420 -0
  40. petal_qc/utils/Geometry.py +756 -0
  41. petal_qc/utils/Progress.py +182 -0
  42. petal_qc/utils/__init__.py +0 -0
  43. petal_qc/utils/all_files.py +35 -0
  44. petal_qc/utils/docx_utils.py +186 -0
  45. petal_qc/utils/fit_utils.py +188 -0
  46. petal_qc/utils/utils.py +180 -0
  47. petal_qc-0.0.0.dist-info/METADATA +29 -0
  48. petal_qc-0.0.0.dist-info/RECORD +51 -0
  49. petal_qc-0.0.0.dist-info/WHEEL +5 -0
  50. petal_qc-0.0.0.dist-info/entry_points.txt +3 -0
  51. petal_qc-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,756 @@
1
+ """A collection of methods t ocompute flatness or fit points to a plame."""
2
+ import math
3
+ from inspect import isfunction
4
+
5
+ import numpy as np
6
+ import numpy.linalg as linalg
7
+ from scipy.spatial import Delaunay, ConvexHull
8
+
9
+
10
+ def vector_angle(v1, v2):
11
+ """Copmpute angle between given vectors."""
12
+ u1 = v1 / linalg.norm(v1)
13
+ u2 = v2 / linalg.norm(v2)
14
+
15
+ y = u1 - u2
16
+ x = u1 + u2
17
+
18
+ a0 = 2 * np.arctan(linalg.norm(y) / linalg.norm(x))
19
+
20
+ if (not np.signbit(a0)) or np.signbit(np.pi - a0):
21
+ return a0
22
+ elif np.signbit(a0):
23
+ return 0.0
24
+ else:
25
+ return np.pi
26
+
27
+
28
+ def flatness_LSPL(M):
29
+ """Compute flatness according to least squares reference plane method.
30
+
31
+ ISO/TS 12781-1
32
+
33
+ Args:
34
+ M (np.ndarray): The data nx3 array
35
+
36
+ Return
37
+ (float) - the computed flatness
38
+ """
39
+
40
+ # calculate the center of mass and translate all points
41
+ com = np.sum(M, axis=0) / len(M)
42
+ q = M - com
43
+
44
+ # calculate 3x3 matrix. The inner product returns total sum of 3x3 matrix
45
+ Q = np.dot(q.T, q)
46
+
47
+ # Calculate eigenvalues and eigenvectors
48
+ la, vectors = np.linalg.eig(Q)
49
+
50
+ # Extract the eigenvector of the minimum eigenvalue
51
+ n = vectors.T[np.argmin(la)]
52
+
53
+ e = np.dot(q, n)
54
+
55
+ eplus = abs(np.amax(e[np.where(e > 0)]))
56
+ eminus = abs(np.amin(e[np.where(e < 0)]))
57
+
58
+ flatness = eplus + eminus
59
+ return flatness
60
+
61
+
62
+ def flatness_conhull(M):
63
+ """Compute (MZPL) flatness by convex hull algorithm.
64
+
65
+ Robust Convex Hull-based Algoritm for Straightness and Flatness
66
+ Determination in Coordinate Measuring (Gyula Hermann)
67
+
68
+ Args:
69
+ M: point array of size (npoints, ndim)
70
+
71
+ Returns
72
+ -------
73
+ flatness - the computed flatness
74
+
75
+ """
76
+ X = M[:, 0]
77
+ Y = M[:, 1]
78
+ Z = M[:, 2]
79
+ max_dis_local = []
80
+ hull = ConvexHull(M, incremental=False, qhull_options=None)
81
+ for plane in hull.equations:
82
+ dis = np.abs(plane[0] * X[:] + plane[1] * Y[:] + plane[2] * Z[:] + plane[3])
83
+ max_dis_local.append(np.max(dis))
84
+
85
+ return np.min(max_dis_local)
86
+
87
+
88
+ def flatness_conhull_old(M):
89
+ """Compute flatness by convex hull algorithm.
90
+
91
+ Robust Convex Hull-based Algoritm for Straightness and Flatness
92
+ Determination in Coordinate Measuring (Gyula Hermann)
93
+
94
+ Args:
95
+ M: point array of size (npoints, ndim)
96
+
97
+ Returns
98
+ -------
99
+ flatness - the computed flatness
100
+
101
+ """
102
+ X = M[:, 0]
103
+ Y = M[:, 1]
104
+ Z = M[:, 2]
105
+ ch = Delaunay(M).convex_hull
106
+
107
+ N = ch.shape[0]
108
+ max_dis_local = np.zeros([N, 1])
109
+ for i in range(0, N):
110
+ P1 = np.array([X[ch[i, 0]], Y[ch[i, 0]], Z[ch[i, 0]]])
111
+ P2 = np.array([X[ch[i, 1]], Y[ch[i, 1]], Z[ch[i, 1]]])
112
+ P3 = np.array([X[ch[i, 2]], Y[ch[i, 2]], Z[ch[i, 2]]])
113
+
114
+ normal = np.cross(P1-P2, P1-P3)
115
+
116
+ D = -normal[0] * P3[0] - normal[1] * P3[1] - normal[2] * P3[2]
117
+
118
+ plano_0 = np.array([normal[0], normal[1], normal[2], D])
119
+ plano = plano_0 / np.sqrt(np.sum(plano_0**2))
120
+
121
+ dis = np.abs(plano[0] * X[:] + plano[1] * Y[:] + plano[2] * Z[:] +
122
+ plano[3]) / np.sqrt(plano[0]**2 + plano[1]**2 + plano[2]**2)
123
+
124
+ max_dis_local[i] = np.max(dis)
125
+
126
+ # planos(i, :) = plano(:);
127
+
128
+ return np.min(max_dis_local)
129
+ # plano_opt = planos(find(max_dis_local == flatness), :);
130
+
131
+
132
+ def project_to_plane(data, V, M=None):
133
+ """Project data points to a plane.
134
+
135
+ Args:
136
+ ----
137
+ data (array): Data array
138
+ V (matrix): The transformation matrix
139
+ M (vector): The mean value of the data
140
+
141
+ Returns
142
+ -------
143
+ array: the data projectoed onto the plane.
144
+
145
+ """
146
+ npts = data.shape[0]
147
+ out = np.zeros(data.shape)
148
+
149
+ if M is not None:
150
+ for i in range(0, npts):
151
+ out[i, :] = np.dot(data[i, :] - M, V)
152
+
153
+ else:
154
+ for i in range(0, npts):
155
+ out[i, :] = np.dot(data[i, :], V)
156
+
157
+ return out
158
+
159
+
160
+ def fit_plane(data, use_average=7):
161
+ """Fit plane where Z dispersion is smaller.
162
+
163
+ This is the plane defined by the eigenvector with smaller eigenvalue
164
+ of the covariance matrix of the data.
165
+
166
+ Args:
167
+ -----
168
+ data: The data
169
+ use_average: bitted word telling which components of average
170
+ should be used.
171
+
172
+ Returns
173
+ -------
174
+ val: the data in the new reference
175
+ V: the eigenvectors
176
+ M: the "center of gravity" of the plane
177
+ L: the eigenvalues
178
+
179
+ """
180
+ M = np.mean(data, 0)
181
+ cmtx = np.cov(np.transpose(data))
182
+ L, V = linalg.eig(cmtx)
183
+
184
+ # We assume the rotation is not too big and unit vectors are
185
+ # close to the original ux, uy and uz
186
+ idx = []
187
+ for i in range(0, V.shape[0]):
188
+ v = np.abs(V[:, i])
189
+ mx = np.amax(v)
190
+ ix = np.where(v == mx)[0][0]
191
+ idx.append(ix)
192
+
193
+ NV = np.zeros(V.shape)
194
+ NL = np.zeros(L.shape)
195
+ for i, j in enumerate(idx):
196
+ NV[:, j] = V[:, i]
197
+ NL[j] = L[i]
198
+
199
+ for i in range(0, 3):
200
+ if NV[i, i] < 0:
201
+ NV[:, i] = -NV[:, i]
202
+
203
+ avg = np.array([0., 0., 0.])
204
+ for i in range(0, 3):
205
+ if (1 << i & use_average):
206
+ avg[i] = M[i]
207
+
208
+ return project_to_plane(data, NV, avg), NV, M, NL
209
+
210
+
211
+ def remove_outliers_indx(data, cut=2.0, debug=False):
212
+ """Remove points far away form the rest.
213
+
214
+ Args:
215
+ ----
216
+ data : The data
217
+ cut: max allowed distance
218
+ debug: be verbose if True.
219
+
220
+ Returns
221
+ -------
222
+ index of valid pints in data array.
223
+
224
+ """
225
+ d = np.abs(data - np.median(data))
226
+ mdev = np.median(d)
227
+ s = d / (mdev if mdev else 1.)
228
+ indx = np.where(s < cut)[0]
229
+ return indx
230
+
231
+
232
+ def remove_outliers(data, cut, zlimit=1e25):
233
+ """Remove points far away form the rest.
234
+
235
+ Args:
236
+ ----
237
+ data : The data
238
+ cut: max allowed distance
239
+ zlimit: limit for Z Defaults to 1e25.
240
+
241
+ """
242
+ # move to the
243
+ val, V, M, L = fit_plane(data)
244
+ npts = val.shape[0]
245
+ ipoint = 0
246
+ vout = np.zeros([npts, 3])
247
+ rms = math.sqrt(L[0])
248
+ for i in range(0, npts):
249
+ sn = abs(val[i, 2]-M[2])/rms
250
+ if sn < cut:
251
+ vout[ipoint, :] = data[i, :]
252
+ ipoint = ipoint + 1
253
+
254
+ return vout[0:ipoint, :]
255
+
256
+
257
+ def __remove_outliers(data, cut=2.0, debug=False):
258
+ """Remove points far away form the rest.
259
+
260
+ Args:
261
+ ----
262
+ data : The data
263
+ cut: max allowed distance
264
+ debug: be bverbose if True
265
+
266
+ """
267
+ # move to the
268
+ val, V, M, L = fit_plane(data)
269
+ Z = val[:, 2]
270
+ d = np.abs(Z - np.median(Z))
271
+ mdev = np.median(d)
272
+ s = d / (mdev if mdev else 1.)
273
+ return data[s < cut, :]
274
+
275
+
276
+ class Point(object):
277
+ """Represents a point in a 2D space."""
278
+
279
+ def __init__(self, x=None, y=None, name=None):
280
+ """Initialization of a Point object.
281
+
282
+ Arguments are coordinates and optionally a name.
283
+ It can be initialized with individual values of
284
+ X and Y, with tuplles or arrays or with Point objects.
285
+
286
+ """
287
+ self.x = None
288
+ self.y = None
289
+
290
+ if name:
291
+ self.name = name
292
+ else:
293
+ self.name = "Point"
294
+
295
+ if x is not None:
296
+ if isinstance(x, Point):
297
+ self.x = x.x
298
+ self.y = x.y
299
+ else:
300
+ try:
301
+ self.x = float(x[0])
302
+ self.y = float(x[1])
303
+ except (TypeError, IndexError):
304
+ try:
305
+ self.x = float(x)
306
+ except ValueError:
307
+ self.x = None
308
+
309
+ try:
310
+ self.y = float(y)
311
+ except ValueError:
312
+ self.y = None
313
+
314
+ def __sub__(self, other):
315
+ """Defference between 2 Point objects."""
316
+ if isinstance(other, Point):
317
+ return Point(self.x - other.x, self.y - other.y)
318
+ else:
319
+ return NotImplemented
320
+
321
+ def __add__(self, other):
322
+ """Addtion of 2 Point objects."""
323
+ if isinstance(other, Point):
324
+ return Point(self.x + other.x, self.y + other.y)
325
+ else:
326
+ return NotImplemented
327
+
328
+ def __mul__(self, other):
329
+ """Scalar product of 2 Point objects."""
330
+ if isinstance(other, Point):
331
+ return self.x * other.x + self.y * other.y
332
+ elif isinstance(other, float) or isinstance(other, int):
333
+ return Point(self.x * other, self.y * other)
334
+ else:
335
+ return NotImplemented
336
+
337
+ def __rmul__(self, other):
338
+ """Multiplication by float or int."""
339
+ if isinstance(other, float) or isinstance(other, int):
340
+ return Point(self.x * other, self.y * other)
341
+ else:
342
+ return NotImplemented
343
+
344
+ def __eq__(self, other):
345
+ """Checks for equality."""
346
+ if isinstance(other, Point):
347
+ return self.x == other.x and self.y == other.y
348
+ else:
349
+ return NotImplemented
350
+
351
+ def __ne__(self, other):
352
+ """Checks for non equality."""
353
+ if isinstance(other, Point):
354
+ return self.x != other.x or self.y != other.y
355
+ else:
356
+ return NotImplemented
357
+
358
+ def __lt__(self, other):
359
+ """Lees than operator.
360
+
361
+ A point is smaller than other if its magnitude is smaller.
362
+ """
363
+ if isinstance(other, Point):
364
+ return self.mag() < other.mag()
365
+ else:
366
+ return NotImplemented
367
+
368
+ def __gt__(self, other):
369
+ """Greater than operator.
370
+
371
+ A point is greater than other if its magnitude is greater.
372
+ """
373
+ if isinstance(other, Point):
374
+ return self.mag() > other.mag()
375
+ else:
376
+ return NotImplemented
377
+
378
+ def __le__(self, other):
379
+ """Lees or equal.
380
+
381
+ Here equality refers to magnitude.
382
+ """
383
+ if isinstance(other, Point):
384
+ return self.mag() <= other.mag()
385
+ else:
386
+ return NotImplemented
387
+
388
+ def __ge__(self, other):
389
+ """Greater or equal.
390
+
391
+ Here equality refers to magnitude.
392
+ """
393
+ if isinstance(other, Point):
394
+ return self.mag() >= other.mag()
395
+ else:
396
+ return NotImplemented
397
+
398
+ def __neg__(self):
399
+ """Unary minus."""
400
+ return Point(-self.x, -self.y)
401
+
402
+ def mag(self):
403
+ """Return the length or magnitude."""
404
+ return math.sqrt(self.x*self.x + self.y*self.y)
405
+
406
+ def mag2(self):
407
+ """The squared of the magnitud."""
408
+ return self.x*self.x + self.y*self.y
409
+
410
+ def norm(self):
411
+ """Return unit vector."""
412
+ v = self.mag()
413
+ return Point(self.x/v, self.y/v)
414
+
415
+ def angle(self, P):
416
+ """Return angle with given point."""
417
+ return math.atan2(P.cross(self), P.dot(self))
418
+
419
+ def cw(self):
420
+ """Return a point like this rotated +90 degrees."""
421
+ return Point(-self.y, self.x)
422
+
423
+ def ccw(self):
424
+ """Return a point like this rotated -90 degress."""
425
+ return Point(self.y, -self.x)
426
+
427
+ def dot(self, a):
428
+ """Dot product with given vector."""
429
+ return self.x * a.x + self.y * a.y
430
+
431
+ def cross(self, b):
432
+ """Cross product with given vector."""
433
+ return self.dot(b.cw())
434
+
435
+ def phi(self):
436
+ """Phi or azimutal angle of vector."""
437
+ return math.atan2(self.y, self.x)
438
+
439
+ def valid(self):
440
+ """Tells if the point has valid values."""
441
+ if self.x is None or self.y is None:
442
+ return False
443
+ else:
444
+ return True
445
+
446
+ def distance(self, other):
447
+ """Distance to a Point or Line."""
448
+ if isinstance(other, Point):
449
+ dd = (self-other).mag()
450
+ return dd
451
+ elif isinstance(other, Line):
452
+ ff = math.sqrt(other.A()**2 + other.B()**2)
453
+ dd = other.A()*self[0] + other.B()*self[1] + other.C()
454
+ return dd/ff
455
+ else:
456
+ raise ValueError
457
+
458
+ def unit(self):
459
+ """Returns a unit vector from this one."""
460
+ return (1.0/self.mag())*self
461
+
462
+ def __getitem__(self, key):
463
+ """Implement the getitem interface."""
464
+ if key < 0 or key > 1:
465
+ raise IndexError
466
+ elif key == 0:
467
+ return self.x
468
+ else:
469
+ return self.y
470
+
471
+ def __setitem__(self, key, val):
472
+ """Implement the setitem interface."""
473
+ if key < 0 or key > 2:
474
+ raise IndexError
475
+ elif key == 0:
476
+ self.x = val
477
+ else:
478
+ self.y = val
479
+
480
+ def __len__(self):
481
+ """Return length."""
482
+ return 2
483
+
484
+ def __str__(self):
485
+ """String representation."""
486
+ return "%f,%f" % (self.x, self.y)
487
+
488
+ def __repr__(self):
489
+ """String representation."""
490
+ return "Point(%f, %f)" % (self.x, self.y)
491
+
492
+
493
+ def dot(a, b):
494
+ """Dot product."""
495
+ return a.x*b.x + a.y*b.y
496
+
497
+
498
+ def cross(a, b):
499
+ """Cross product."""
500
+ return dot(a, b.cw())
501
+
502
+
503
+ class Line(object):
504
+ """Represents a line.
505
+
506
+ We store the slope (m) and the intercept (b)
507
+
508
+ """
509
+
510
+ def __init__(self, m=None, n=None):
511
+ """Line creation.
512
+
513
+ We create the line in various forms:
514
+ 1) m and n are floats: the slope intercept form
515
+ 2) m is a float and n is a Point: line with that slope passing
516
+ through the given point
517
+ 3) m and n are points: line passing through 2 points
518
+ V = n - m
519
+ O = m
520
+ """
521
+ self.P1 = None
522
+ self.P2 = None
523
+ if isinstance(m, Point):
524
+ self.P1 = m
525
+ if isinstance(n, Point):
526
+ self.P2 = n
527
+ delta_x = (n.x - m.x)
528
+ delta_y = (n.y - m.y)
529
+ if delta_x == 0.0: # vertical line
530
+ self.O = Point(n.x, n.y)
531
+ self.V = Point(0.0, 1.0)
532
+ self.m = None
533
+ self.b = None
534
+ return
535
+
536
+ self.m = delta_y/delta_x
537
+ self.b = - self.m * n.x + n.y
538
+ self.O = Point(m.x, m.y)
539
+ self.delta = Point(delta_x, delta_y)
540
+ self.V = self.delta.norm()
541
+
542
+ else: # n has to be a number
543
+ self.m = n
544
+ self.b = - self.m * m.x + m.y
545
+ alpha = math.atan(n)
546
+ self.O = m
547
+ self.V = Point(math.cos(alpha), math.sin(alpha))
548
+ self.delta = self.V
549
+
550
+ else: # m has to be a number
551
+ if isinstance(n, Point):
552
+ self.m = m
553
+ self.b = - self.m * n.x + n.y
554
+ alpha = math.atan(m)
555
+ self.O = n
556
+ self.V = Point(math.cos(alpha), math.sin(alpha))
557
+ self.delta = self.V
558
+ else:
559
+ self.m = m
560
+ self.b = n
561
+ alpha = math.atan(m)
562
+ self.O = Point(0., n)
563
+ self.V = Point(math.cos(alpha), math.sin(alpha))
564
+ self.delta = self.V
565
+
566
+ def __str__(self):
567
+ """Stringrerpresentation."""
568
+ return "Line(%f, %f)" % (self.m, self.b)
569
+
570
+ def A(self):
571
+ """A coeff."""
572
+ return self.m
573
+
574
+ def B(self):
575
+ """B coeff."""
576
+ return -1
577
+
578
+ def C(self):
579
+ """C coeff."""
580
+ return self.b
581
+
582
+ def eval(self, x):
583
+ """Evaluates the line."""
584
+ if self.m:
585
+ return self.m * x + self.b
586
+
587
+ else:
588
+ return self.b
589
+
590
+ def param(self, t):
591
+ """Return point corresponding to given parameter."""
592
+ out = self.P1 + t*self.V
593
+ if not isinstance(out, Point):
594
+ out = Point(out)
595
+
596
+ return out
597
+
598
+ def __call__(self, x):
599
+ """Line evaluation.
600
+
601
+ Evaluates the line in parametric form x=0 gives P0, and x=1 gives P1
602
+ """
603
+ out = self.O + x*self.delta
604
+ return out
605
+
606
+ def line_perpendicular_at_point(self, P):
607
+ """Return the line perpendicular passing by point."""
608
+ L = Line(-1.0/self.m, P)
609
+ return L
610
+
611
+ def line_parallel_at_distance(self, d):
612
+ """Returns the line parallel to this one which is at a distance d."""
613
+ if not self.m:
614
+ P = Point(self.O.x + d, 0)
615
+ V = Point(self.O.x + d, 1)
616
+ return Line(P, V)
617
+
618
+ else:
619
+ new_m = self.m
620
+ new_b = self.b + d * math.sqrt(1 + self.m*self.m)
621
+ return Line(new_m, new_b)
622
+
623
+ def line_at_angle(self, angle, center=None):
624
+ """Returns a line which forms an angle w.r.t center."""
625
+ if not center:
626
+ center = self.O
627
+ else:
628
+ res = center.y - (self.m * center.x + self.b)
629
+ if abs(res) > 1e-10:
630
+ raise Exception("Line.line_at_angle: center does not belong to line")
631
+
632
+ ca = math.cos(angle)
633
+ sa = math.sin(angle)
634
+ bx = self.V.x * ca - self.V.y * sa
635
+ by2 = 1.0-bx*bx
636
+ if by2 < 0:
637
+ bx = self.V.x * ca + self.V.y * sa
638
+ by2 = 1.0 - bx*bx
639
+
640
+ by = math.sqrt(by2)
641
+ b = Point(bx, by)
642
+ b1 = center + b
643
+ return Line(center, b1)
644
+
645
+ def line_angle(self, P=None):
646
+ """Returns the angle with a given direction."""
647
+ if not P:
648
+ P = Point(1.0, 0.0)
649
+
650
+ return P.angle(self.V)
651
+
652
+ def angle(self, other):
653
+ """Angle between 2 lines."""
654
+ angle = math.atan((other.m-self.m)/(1+self.m*other.m))
655
+ return angle
656
+
657
+ def cross_point(self, other):
658
+ """Intersection point."""
659
+ if self.m is None:
660
+ # vertical line
661
+ if other.m is None:
662
+ raise Exception("Parallel Lines")
663
+ else:
664
+ x = self.O.x
665
+ y = other.m*x + other.b
666
+ return Point(x, y)
667
+
668
+ if other.m is None:
669
+ return other.cross_point(self)
670
+
671
+ D = other.m - self.m
672
+ if D == 0.:
673
+ raise Exception("Parallel Lines")
674
+
675
+ x = (self.b - other.b)/D
676
+ y = (-self.m*other.b + other.m*self.b)/D
677
+
678
+ return Point(x, y)
679
+
680
+ def is_parallel(self, other):
681
+ """True if given line is parallel."""
682
+ return self.m == other.m
683
+
684
+ def is_perpendicular(self, other):
685
+ """True if given line is perpendicular."""
686
+ return self.m*other.m == -1.0
687
+
688
+ def cross_circle(self, C, R, tmin, tmax):
689
+ """Computes the crossing point with a circle.
690
+
691
+ C: the center of the circle
692
+ R: the radius
693
+ tmin, tmax : angles limiting the crossing point or
694
+ tmin = function to accept a point
695
+ tmax ignored
696
+ """
697
+ a = self.m
698
+ b = self.b
699
+ c = C.x
700
+ d = C.y
701
+ func = None
702
+
703
+ if isfunction(tmin):
704
+ func = tmin
705
+
706
+ else:
707
+ if tmin < 0:
708
+ tmin += 2*math.pi
709
+
710
+ if tmax < 0:
711
+ tmax += 2*math.pi
712
+
713
+ if tmin > tmax:
714
+ tmp = tmax
715
+ tmax = tmin
716
+ tmin = tmp
717
+
718
+ xx = a*a+1.0
719
+ det = xx*R*R - d*d + 2.0*d*(a*c+b) - a*a*c*c - 2.0*a*b*c-b*b
720
+ if det < 0:
721
+ return None
722
+
723
+ det = math.sqrt(det)
724
+ yy = a*(d-b)+c
725
+ x1 = (det-yy)/xx
726
+ x2 = (det+yy)/xx
727
+
728
+ p1 = Point(x1, a*x1+b)
729
+ p2 = Point(x2, a*x2+b)
730
+
731
+ if func:
732
+ if func(p1, R, C):
733
+ return p1
734
+ elif func(p2, R, C):
735
+ return p2
736
+ else:
737
+ return None
738
+
739
+ else:
740
+ V = (p1-C)
741
+ dd = V.phi()
742
+ if dd < 0.0:
743
+ dd += 2*math.pi
744
+
745
+ if tmin < dd and dd < tmax:
746
+ return p1
747
+
748
+ V = (p2-C)
749
+ dd = V.phi()
750
+ if dd < 0.0:
751
+ dd += 2*math.pi
752
+
753
+ if tmin < dd and dd < tmax:
754
+ return p2
755
+
756
+ return None