ElectroMagneticPython 2.1.4__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.
EMpy/devices.py ADDED
@@ -0,0 +1,1414 @@
1
+ # pylint: disable=C0302,W0622,W1001,R0902,R0903,W0201,R0913,R0914,C0325,W0221
2
+
3
+ """Set of objects and functions to describe the behaviour of some
4
+ widely known devices and build new ones.
5
+
6
+ A set of widely known devices is described, by its transfer function,
7
+ transfer matrix and chain matrix:
8
+
9
+ - Coupler.
10
+ - Line.
11
+ - Mach-Zehnder Interferometer.
12
+ - All-Pass Ring Resonator.
13
+ - Single Ring Resonator.
14
+ - N-Rings Resonator.
15
+ - Tunable Triple-Coupled Ring Resonator.
16
+ - Tunable Coupled Ring Triple-Coupled Ring Resonator.
17
+ - Tunable Coupled Triple-Coupled Ring Resonator.
18
+ - Straight Waveguide.
19
+ - Etalon.
20
+
21
+ By combining the transfer matrices and chain matrices of known
22
+ devices, new ones can be studied.
23
+
24
+ @see: U{Schwelb, "Transmission, Group Delay, and Dispersion in
25
+ Single-Ring Optical Resonators and Add/Drop Filters - A Tutorial
26
+ Overview", JLT 22(5), p. 1380, 2004
27
+ <http://jlt.osa.org/abstract.cfm?id=80103>}
28
+ @see: U{Barbarossa, "Theoretical Analysis of Triple-Coupler Ring-Based
29
+ Optical Guided-Wave Resonator", JLT 13(2), p. 148, 1995
30
+ <http://ieeexplore.ieee.org/xpl/freeabs_all.jsp?tp=&arnumber=365200&
31
+ isnumber=8367>}
32
+
33
+ """
34
+ __author__ = "Lorenzo Bolla"
35
+
36
+ import numpy
37
+ import EMpy.utils
38
+ from functools import reduce
39
+
40
+
41
+ class DeviceMatrix:
42
+
43
+ """Device Matrix.
44
+
45
+ Base class for TransferMatrix and ChainMatrix.
46
+
47
+ Notes
48
+ =====
49
+
50
+ - Data is stored as numpy.matrix.
51
+ - It is a virtual class.
52
+
53
+ @ivar data: Device matrix data, automatically converted into a
54
+ numpy.matrix on assignment.
55
+
56
+ """
57
+
58
+ def __init__(self, data):
59
+ self.data = data
60
+
61
+ def getdata(self):
62
+ return self.__data
63
+
64
+ def setdata(self, value):
65
+ self.__data = numpy.asmatrix(value)
66
+
67
+ __data = None
68
+ data = property(fget=getdata, fset=setdata, doc="data")
69
+
70
+ def compose(self, A):
71
+ """Compose two device matrices.
72
+
73
+ @raise NotImplementedError: always, because DeviceMatrix is a
74
+ virtual class.
75
+
76
+ """
77
+ raise NotImplementedError("DeviceMatrix is virtual class.")
78
+
79
+ def __str__(self):
80
+ return self.data.__str__()
81
+
82
+ __repr__ = __str__
83
+
84
+
85
+ class TransferMatrix(DeviceMatrix):
86
+
87
+ """Transfer Matrix.
88
+
89
+ Notation::
90
+
91
+ b1 = h11 h12 * a1 --> b12 = H a12
92
+ b2 h21 h22 a2
93
+
94
+ """
95
+
96
+ def __init__(self, data):
97
+ DeviceMatrix.__init__(self, data)
98
+
99
+ def to_transfer(self):
100
+ """Convert to transfer matrix (nothing to do).
101
+
102
+ @rtype: L{TransferMatrix}
103
+
104
+ """
105
+
106
+ return self
107
+
108
+ def to_chain(self):
109
+ """Convert to chain matrix.
110
+
111
+ Notation::
112
+
113
+ b1 = h11 a1 + h12 a2
114
+ --> a1 = (b2 - h22 a2) / h21 = 1 / h21 b2 - h22 / h21 a2
115
+ b2 = h21 a1 + h22 a2
116
+ --> b1 = h11 / h21 * (b2 - h22 a2) + h12 a2
117
+ = h11 / h21 b2 + (h12 * h21 - h11 * h22) / h12 a2
118
+
119
+ @rtype: L{ChainMatrix}
120
+
121
+ """
122
+ return ChainMatrix(
123
+ [
124
+ [1.0 / self.data[1, 0], -self.data[1, 1] / self.data[1, 0]],
125
+ [
126
+ self.data[0, 0] / self.data[1, 0],
127
+ (
128
+ self.data[0, 1] * self.data[1, 0]
129
+ - self.data[0, 0] * self.data[1, 1]
130
+ )
131
+ / self.data[1, 0],
132
+ ],
133
+ ]
134
+ )
135
+
136
+ def compose(self, M):
137
+ """Compose the transfer matrix with another device matrix.
138
+
139
+ Notation::
140
+
141
+ b12 = H1 a12 and d12 = H2 c12, but b12 == c12,
142
+
143
+ therefore::
144
+
145
+ d12 = H2*H1 a12.
146
+
147
+ Notes
148
+ =====
149
+
150
+ First, convert the second matrix to transfer.
151
+
152
+ @param M: DeviceMatrix (will be converted to TransferMatrix).
153
+ @rtype: L{TransferMatrix}
154
+
155
+ """
156
+
157
+ return TransferMatrix(M.to_transfer().data * self.data)
158
+
159
+
160
+ class ChainMatrix(DeviceMatrix):
161
+
162
+ """Chain Matrix.
163
+
164
+ Notation::
165
+
166
+ a1 = g11 g12 * b2 --> ab1 = G ba2
167
+ b1 g21 g22 a2
168
+
169
+ """
170
+
171
+ def __init__(self, *args, **kwargs):
172
+ DeviceMatrix.__init__(self, *args, **kwargs)
173
+
174
+ def to_transfer(self):
175
+ """Convert to transfer matrix.
176
+
177
+ Notation::
178
+
179
+ a1 = g11 b2 + g12 a2
180
+ b2 = 1 / g11 a1 - g12 / g11 a2 = 1 / g11 a1 - g12 / g11 a2
181
+ b1 = g21 b2 + g22 a2
182
+ b1 = g21 / g11 a1 + (g22 - g12 * g21 / g11) a2
183
+ = g21 / g11 a1 + (g11 g22 - g12 g21) / g11 a2
184
+
185
+ @rtype: L{TransferMatrix}
186
+
187
+ """
188
+ return TransferMatrix(
189
+ [
190
+ [
191
+ self.data[1, 0] / self.data[0, 0],
192
+ (
193
+ self.data[0, 0] * self.data[1, 1]
194
+ - self.data[0, 1] * self.data[1, 0]
195
+ )
196
+ / self.data[0, 0],
197
+ ],
198
+ [1.0 / self.data[0, 0], -self.data[0, 1] / self.data[0, 0]],
199
+ ]
200
+ )
201
+
202
+ def to_chain(self):
203
+ """Convert to chain matrix (nothing to do).
204
+
205
+ @rtype: L{ChainMatrix}
206
+
207
+ """
208
+
209
+ return self
210
+
211
+ def compose(self, M):
212
+ """Compose the chain matrix with another device matrix.
213
+
214
+ Notation::
215
+
216
+ ab1 = G1 ba2 and ab3 = G2 ba4, but ba2 == ab3,
217
+
218
+ therefore::
219
+
220
+ ab1 = G1*G2 ba4.
221
+
222
+ Notes
223
+ =====
224
+
225
+ First, convert the second matrix to chain.
226
+
227
+ @param M: L{DeviceMatrix} (will be converted to L{ChainMatrix}).
228
+ @rtype: L{ChainMatrix}
229
+
230
+ """
231
+
232
+ return ChainMatrix(self.data * M.to_chain().data)
233
+
234
+
235
+ def composeTM(A, B):
236
+ """Compose two transfer matrices.
237
+
238
+ Notes
239
+ =====
240
+
241
+ First convert them to TM, to be sure.
242
+
243
+ @param A: First L{TransferMatrix}.
244
+ @param B: Second L{TransferMatrix}.
245
+ @rtype: L{TransferMatrix}
246
+
247
+ """
248
+
249
+ return A.to_transfer().compose(B)
250
+
251
+
252
+ def composeCM(A, B):
253
+ """Compose two chain matrices.
254
+
255
+ Notes
256
+ =====
257
+
258
+ First convert them to CM, to be sure.
259
+
260
+ @param A: First L{ChainMatrix}.
261
+ @param B: Second L{ChainMatrix}.
262
+ @rtype: L{ChainMatrix}
263
+
264
+ """
265
+
266
+ return A.to_chain().compose(B)
267
+
268
+
269
+ def composeTMlist(TMlist):
270
+ """Compose n transfer matrices in a list.
271
+
272
+ @param TMlist: List of L{TransferMatrix}.
273
+ @rtype: L{TransferMatrix}
274
+
275
+ """
276
+
277
+ return reduce(composeTM, TMlist)
278
+
279
+
280
+ def composeCMlist(CMlist):
281
+ """Compose n chain matrices in a list.
282
+
283
+ @param CMlist: List of L{ChainMatrix}.
284
+ @rtype: L{ChainMatrix}
285
+
286
+ """
287
+
288
+ return reduce(composeCM, CMlist)
289
+
290
+
291
+ class Device:
292
+
293
+ """Generic device.
294
+
295
+ Notes
296
+ =====
297
+
298
+ Parent virtual class for all the other devices.
299
+
300
+ """
301
+
302
+ def sanity_check(self):
303
+ raise NotImplementedError()
304
+
305
+ def solve(self, *args, **kwargs):
306
+ raise NotImplementedError()
307
+
308
+
309
+ class Coupler(Device):
310
+
311
+ """Coupler.
312
+
313
+ Examples
314
+ ========
315
+
316
+ Here is how to compute the THRU and DROP of a 3dB coupler, with
317
+ wavelength-dependent losses:
318
+
319
+ >>> wls = numpy.linspace(1.52e-6, 1.57e-6, 1000)
320
+ >>> k = .5 ** .5
321
+ >>> q = numpy.linspace(.9, .95, 1000)
322
+ >>> coupler = EMpy.devices.Coupler(wls, k, q).solve()
323
+
324
+ @ivar wl: Wavelength.
325
+ @type wl: numpy.ndarray
326
+ @ivar K: Field coupling coefficient.
327
+ @type K: numpy.ndarray
328
+ @ivar q: Field coupling loss.
329
+ @type q: numpy.ndarray
330
+ @ivar THRU: also called "bar".
331
+ @type THRU: numpy.ndarray
332
+ @ivar DROP: also called "cross".
333
+ @type DROP: numpy.ndarray
334
+
335
+ """
336
+
337
+ def __init__(self, wl, K, q=1.0):
338
+ """Set the parameters of the coupler and check them."""
339
+
340
+ self.wl = wl
341
+ one = numpy.ones_like(wl)
342
+ # make K,q of the same shape as wl (scalar or ndarray)
343
+ self.K = K * one
344
+ self.q = q * one
345
+ self.sanity_check()
346
+
347
+ def sanity_check(self):
348
+ """Check for good input.
349
+
350
+ @raise ValueError: if input numpy.ndarrays have wrong shape.
351
+
352
+ """
353
+
354
+ if (numpy.isscalar(self.q) or numpy.isscalar(self.K)) and not numpy.isscalar(
355
+ self.wl
356
+ ):
357
+ raise ValueError("wl is not a scalar but K and q are.")
358
+ if (
359
+ not numpy.isscalar(self.q) or not numpy.isscalar(self.K)
360
+ ) and numpy.isscalar(self.wl):
361
+ raise ValueError("wl is a scalar but K and q are not.")
362
+ if not (
363
+ numpy.asarray(self.wl).shape
364
+ == numpy.asarray(self.K).shape
365
+ == numpy.asarray(self.q).shape
366
+ ):
367
+ raise ValueError("wl, K and q must have the same shape")
368
+
369
+ def solve(self):
370
+ """Compute the THRU and DROP."""
371
+
372
+ Kbar = numpy.sqrt(1.0 - self.K**2)
373
+ self.THRU = self.q * Kbar
374
+ self.DROP = self.q * 1j * self.K
375
+
376
+ return self
377
+
378
+ def TM(self, wl=None):
379
+ """Return the L{TransferMatrix} of the L{Coupler}, for a given wl.
380
+
381
+ Notation::
382
+
383
+ TM = q * [ t jk] , with t = (1 - k**2)**.5
384
+ [jk t]
385
+
386
+ @param wl: Wavelength at which computing the TM. If None, use
387
+ the only wl provided at construction time.
388
+ @type wl: scalar
389
+
390
+ @return: L{TransferMatrix}.
391
+ @rtype: L{TransferMatrix}
392
+
393
+ @raise ValueError: (if wl=None and self.wl is not scalar) or
394
+ wl is not scalar.
395
+
396
+ """
397
+
398
+ if wl is None:
399
+ if not numpy.isscalar(self.wl):
400
+ raise ValueError("which wl?")
401
+ K = self.K
402
+ q = self.q
403
+ elif numpy.isscalar(wl):
404
+ K = numpy.interp(
405
+ numpy.atleast_1d(wl),
406
+ numpy.atleast_1d(self.wl),
407
+ numpy.atleast_1d(self.K),
408
+ ).item()
409
+ q = numpy.interp(
410
+ numpy.atleast_1d(wl),
411
+ numpy.atleast_1d(self.wl),
412
+ numpy.atleast_1d(self.q),
413
+ ).item()
414
+ else:
415
+ raise ValueError("wl must be scalar")
416
+
417
+ Kbar = numpy.sqrt(1 - K**2)
418
+ return TransferMatrix([[q * Kbar, q * 1j * K], [q * 1j * K, q * Kbar]])
419
+
420
+ def CM(self, wl=None):
421
+ """Return the L{ChainMatrix} of the coupler.
422
+
423
+ Notes
424
+ =====
425
+
426
+ First build its L{TransferMatrix}, then convert it to L{ChainMatrix}.
427
+
428
+ @param wl: Wavelength at which computing the L{ChainMatrix}.
429
+
430
+ @return: L{ChainMatrix}.
431
+ @rtype: L{ChainMatrix}
432
+
433
+ """
434
+
435
+ return self.TM(wl).to_chain()
436
+
437
+
438
+ class Line(Device):
439
+
440
+ """Line.
441
+
442
+ Notes
443
+ =====
444
+
445
+ The Line device is a four ports device, hence is made of two
446
+ two-ports lines.
447
+
448
+ Examples
449
+ ========
450
+
451
+ Here is how to create a L{Line}, where the first line is a 2.pi.R1
452
+ long L{SWG} and the second has zero length.
453
+
454
+ >>> wls = numpy.linspace(1.52e-6, 1.57e-6, 1000)
455
+ >>> SWG = EMpy.devices.SWG(400, 220, 125).solve(wls)
456
+ >>> R1 = 5.664e-6
457
+ >>> line = EMpy.devices.Line(wls, SWG.neff, 2 * numpy.pi * R1, SWG.neff, 0)
458
+
459
+ @ivar wl: Wavelength.
460
+ @type wl: numpy.ndarray
461
+ @ivar neff1: Effective index of the first line.
462
+ @type neff1: numpy.ndarray
463
+ @ivar l1: Length of the first line.
464
+ @type l1: scalar
465
+ @ivar neff2: Effective index of the second line.
466
+ @type neff2: numpy.ndarray
467
+ @ivar l2: Length of the second line.
468
+ @type l2: scalar
469
+ @ivar THRU: output of the first line.
470
+ @type THRU: numpy.ndarray
471
+ @ivar DROP: output of the second line.
472
+ @type DROP: numpy.ndarray
473
+
474
+ """
475
+
476
+ def __init__(self, wl, neff1, l1, neff2, l2):
477
+ """Set the parameters of the line and check them."""
478
+
479
+ self.wl = wl
480
+ one = numpy.ones_like(wl)
481
+ self.neff1 = neff1 * one
482
+ self.l1 = l1
483
+ self.neff2 = neff2 * one
484
+ self.l2 = l2
485
+ self.sanity_check()
486
+
487
+ def sanity_check(self):
488
+ """Check for good input.
489
+
490
+ @raise ValueError: if l1 and l2 are not scalar.
491
+
492
+ """
493
+
494
+ if not (numpy.isscalar(self.l1) and numpy.isscalar(self.l2)):
495
+ raise ValueError("lengths must be scalars")
496
+
497
+ def solve(self):
498
+ """Compute the THRU and DROP."""
499
+
500
+ beta1 = 2 * numpy.pi * self.neff1 / self.wl
501
+ beta2 = 2 * numpy.pi * self.neff2 / self.wl
502
+ self.THRU = numpy.exp(-1j * beta1 * self.l1)
503
+ self.DROP = numpy.exp(-1j * beta2 * self.l2)
504
+
505
+ return self
506
+
507
+ def TM(self, wl=None):
508
+ """Return the L{TransferMatrix} of the L{Line}, for a given wl.
509
+
510
+ Notation::
511
+
512
+ TM = [exp(-1j * beta1 * l1) 0 ]
513
+ [0 exp(-1j * beta2 * l2)]
514
+
515
+ @param wl: Wavelength at which computing the TM. If None, use
516
+ the only wl provided at construction time.
517
+ @type wl: scalar
518
+
519
+ @return: L{TransferMatrix}.
520
+ @rtype: L{TransferMatrix}
521
+
522
+ @raise ValueError: (if wl=None and self.wl is not scalar) or
523
+ wl is not scalar.
524
+
525
+ """
526
+
527
+ if wl is None:
528
+ if not numpy.isscalar(self.wl):
529
+ raise ValueError("which wl?")
530
+ neff1 = self.neff1
531
+ neff2 = self.neff2
532
+ wl = self.wl
533
+ elif numpy.isscalar(wl):
534
+ neff1 = numpy.interp(
535
+ numpy.atleast_1d(wl),
536
+ numpy.atleast_1d(self.wl),
537
+ numpy.atleast_1d(self.neff1),
538
+ ).item()
539
+ neff2 = numpy.interp(
540
+ numpy.atleast_1d(wl),
541
+ numpy.atleast_1d(self.wl),
542
+ numpy.atleast_1d(self.neff2),
543
+ ).item()
544
+ else:
545
+ raise ValueError("wl must be scalar")
546
+
547
+ beta1 = 2 * numpy.pi * neff1 / wl
548
+ beta2 = 2 * numpy.pi * neff2 / wl
549
+ return TransferMatrix(
550
+ [
551
+ [numpy.exp(-1j * beta1 * self.l1), 0],
552
+ [0, numpy.exp(-1j * beta2 * self.l2)],
553
+ ]
554
+ )
555
+
556
+ def CM(self, wl=None):
557
+ """Return the L{ChainMatrix} of the line.
558
+
559
+ Notes
560
+ =====
561
+
562
+ First build its L{TransferMatrix}, then convert it to L{ChainMatrix}.
563
+
564
+ @return: L{ChainMatrix}.
565
+ @rtype: L{ChainMatrix}
566
+
567
+ """
568
+
569
+ return self.TM(wl).to_chain()
570
+
571
+
572
+ class MZ(Device):
573
+
574
+ """Mach-Zehnder Interferometer (MZI).
575
+
576
+ A MZI is made of two L{Coupler}s connected by a L{Line}.
577
+ A different phase shift on either arms of the L{Line} determines
578
+ the amount of power in bar (THRU) or cross (DROP).
579
+
580
+ Examples
581
+ ========
582
+
583
+ Here is how to build a MZI:
584
+
585
+ >>> wls = numpy.linspace(1.52e-6, 1.57e-6, 1000)
586
+ >>> SWG = EMpy.devices.SWG(400, 220, 125).solve(wls)
587
+ >>> R1 = 5.664e-6
588
+ >>> Kbus2ringeq = .5
589
+ >>> coupler = EMpy.devices.Coupler(wls, Kbus2ringeq**.5)
590
+ >>> line = EMpy.devices.Line(wls, SWG.neff, 2 * numpy.pi * R1, SWG.neff, 0)
591
+ >>> mz = EMpy.devices.MZ(coupler, line, coupler).solve()
592
+
593
+ @ivar coupler1: First L{Coupler}
594
+ @type coupler1: L{Coupler}
595
+ @ivar line: L{Line} connecting the two L{Coupler}s.
596
+ @type line: L{Line}
597
+ @ivar coupler2: Second L{Coupler}
598
+ @type coupler2: L{Coupler}
599
+
600
+ """
601
+
602
+ def __init__(self, coupler1, line, coupler2):
603
+ """Set the parameters of the MZ and check them."""
604
+
605
+ self.wl = coupler1.wl
606
+ self.coupler1 = coupler1
607
+ self.coupler2 = coupler2
608
+ self.line = line
609
+ self.sanity_check()
610
+
611
+ def sanity_check(self):
612
+ """Check for good input.
613
+
614
+ @raise ValueError: if coupler1 or coupler2 or line have incompatible wl.
615
+
616
+ """
617
+
618
+ self.coupler1.sanity_check()
619
+ self.coupler2.sanity_check()
620
+ self.line.sanity_check()
621
+ if not (
622
+ numpy.alltrue(self.coupler1.wl == self.line.wl)
623
+ and numpy.alltrue(self.coupler1.wl == self.coupler2.wl)
624
+ ):
625
+ raise ValueError("incompatible wl")
626
+
627
+ def solve(self):
628
+ """Compute the THRU and DROP."""
629
+
630
+ K1 = self.coupler1.K
631
+ K2 = self.coupler2.K
632
+ K1bar = numpy.sqrt(1.0 - K1**2)
633
+ K2bar = numpy.sqrt(1.0 - K2**2)
634
+ q1 = self.coupler1.q
635
+ q2 = self.coupler2.q
636
+
637
+ beta1 = 2.0 * numpy.pi * self.line.neff1 / self.wl
638
+ beta2 = 2.0 * numpy.pi * self.line.neff2 / self.wl
639
+
640
+ dephasing1 = numpy.exp(-1j * beta1 * self.line.l1)
641
+ dephasing2 = numpy.exp(-1j * beta2 * self.line.l2)
642
+
643
+ self.THRU = q1 * q2 * (K1bar * K2bar * dephasing1 - K1 * K2 * dephasing2)
644
+ self.DROP = q1 * q2 * 1j * (K1 * K2bar * dephasing2 + K2 * K1bar * dephasing1)
645
+
646
+ return self
647
+
648
+ def TM(self, wl=None):
649
+ """Return the transfer matrix of the MZ.
650
+
651
+ Notes
652
+ =====
653
+
654
+ Compose the TM of the single devices which compose the MZ.
655
+
656
+ @return: L{TransferMatrix}.
657
+ @rtype: L{TransferMatrix}
658
+
659
+ """
660
+
661
+ return composeTMlist(
662
+ [self.coupler1.TM(wl), self.line.TM(wl), self.coupler2.TM(wl)]
663
+ )
664
+
665
+ def CM(self, wl=None):
666
+ """Return the chain matrix of the MZ.
667
+
668
+ Notes
669
+ =====
670
+
671
+ First build its L{TransferMatrix}, then convert it to L{ChainMatrix}.
672
+
673
+ @return: L{ChainMatrix}.
674
+ @rtype: L{ChainMatrix}
675
+
676
+ """
677
+
678
+ return self.TM(wl).to_chain()
679
+
680
+
681
+ class APRR(Device):
682
+
683
+ """All-Pass Ring Resonator.
684
+
685
+ An APRR is made of a L{Coupler} with a feedback line (the ring).
686
+
687
+ Examples
688
+ ========
689
+
690
+ >>> wls = numpy.linspace(1.5e-6, 1.6e-6, 1000)
691
+ >>> K = numpy.sqrt(0.08)
692
+ >>> q = 1.
693
+ >>> l = 2*numpy.pi*5e-6
694
+ >>> SWG = EMpy.devices.SWG(488, 220, 25).solve(wls)
695
+ >>> coupler = EMpy.devices.Coupler(wls, K, q)
696
+ >>> APRR = EMpy.devices.APRR(coupler, SWG.neff, l).solve()
697
+
698
+ @ivar coupler: L{Coupler}.
699
+ @type coupler: L{Coupler}
700
+ @ivar neff: Effective index of the feedback line.
701
+ @type neff: numpy.ndarray
702
+ @ivar l: Length of the feedback line (perimeter of the ring).
703
+ @type l: scalar
704
+ @ivar THRU: THRU of the all-pass.
705
+ @type THRU: numpy.ndarray
706
+
707
+ """
708
+
709
+ def __init__(self, coupler, neff, l):
710
+ """Set the parameters of the all-pass and check them."""
711
+
712
+ self.wl = coupler.wl
713
+ self.coupler = coupler
714
+ self.neff = neff
715
+ self.l = l
716
+ self.sanity_check()
717
+
718
+ def sanity_check(self):
719
+ """Check for good input.
720
+
721
+ @raise ValueError: if l is not a scalar or wl and neff have
722
+ different shapes.
723
+
724
+ """
725
+
726
+ self.coupler.sanity_check()
727
+ if not numpy.isscalar(self.wl):
728
+ if not numpy.isscalar(self.neff):
729
+ if self.wl.shape != self.neff.shape:
730
+ raise ValueError("wl and neff must have the same shape.")
731
+ if not numpy.isscalar(self.l):
732
+ raise ValueError("l must be scalar.")
733
+
734
+ def solve(self):
735
+ """Compute the THRU."""
736
+
737
+ K = self.coupler.K
738
+ q = self.coupler.q
739
+
740
+ Kbar = numpy.sqrt(1.0 - K**2)
741
+ beta = 2.0 * numpy.pi * self.neff / self.wl
742
+ t = numpy.exp(-1j * beta * self.l)
743
+ self.THRU = q * (Kbar - q * t) / (1.0 - q * Kbar * t)
744
+
745
+ return self
746
+
747
+
748
+ class SRR(Device):
749
+
750
+ """Single Ring Resonator.
751
+
752
+ A SRR is made of the parallel of a L{Coupler} a feedback line (the
753
+ ring) and another L{Coupler}.
754
+
755
+ Examples
756
+ ========
757
+
758
+ >>> wls = numpy.linspace(1.5e-6, 1.6e-6, 1000)
759
+ >>> coupler1 = EMpy.devices.Coupler(wls, 0.08**.5)
760
+ >>> coupler2 = EMpy.devices.Coupler(wls, 0.08**.5)
761
+ >>> R = 5e-6
762
+ >>> l1 = numpy.pi * R
763
+ >>> l2 = numpy.pi * R
764
+ >>> SWG = EMpy.devices.SWG(488, 220, 25).solve(wls)
765
+ >>> SRR = EMpy.devices.SRR(coupler1, coupler2, SWG.neff, l1, l2).solve()
766
+
767
+ @ivar coupler1: Bus-to-Ring L{Coupler}.
768
+ @type coupler1: L{Coupler}
769
+ @ivar coupler2: Ring-to-Bus L{Coupler}.
770
+ @type coupler2: L{Coupler}
771
+ @ivar neff: Effective index of the feedback line.
772
+ @type neff: numpy.ndarray
773
+ @ivar l1: Length of the feedback line (perimeter of the ring) from
774
+ coupler1 to coupler2.
775
+ @type l1: scalar
776
+ @ivar l2: Length of the feedback line (perimeter of the ring) from
777
+ coupler2 to coupler1.
778
+ @type l2: scalar
779
+ @ivar THRU: THRU of the filter.
780
+ @type THRU: numpy.ndarray
781
+ @ivar DROP: DROP of the filter.
782
+ @type DROP: numpy.ndarray
783
+
784
+ """
785
+
786
+ def __init__(self, coupler1, coupler2, neff, l1, l2):
787
+ """Set the parameters of the device and check them."""
788
+
789
+ self.wl = coupler1.wl
790
+ self.coupler1 = coupler1
791
+ self.coupler2 = coupler2
792
+ self.neff = neff
793
+ self.l1 = l1
794
+ self.l2 = l2
795
+ self.line1 = Line(self.wl, neff, 0.0, neff, l1)
796
+ self.line2 = Line(self.wl, neff, 0.0, neff, l2)
797
+ self.sanity_check()
798
+
799
+ def sanity_check(self):
800
+ """Check for good input.
801
+
802
+ @raise ValueError: if wl are incompatible.
803
+
804
+ """
805
+
806
+ self.coupler1.sanity_check()
807
+ self.coupler2.sanity_check()
808
+ self.line1.sanity_check()
809
+ self.line2.sanity_check()
810
+ if not (
811
+ numpy.alltrue(self.coupler1.wl == self.coupler2.wl)
812
+ and numpy.alltrue(self.coupler1.wl == self.line1.wl)
813
+ and numpy.alltrue(self.coupler1.wl == self.line2.wl)
814
+ ):
815
+ raise ValueError("incompatible wl")
816
+
817
+ def solve(self):
818
+ """Compute the THRU and the DROP."""
819
+
820
+ K1 = self.coupler1.K
821
+ K2 = self.coupler2.K
822
+ K1bar = numpy.sqrt(1.0 - K1**2)
823
+ K2bar = numpy.sqrt(1.0 - K2**2)
824
+ q1 = self.coupler1.q
825
+ q2 = self.coupler2.q
826
+ l1 = self.line1.l2
827
+ l2 = self.line2.l2
828
+
829
+ beta = 2.0 * numpy.pi * self.neff / self.wl
830
+
831
+ t1 = numpy.exp(-1j * beta * l1)
832
+ t2 = numpy.exp(-1j * beta * l2)
833
+
834
+ denom = 1.0 - q1 * q2 * K1bar * K2bar * t1 * t2
835
+ self.DROP = -q1 * q2 * K1 * K2 * t2 / denom
836
+ self.THRU = q1 * (K1bar - q1 * q2 * K2bar * t1 * t2) / denom
837
+
838
+ return self
839
+
840
+ def TM(self, wl=None):
841
+ """Return the L{TransferMatrix} of the SRR.
842
+
843
+ Notes
844
+ =====
845
+
846
+ First build its L{ChainMatrix}, then convert it to L{TransferMatrix}.
847
+
848
+ @return: L{TransferMatrix}.
849
+ @rtype: L{TransferMatrix}
850
+
851
+ """
852
+
853
+ return self.CM(wl).to_transfer()
854
+
855
+ def CM(self, wl=None):
856
+ """Return the L{ChainMatrix} of the SRR.
857
+
858
+ Notes
859
+ =====
860
+
861
+ Compose the CM of the single devices which compose the SRR.
862
+
863
+ @return: L{ChainMatrix}.
864
+ @rtype: L{ChainMatrix}
865
+
866
+ """
867
+
868
+ return composeCM(
869
+ composeTMlist([self.line1.TM(wl), self.coupler1.TM(wl), self.line2.TM(wl)]),
870
+ self.coupler2.CM(wl),
871
+ )
872
+
873
+
874
+ class NRR(Device):
875
+
876
+ """N-Rings Resonator.
877
+
878
+ A NRR is made of the parallel of two lines connected by N ring resonators.
879
+
880
+ Examples
881
+ ========
882
+
883
+ >>> wls = numpy.linspace(1.5e-6, 1.6e-6, 1000)
884
+ >>> from EMpy.devices import Coupler
885
+ >>> couplers = [Coupler(wls, 0.08**.5), Coupler(wls, 0.003**.5),
886
+ Coupler(wls, 0.08**.5)]
887
+ >>> q = [1., 1., 1.]
888
+ >>> R = 5e-6
889
+ >>> l1s = [numpy.pi * R, numpy.pi * R]
890
+ >>> l2s = [numpy.pi * R, numpy.pi * R]
891
+ >>> SWG = EMpy.devices.SWG(488, 220, 25).solve(wls)
892
+ >>> neffs = [SWG.neff, SWG.neff]
893
+ >>> NRR = EMpy.devices.NRR(couplers, neffs, l1s, l2s).solve()
894
+
895
+ @ivar Ks: List of L{Coupler}s.
896
+ @type Ks: list
897
+ @ivar neffs: List of rings' effective indexes.
898
+ @type neffs: numpy.ndarray
899
+ @ivar l1s: List of rings' arcs connecting one coupler to the next.
900
+ @type l1s: list
901
+ @ivar l2s: List of rings' arcs connecting one coupler to the previous.
902
+ @type l2s: list
903
+ @ivar THRU: THRU of the filter.
904
+ @type THRU: numpy.ndarray
905
+ @ivar DROP: DROP of the filter.
906
+ @type DROP: numpy.ndarray
907
+
908
+ """
909
+
910
+ def __init__(self, Ks, neffs, l1s, l2s):
911
+ """Set the parameters of the device and check them."""
912
+
913
+ self.wl = Ks[0].wl
914
+ self.Ks = Ks
915
+ self.neffs = neffs
916
+ self.l1s = l1s
917
+ self.l2s = l2s
918
+ self.sanity_check()
919
+
920
+ def sanity_check(self):
921
+ """Check for good input.
922
+
923
+ @raise ValueError: if wl or the number of couplers are incompatible.
924
+
925
+ """
926
+
927
+ for K in self.Ks:
928
+ K.sanity_check()
929
+ if not (numpy.alltrue(self.wl == K.wl)):
930
+ raise ValueError("incompatible wl")
931
+ if not (len(self.Ks) - 1 == len(self.neffs) == len(self.l1s) == len(self.l2s)):
932
+ raise ValueError("number of couplers and number of rings do not match.")
933
+
934
+ def solve(self):
935
+ """Compute the THRU and the DROP.
936
+
937
+ Notes
938
+ =====
939
+
940
+ No analytic expression is available, therefore build the TM of
941
+ the device for each wl.
942
+
943
+ """
944
+
945
+ self.THRU = numpy.zeros_like(self.wl).astype("complex")
946
+ self.DROP = numpy.zeros_like(self.wl).astype("complex")
947
+ for iwl, wl in enumerate(self.wl):
948
+ H = self.TM(wl)
949
+ self.THRU[iwl] = H.data[0, 0]
950
+ self.DROP[iwl] = H.data[0, 1]
951
+
952
+ return self
953
+
954
+ def TM(self, wl=None):
955
+ """Return the L{TransferMatrix} of the NRR.
956
+
957
+ Notes
958
+ =====
959
+
960
+ Compose the TM of the single devices which compose the NRR.
961
+
962
+ @return: L{TransferMatrix}.
963
+ @rtype: L{TransferMatrix}
964
+
965
+ """
966
+
967
+ return self.CM(wl).to_transfer()
968
+
969
+ def CM(self, wl=None):
970
+ """Return the L{TransferMatrix} of the NRR.
971
+
972
+ Notes
973
+ =====
974
+
975
+ First build its L{TransferMatrix}, then convert it to L{ChainMatrix}.
976
+
977
+ @return: L{ChainMatrix}.
978
+ @rtype: L{ChainMatrix}
979
+
980
+ """
981
+
982
+ if wl is None:
983
+ wl = self.wl
984
+ Hs = []
985
+ for K, neff, l1, l2 in zip(self.Ks, self.neffs, self.l1s, self.l2s):
986
+ Hs.append(
987
+ composeTMlist(
988
+ [
989
+ Line(self.wl, neff, 0.0, neff, l1).TM(wl),
990
+ K.TM(wl),
991
+ Line(self.wl, neff, 0.0, neff, l2).TM(wl),
992
+ ]
993
+ )
994
+ )
995
+ return composeCM(composeTMlist(Hs), self.Ks[-1].CM(wl))
996
+
997
+
998
+ class T_TCRR(Device):
999
+
1000
+ """Tunable Triple-Coupled Ring Resonator.
1001
+
1002
+ A T_TCRR is made of a L{MZ}, with a feedback line.
1003
+
1004
+ """
1005
+
1006
+ def __init__(self, neff, K, q, l, coupling=None):
1007
+ """Set the parameters of the device.
1008
+
1009
+ INPUT
1010
+ neff = effective index (can be complex).
1011
+ K = field coupling coeffs.
1012
+ q = coupler losses.
1013
+ l = ring arcs.
1014
+ coupling = 'optimum' to change K2, K3 and l.
1015
+ """
1016
+
1017
+ self.neff = numpy.asarray(neff)
1018
+ self.K = numpy.asarray(K)
1019
+ self.q = q
1020
+ self.l = numpy.asarray(l)
1021
+ self.coupling = coupling
1022
+ self.sanity_check()
1023
+
1024
+ def sanity_check(self):
1025
+ """Check for good input."""
1026
+ if self.K.shape != (3,):
1027
+ raise ValueError("K must be a 1D-array with 3 elements.")
1028
+ if not numpy.isscalar(self.q):
1029
+ raise ValueError("q must be a scalar.")
1030
+ if self.l.shape != (4,):
1031
+ raise ValueError("l must be a 1D-array with 4 elements.")
1032
+
1033
+ def solve(self, wls):
1034
+ """Compute the THRU and the DROP."""
1035
+ neff, K, q, l, coupling = (self.neff, self.K, self.q, self.l, self.coupling)
1036
+
1037
+ # transform the input in ndarrays
1038
+ wls = numpy.asarray(wls)
1039
+ if wls.shape != neff.shape:
1040
+ raise ValueError("wrong wls and neff shape.")
1041
+
1042
+ if coupling == "optimum":
1043
+ # optimum: l2 = 2l1, l3 = l4
1044
+ circ = l[0] + l[2] + l[3]
1045
+ l = circ * numpy.array([0.5, 1.0, 0.25, 0.25])
1046
+ # optimum: K1==K2, K3=2*K1*K1bar
1047
+ K[1] = K[0]
1048
+ K[2] = 2.0 * K[0] * numpy.sqrt(1 - K[0] ** 2)
1049
+
1050
+ Kbar = numpy.sqrt(1.0 - K**2)
1051
+ beta = 2.0 * numpy.pi * neff / wls
1052
+
1053
+ t1 = numpy.exp(-1j * beta * l[0])
1054
+ t2 = numpy.exp(-1j * beta * l[1])
1055
+ t3 = numpy.exp(-1j * beta * l[2])
1056
+ t4 = numpy.exp(-1j * beta * l[3])
1057
+
1058
+ denom = (
1059
+ q**3 * Kbar[2] * t3 * t4 * (Kbar[0] * Kbar[1] * t1 - K[0] * K[1] * t2)
1060
+ - 1.0
1061
+ )
1062
+ self.DROP = (
1063
+ q**3 * K[2] * t3 * (K[0] * Kbar[1] * t1 + Kbar[0] * K[1] * t2) / denom
1064
+ )
1065
+ self.THRU = (
1066
+ q**2 * (K[0] * K[1] * t1 - Kbar[0] * Kbar[1] * t2)
1067
+ + q**5 * Kbar[2] * t1 * t2 * t3 * t4
1068
+ ) / denom
1069
+
1070
+ return self
1071
+
1072
+
1073
+ class T_CRTCRR(Device):
1074
+
1075
+ """Tunable Coupled Ring Triple-Coupled Ring Resonator.
1076
+
1077
+ NOTE
1078
+ see 1997_IEEE Proc El_Barbarossa_Novel
1079
+ """
1080
+
1081
+ def __init__(self, neff, K, q, l, coupling=None):
1082
+ """Set the parameters of the device.
1083
+
1084
+ INPUT
1085
+ neff = effective index (can be complex).
1086
+ K = field coupling coeffs.
1087
+ q = coupler losses.
1088
+ l = ring arcs.
1089
+ coupling = 'optimum' <not implemented yet>.
1090
+ """
1091
+
1092
+ self.neff = numpy.asarray(neff)
1093
+ self.K = numpy.asarray(K)
1094
+ self.q = q
1095
+ self.l = numpy.asarray(l)
1096
+ self.coupling = coupling
1097
+ self.sanity_check()
1098
+
1099
+ def sanity_check(self):
1100
+ """Check for good input."""
1101
+ if not (self.K.shape == self.l.shape == (3,)):
1102
+ raise ValueError("K and l must be 1D-arrays with 3 elements.")
1103
+ if not numpy.isscalar(self.q):
1104
+ raise ValueError("q must be a scalar.")
1105
+
1106
+ def solve(self, wls):
1107
+ """Compute the THRU and the DROP."""
1108
+ neff, K, q, l, _ = (self.neff, self.K, self.q, self.l, self.coupling)
1109
+
1110
+ # transform the input in ndarrays
1111
+ wls = numpy.asarray(wls)
1112
+ if wls.shape != neff.shape:
1113
+ raise ValueError("wrong wls and neff shape.")
1114
+
1115
+ Kbar = numpy.sqrt(1.0 - K**2)
1116
+ beta = 2.0 * numpy.pi * neff / wls
1117
+
1118
+ t1 = numpy.exp(-1j * beta * l[0])
1119
+ t2 = numpy.exp(-1j * beta * l[1])
1120
+ t3 = numpy.exp(-1j * beta * l[2])
1121
+ t31 = t3 / t1
1122
+
1123
+ N1 = Kbar[0] ** 2 - K[0] ** 2 * t31
1124
+ N1bar = K[0] ** 2 - Kbar[0] ** 2 * t31
1125
+
1126
+ denom = (
1127
+ 1
1128
+ - q**3 * t1**2 * N1 * Kbar[2]
1129
+ - q**2 * Kbar[1] * t2**2 * (Kbar[2] - q**3 * t1**2 * N1)
1130
+ )
1131
+ self.DROP = (
1132
+ 1j
1133
+ * q**4
1134
+ * K[0]
1135
+ * K[1]
1136
+ * K[2]
1137
+ * Kbar[0]
1138
+ * t1**1.5
1139
+ * t2
1140
+ * (1 + t31)
1141
+ / denom
1142
+ )
1143
+ self.THRU = (
1144
+ q**2
1145
+ * t1
1146
+ * (
1147
+ N1bar
1148
+ - q**2 * Kbar[1] * Kbar[2] * t2**2 * N1bar
1149
+ + q**3 * Kbar[2] * t1 * t3
1150
+ - q**5 * Kbar[1] * t1 * t2**2 * t3
1151
+ )
1152
+ / denom
1153
+ )
1154
+
1155
+ return self
1156
+
1157
+
1158
+ class T_CTCRR(Device):
1159
+
1160
+ """Tunable Coupled Triple Coupled Ring Resonator.
1161
+
1162
+ NOTE
1163
+ see 1997_IEEE Proc El_Barbarossa_Novel
1164
+ """
1165
+
1166
+ def __init__(self, neff, K, q, l, coupling=None):
1167
+ """Set the parameters of the device.
1168
+
1169
+ INPUT
1170
+ neff = effective index (can be complex).
1171
+ K = field coupling coeffs.
1172
+ q = coupler losses.
1173
+ l = ring arcs.
1174
+ coupling = 'optimum'.
1175
+ """
1176
+
1177
+ self.neff = numpy.asarray(neff)
1178
+ self.K = numpy.asarray(K)
1179
+ self.q = q
1180
+ self.l = numpy.asarray(l)
1181
+ self.coupling = coupling
1182
+ self.sanity_check()
1183
+
1184
+ def sanity_check(self):
1185
+ """Check for good input."""
1186
+ if self.K.shape != (3,):
1187
+ raise ValueError("K must be a 1D-array with 3 elements.")
1188
+ if self.l.shape != (4,):
1189
+ raise ValueError("l must be a 1D-array with 4 elements.")
1190
+ if not numpy.isscalar(self.q):
1191
+ raise ValueError("q must be a scalar.")
1192
+
1193
+ def solve(self, wls):
1194
+ """Compute the THRU and the DROP."""
1195
+ neff, K, q, l, coupling = (self.neff, self.K, self.q, self.l, self.coupling)
1196
+
1197
+ # transform the input in ndarrays
1198
+ wls = numpy.asarray(wls)
1199
+ if wls.shape != neff.shape:
1200
+ raise ValueError("wrong wls and neff shape.")
1201
+
1202
+ if coupling is not None:
1203
+ # optimum: l2 = 2l1, l3 = l4
1204
+ l[1] = l[0] # OKKIO: check me!
1205
+ l[2] = 2 * l[0]
1206
+ l[3] = 2 * l[1]
1207
+ # optimum: see article
1208
+ K[2] = numpy.absolute(
1209
+ 2
1210
+ * K[0]
1211
+ * K[1]
1212
+ * numpy.sqrt(1 - K[0] ** 2)
1213
+ * numpy.sqrt(1 - K[1] ** 2)
1214
+ / (K[0] ** 2 + K[1] ** 2 - 2 * K[0] ** 2 * K[1] ** 2 - 1)
1215
+ )
1216
+
1217
+ Kbar = numpy.sqrt(1.0 - K**2)
1218
+ beta = 2.0 * numpy.pi * neff / wls
1219
+
1220
+ t1 = numpy.exp(-1j * beta * l[0])
1221
+ t2 = numpy.exp(-1j * beta * l[1])
1222
+ t3 = numpy.exp(-1j * beta * l[2])
1223
+ t4 = numpy.exp(-1j * beta * l[3])
1224
+ t31 = t3 / t1
1225
+ t42 = t4 / t2
1226
+
1227
+ N1 = Kbar[0] ** 2 - K[0] ** 2 * t31
1228
+ N1bar = K[0] ** 2 - Kbar[0] ** 2 * t31
1229
+ N2 = Kbar[1] ** 2 - K[1] ** 2 * t42
1230
+
1231
+ denom = (
1232
+ 1.0
1233
+ - q**3 * t1**2 * N1 * Kbar[2]
1234
+ - q**3 * t2**2 * N2 * (Kbar[2] - q**3 * t1**2 * N1)
1235
+ )
1236
+ self.DROP = (
1237
+ 1j
1238
+ * q**5
1239
+ * K[0]
1240
+ * K[1]
1241
+ * K[2]
1242
+ * Kbar[0]
1243
+ * Kbar[1]
1244
+ * t1**1.5
1245
+ * t2**1.5
1246
+ * (1 + t31)
1247
+ * (1 + t42)
1248
+ / denom
1249
+ )
1250
+ self.THRU = (
1251
+ q**5 * t1**2 * t3 * (q**3 * t2**2 * N2 - Kbar[2])
1252
+ + q**2 * t1 * N1bar * (q**3 * t2**2 * N2 * Kbar[2] - 1.0)
1253
+ ) / denom
1254
+
1255
+ return self
1256
+
1257
+
1258
+ class SWG(Device):
1259
+ """SOI rib straight waveguides.
1260
+
1261
+ Compute the effective index, dispersion and group index of a SOI
1262
+ rib straight waveguides.
1263
+
1264
+ Available dimensions:
1265
+
1266
+ - width: 400nm..488nm.
1267
+ - height: 220nm.
1268
+ - temperature: 25..225 centrigrades.
1269
+
1270
+ Notes
1271
+ =====
1272
+
1273
+ The effective index is interpolated from values obtained by FimmWAVE.
1274
+ Interpolation is done by a polynomial of degree 2.
1275
+
1276
+ @ivar w: Width of the waveguide.
1277
+ @type w: scalar
1278
+ @ivar h: Height of the waveguide.
1279
+ @type h: scalar
1280
+ @ivar T: Working temperature.
1281
+ @type T: scalar
1282
+ @ivar neff: Effective index.
1283
+ @type neff: numpy.ndarray
1284
+ @ivar disp: Dispersion.
1285
+ @type disp: numpy.ndarray
1286
+ @ivar ng: Group index.
1287
+ @type ng: numpy.ndarray
1288
+
1289
+ """
1290
+
1291
+ pf488_25 = [-0.00000006023385, -0.00096294997155, 4.06422992413444]
1292
+ pf488_125 = [-0.00000006391000, -0.00095671206284, 4.08461627147979]
1293
+ pf488_225 = [-0.00000006806052, -0.00094901669805, 4.10613946676196]
1294
+
1295
+ pf400_25 = [1.733732171601458e-007, -1.911057235401467e-003, 4.774734532616097e000]
1296
+ pf400_125 = [1.477287990868007e-007, -1.842388456386622e-003, 4.750925877259235e000]
1297
+ pf400_225 = [1.255208385485759e-007, -1.784557912270417e-003, 4.737972109281652e000]
1298
+
1299
+ def __init__(self, w, h, T):
1300
+
1301
+ self.w = w
1302
+ self.h = h # not used!
1303
+ self.T = T
1304
+
1305
+ def sanity_check(self):
1306
+ pass
1307
+
1308
+ def solve(self, wls):
1309
+ """Compute neff, disp and ng for a given wls.
1310
+
1311
+ @param wls: Wavelength.
1312
+ @type wls: numpy.ndarray
1313
+
1314
+ @raise ValueError: if w or T are out of bounds (and
1315
+ interpolation is not possible).
1316
+
1317
+ """
1318
+
1319
+ if (self.w, self.T) == (488, 25):
1320
+ pf = SWG.pf488_25
1321
+ elif (self.w, self.T) == (488, 125):
1322
+ pf = SWG.pf488_125
1323
+ elif (self.w, self.T) == (488, 225):
1324
+ pf = SWG.pf488_225
1325
+ elif (self.w, self.T) == (400, 225):
1326
+ pf = SWG.pf400_25
1327
+ elif (self.w, self.T) == (400, 125):
1328
+ pf = SWG.pf400_125
1329
+ elif (self.w, self.T) == (400, 225):
1330
+ pf = SWG.pf400_225
1331
+ elif self.w < 400 or self.w > 488 or self.T < 25 or self.T > 225:
1332
+ raise ValueError("input out of bounds")
1333
+ else:
1334
+ import scipy.interpolate
1335
+
1336
+ pf_ = numpy.zeros((2, 3, 3))
1337
+ [w_, T_] = numpy.meshgrid([400, 488], [25, 125, 225])
1338
+ pf_[0, 0, :] = SWG.pf400_25
1339
+ pf_[0, 1, :] = SWG.pf400_125
1340
+ pf_[0, 2, :] = SWG.pf400_225
1341
+ pf_[1, 0, :] = SWG.pf488_25
1342
+ pf_[1, 1, :] = SWG.pf488_125
1343
+ pf_[1, 2, :] = SWG.pf488_225
1344
+ pf = [
1345
+ scipy.interpolate.interp2d(w_.T, T_.T, pf_[:, :, i])(self.w, self.T)[0]
1346
+ for i in range(3)
1347
+ ]
1348
+
1349
+ # transform the input in an ndarray
1350
+ wls = numpy.asarray(wls)
1351
+
1352
+ wls_nm = wls * 1e9
1353
+ neff = numpy.polyval(pf, wls_nm)
1354
+ # disp = dneff/dlambda [nm^-1]
1355
+ disp = numpy.polyval([2 * pf[0], pf[1]], wls_nm)
1356
+ # ng = neff - lambda * disp
1357
+ ng = neff - disp * wls_nm
1358
+
1359
+ self.neff = neff
1360
+ self.disp = disp
1361
+ self.ng = ng
1362
+
1363
+ return self
1364
+
1365
+
1366
+ class Etalon(Device):
1367
+
1368
+ """Etalon.
1369
+
1370
+ @ivar R: Reflectivity.
1371
+ @ivar theta: Angle of incidence.
1372
+
1373
+ @see: U{http://en.wikipedia.org/wiki/Etalon}
1374
+
1375
+ """
1376
+
1377
+ def __init__(self, layer, R, theta):
1378
+ self.FWHMwl = self.FSRwl / self.FINESSE
1379
+ self.layer = layer
1380
+ self.R = R
1381
+ self.theta = theta
1382
+
1383
+ def sanity_check(self):
1384
+ pass
1385
+
1386
+ def solve(self, wls):
1387
+ """Compute the frequency response of the etalon."""
1388
+
1389
+ # transform the input in an ndarray
1390
+ wls = numpy.asarray(wls)
1391
+
1392
+ n = self.layer.mat.n(wls)
1393
+ l = self.layer.thickness
1394
+
1395
+ # phase difference between any succeeding reflection
1396
+ self.delta = 2 * numpy.pi / wls * 2 * n * l * numpy.cos(self.theta)
1397
+ # coefficient of finesse
1398
+ self.F = 4.0 * self.R / (1 - self.R) ** 2
1399
+ # transmission function
1400
+ self.Te = 1.0 / (1.0 + self.F * numpy.sin(self.delta / 2.0) ** 2)
1401
+ self.Rmax = 1.0 - 1.0 / (1.0 + self.F)
1402
+
1403
+ self.wl0 = numpy.mean(wls)
1404
+ self.FSRwl = self.wl0**2 / (2 * n * l * numpy.cos(self.theta) + self.wl0)
1405
+
1406
+ (self.f0, self.FSR) = EMpy.utils.wl2f(self.wl0, self.FSRwl)
1407
+
1408
+ self.FINESSE = numpy.pi / (2 * numpy.arcsin(1.0 / numpy.sqrt(self.F)))
1409
+ # self.FINESSE = numpy.pi * numpy.sqrt(self.F) / 2.
1410
+ # self.FINESSE = numpy.pi * numpy.sqrt(self.R) / (1 - self.R)
1411
+
1412
+ self.FWHM = self.FSR / self.FINESSE
1413
+
1414
+ return self