PLaSK library
Loading...
Searching...
No Matches
threshold.py
Go to the documentation of this file.
1# This file is part of PLaSK (https://plask.app) by Photonics Group at TUL
2# Copyright (c) 2022 Lodz University of Technology
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# coding: utf8
14# Copyright (C) 2014 Photonics Group, Lodz University of Technology
15
16import numpy as np
17
18import electrical.diffusion
19import electrical.shockley
20import gain.freecarrier
21import plask
22import thermal.static
23
24try:
25 import scipy
26except ImportError:
27 plask.print_log('warning', "scipy could not be imported."
28 " You will not be able to run some algorithms."
29 " Install scipy to resolve this issue.")
30else:
31 import scipy.optimize
32
33from .thermoelectric import attribute, h5open, ThermoElectric
34from . import _doc
35
36
38
39 _OPTICAL_ROOTS = {}
40
41 _Diffusion = None
42 _Gain = None
43 _Optical = None
44 _optarg = 'lam'
45 _lam0 = 'lam0'
46
47 tfreq = 6.0
48 vmin = None
49 vmax = None
50 optical_resolution = (800, 600)
51 vtol = 1e-5
52 quick = False
53 maxiter = 50
54 skip_thermal = False
55
56 def __init__(self, name):
57 super().__init__(name)
58 self.diffusion = self._Diffusion(name)
59 self.gain = self._Gain(name)
60 self.optical = self._Optical(name)
64 self._invalidate = None
65 self.modeno = None
66 self._sn = 0
67
68 def __reconnect(self):
69 self.diffusion.inTemperature = self.thermal.outTemperature
70 self.diffusion.inCurrentDensity = self.electrical.outCurrentDensity
71 self.gain.inTemperature = self.thermal.outTemperature
72 self.gain.inCarriersConcentration = self.diffusion.outCarriersConcentration
73 self.optical.inTemperature = self.thermal.outTemperature
74 self.optical.inGain = self.gain.outGain
75
76 def reconnect(self):
77 """
78 Reconnect all internal solvers.
79
80 This method should be called if some of the internal solvers were changed manually.
81 """
82 super().reconnect()
84
85 def _parse_xpl(self, tag, manager):
86 if tag == 'root':
87 self.vmin = tag.get('vmin', self.vmin)
88 self.vmax = tag.get('vmax', self.vmax)
89 self.ivb = tag['bcond']
90 self.vtolvtol = tag.get('vtol', self.vtolvtol)
91 self.maxitermaxiter = tag.get('maxiter', self.maxitermaxiter)
92 self.quickquick = tag.get('quick', self.quickquick)
93 elif tag == 'diffusion':
94 self._read_attr(tag, 'fem-method', self.diffusion, str, 'fem_method')
95 self._read_attr(tag, 'accuracy', self.diffusion, float)
96 self._read_attr(tag, 'abs-accuracy', self.diffusion, float, 'abs_accuracy')
97 self._read_attr(tag, 'maxiters', self.diffusion, int)
98 self._read_attr(tag, 'maxrefines', self.diffusion, int)
99 self._read_attr(tag, 'interpolation', self.diffusion, str)
100 elif tag == 'gain':
101 self._read_attr(tag, 'lifetime', self.gain, float)
102 self._read_attr(tag, 'matrix-elem', self.gain, float, 'matrix_element')
103 self._read_attr(tag, 'strained', self.gain, bool)
104 elif tag.name in self._OPTICAL_ROOTS:
105 root = getattr(self.optical, self._OPTICAL_ROOTS[tag.name])
106 self._read_attr(tag, 'method', root, str)
107 self._read_attr(tag, 'tolx', root, float)
108 self._read_attr(tag, 'tolf-min', root, float, 'tolf_min')
109 self._read_attr(tag, 'tolf-max', root, float, 'tolf_max')
110 self._read_attr(tag, 'maxstep', root, float)
111 self._read_attr(tag, 'maxiter', root, int)
112 self._read_attr(tag, 'alpha', root, float)
113 self._read_attr(tag, 'lambda', root, float)
114 self._read_attr(tag, 'initial-range', root, tuple, 'initial_range')
115 elif tag == 'output':
117 tag.get('optical-res-y', self.optical_resolutionoptical_resolution[1])
118 else:
119 if tag == 'geometry':
120 self.optical.geometry = self.diffusion.geometry = self.gain.geometry = \
121 tag.getitem(manager.geo, 'optical')
122 elif tag == 'mesh':
123 if 'diffusion' in tag: self.diffusion.mesh = tag.getitem(manager.msh, 'diffusion')
124 if 'optical' in tag: self.optical.mesh = tag.getitem(manager.msh, 'optical')
125 if 'gain' in tag: self.gain.mesh = tag.getitem(manager.msh, 'gain')
126 elif tag == 'loop':
127 self.skip_thermalskip_thermal = tag.get('skip-thermal', self.skip_thermalskip_thermal)
128 self._read_attr(tag, 'inittemp', self.gain, float, 'T0')
129 super()._parse_xpl(tag, manager)
130
131 def on_initialize(self):
132 if not self.skip_thermalskip_thermal:
133 self.thermal.initialize()
134 self.electrical.initialize()
135 self.diffusion.initialize()
136 self.gain.initialize()
137 self.optical.initialize()
138
139 def on_invalidate(self):
140 if not self.skip_thermalskip_thermal:
141 self.thermal.invalidate()
143 self.diffusion.invalidate()
144 self.optical.invalidate()
145
146 def _optargs(self):
147 return {}
148
149 def _get_lam(self):
150 if callable(self.get_lamget_lam):
151 return self.get_lamget_lam()
152 else:
153 return self.get_lamget_lam
154
155 def get_lam(self):
156 raise NotImplemented('get_lam')
157
159 """
160 Perform thermo-electric calculations.
161
162 This method may be called manually to perform thermo-electric calculations.
163 Afterwards, one may investigate gain spectrum or verify settings of the optical
164 solver.
165 """
166 self.initialize()
168 self.electrical.compute()
169 else:
170 if self._invalidate:
171 self.thermal.invalidate()
173 self.diffusion.invalidate()
174 self.gain.invalidate()
175 verr = 2. * self.electrical.maxerr
176 terr = 2. * self.thermal.maxerr
177 while terr > self.thermal.maxerr or verr > self.electrical.maxerr:
178 verr = self.electrical.compute(self.tfreqtfreq)
179 terr = self.thermal.compute(1)
180 self.diffusion.compute()
181
182 def step(self, volt, save=False):
183 """
184 Function performing one step of the threshold search.
185
186 Args:
187 volt (float): Voltage on a specified boundary condition (V).
188
189 save (bool): If `True` the computed fields are saved to the
190 HDF5 file after each computations step.
191
192 Returns:
193 float: Loss of a specified mode
194 """
195 plask.print_log('detail', "ThresholdSearch: V = {0:.4f} V".format(volt))
196 self.electrical.voltage_boundary[self.ivb].value = volt
198 self.optical.invalidate()
199 optstart = getattr(self, 'get_'+self._optarg)()
200 if self._optarg is None:
201 self.modeno = self.optical.find_mode(optstart, **self._optargs())
202 else:
203 _optargs = self._optargs().copy()
204 _optargs[self._optarg] = optstart
205 self.modeno = self.optical.find_mode(**_optargs)
206 val = self.optical.modes[self.modeno].loss
207 plask.print_log('result', "ThresholdSearch: V = {:.4f} V, loss = {:g} / cm".format(volt, val))
208 if save:
209 self.savesave(None if save is True else save, 'ThresholdSearchStep/{:03}[{:.4f}V]'.format(self._sn, volt))
210 self._sn += 1
211 return val
212
213 def _quickstep(self, arg):
214 """
215 Function performing one step of the quick threshold search.
216
217 Args:
218 arg (array): Array containing voltage on a specified boundary condition (V) and wavelength.
219
220 Returns:
221 array: Imaginary and real part of a specified mode
222 """
223 volt, lam = arg * self._quickscale
224 plask.print_log('detail', "ThresholdSearch: V = {0:.4f} V, lam = {1:g} nm".format(volt, lam))
225 self.electrical.voltage_boundary[self.ivb].value = volt
227 if self._optarg is None:
228 det = self.optical.get_determinant(lam, **self._optargs())
229 else:
230 _optargs = self._optargs().copy()
231 _optargs[self._optarg] = lam
232 det = self.optical.get_determinant(**_optargs)
233 plask.print_log('result', "ThresholdSearch: V = {0:.4f} V, lam = {1:g} nm, det = {2.real}{2.imag:+g}j"
234 .format(volt, lam, det))
235 return np.array([det.real, det.imag])
236
238 """
239 Function computing determinant of the optical solver.
240
241 Args:
242 lam (float or array): Wavelength to compute the determinant for (nm).
243
244 Returns:
245 float or array: Optical determinant.
246 """
248 if self._optarg is None:
249 return self.optical.get_determinant(lam, **self._optargs())
250 else:
251 _optargs = self._optargs().copy()
252 _optargs[self._optarg] = lam
253 return self.optical.get_determinant(**_optargs)
254
255 def _get_in_junction(self, func, axis=None):
256 if axis is None: axis = self.optical.mesh
257 results = {}
258 for i, (lb, msh) in enumerate(self._iter_levels(self.diffusion.geometry, axis, 'QW', 'QD', 'gain')):
259 if not lb:
260 lb = 0
261 else:
262 try: lb = int(lb)
263 except ValueError: pass
264 results[lb] = func(msh)
265 return results
266
267 def get_junction_concentrations(self, interpolation='linear'):
268 """
269 Get carriers concentration at the active regions.
270
271 Args:
272 interpolation (str): Interpolation used when retrieving current density.
273
274 Return:
275 dict: Dictionary of junction current density data.
276 Keys are the junction number.
277 """
278 return self._get_in_junction(lambda msh: self.diffusion.outCarriersConcentration(msh, interpolation),
279 self.diffusion.mesh)
280
281 def get_junction_gains(self, axis=None, interpolation='linear'):
282 """
283 Get gain at the active regions.
284
285 Args:
286 axis (mesh or sequence): Points along horizontal axis to plot gain at.
287 Defaults to the optical mesh.
288
289 interpolation (str): Interpolation used when retrieving current density.
290
291 Return:
292 dict: Dictionary of junction current density data.
293 Keys are the junction number.
294 """
295 lam = getattr(self.optical, self._lam0).real
296 if lam is None: lam = self._get_lam().real
297 return self._get_in_junction(lambda msh: self.gain.outGain(msh, lam, interpolation), axis)
298
299 def get_gain_spectrum(self, lams, pos=0., junction=0):
300 """
301 Get gain spectrum for specified junction.
302
303 Args:
304 lams (array of floats): Wavelengths for which the spectrum should be plotted.
305
306 pos (float): Lateral position fo the point in which the spectrum is plotted.
307
308 junction (int): Junction number to take gain from.
309
310 Return:
311 Data: Gain spectrum.
312 """
313 level = list(self._iter_levels(self.diffusion.geometry, [pos]))[junction][1]
314 return self.gain.spectrum(level[0])(lams)
315
316 def _plot_in_junction(self, func, axis, bounds, kwargs, label):
317 if axis is None: axis = self.optical.mesh
318 i = 0
319 for i, (lb, msh) in enumerate(self._iter_levels(self.diffusion.geometry, axis, 'QW', 'QD', 'gain')):
320 values = np.array(func(msh))
321 if label is None:
322 lab = "Junction {:s}".format(lb)
323 elif isinstance(label, tuple) or isinstance(label, tuple):
324 lab = label[i]
325 else:
326 lab = label
327 plask.plot(msh.axis0, values, label=lab, **kwargs)
328 if i > 1:
329 plask.legend(loc='best')
330 plask.xlabel(u"${}$ (µm)".format(plask.config.axes[-2]))
331 if bounds:
332 self._plot_hbounds(self.optical)
333
334 def plot_junction_concentration(self, bounds=True, interpolation='linear', label=None, **kwargs):
335 """
336 Plot carriers concentration at the active region.
337
338 Args:
339 bounds (bool): If *True* then the geometry objects boundaries are
340 plotted.
341
342 interpolation (str): Interpolation used when retrieving current density.
343
344 label (str or sequence): Label for each junction. It can be a sequence of
345 consecutive labels for each junction, or a string
346 in which case the same label is used for each
347 junction. If omitted automatic label is generated.
348
349 **kwargs: Keyword arguments passed to the plot function.
350 """
351 self._plot_in_junction(lambda msh: self.diffusion.outCarriersConcentration(msh, interpolation),
352 self.diffusion.mesh, bounds, kwargs, label)
353 plask.ylabel(u"Carriers Concentration (1/cm\xb3)")
354 plask.window_title("Carriers Concentration")
355
356 def plot_junction_gain(self, axis=None, bounds=True, interpolation='linear', label=None, **kwargs):
357 """
358 Plot gain at the active region.
359
360 Args:
361 axis (mesh or sequence): Points along horizontal axis to plot gain at.
362 Defaults to the optical mesh.
363
364 bounds (bool): If *True* then the geometry objects boundaries are
365 plotted.
366
367 interpolation (str): Interpolation used when retrieving current density.
368
369 label (str or sequence): Label for each junction. It can be a sequence of
370 consecutive labels for each junction, or a string
371 in which case the same label is used for each
372 junction. If omitted automatic label is generated.
373
374 **kwargs: Keyword arguments passed to the plot function.
375 """
376 lam = getattr(self.optical, self._lam0).real
377 if lam is None: lam = self._get_lam().real
378 self._plot_in_junction(lambda msh: self.gain.outGain(msh, lam, interpolation),
379 axis, bounds, kwargs, label)
380 plask.ylabel(u"Gain (1/cm)")
381 plask.window_title("Gain Profile")
382
383 def plot_gain_spectrum(self, lams, pos=0., junction=0, comp=None, **kwargs):
384 """
385 Plot gain spectrum for specified junction.
386
387 Args:
388 lams (array of floats): Wavelengths for which the spectrum should be plotted.
389
390 pos (float): Lateral position fo the point in which the spectrum is plotted.
391
392 junction (int): Junction number to take gain from.
393
394 comp (int or str): Spectrum component to plot
395
396 **kwargs: Keyword arguments passed to the plot function.
397 """
398 level = list(self._iter_levels(self.diffusion.geometry, [pos], 'QW', 'QD', 'gain'))[junction][1]
399 spectrum = self.gain.spectrum(level[0])
400 if comp is None:
401 plask.plot(lams, spectrum(lams), **kwargs)
402 else:
403 _comp = comp
404 try:
405 if isinstance(comp, str):
406 comp = plask.config.axes.index(comp)
407 if comp != 0: comp -= 1
408 if comp < 0 or comp > 1:
409 raise ValueError(comp)
410 except (ValueError, TypeError) as err:
411 raise ValueError("Bad spectrum component '{}'".format(_comp)) from err
412 plask.plot(lams, np.array(spectrum(lams))[:,comp], **kwargs)
413 plask.xlabel(u"Wavelength (nm)")
414 plask.ylabel(u"Gain (1/cm)")
415 plask.window_title("Gain Spectrum")
416
417 def plot_optical_determinant(self, lams, **kwargs):
418 """
419 Function plotting determinant of the optical solver.
420
421 Args:
422 lams (array): Wavelengths to plot the determinant for (nm).
423
424 **kwargs: Keyword arguments passed to the plot function.
425 """
426 vals = self.get_optical_determinant(lams)
427 plask.plot(lams, abs(vals))
428 plask.yscale('log')
429 plask.xlabel("Wavelength (nm)")
430 plask.ylabel("Determinant [ar.u.]")
431
432 def compute(self, save=True, invalidate=False, group='ThresholdSearch', stepsave=False):
433 """
434 Execute the algorithm.
435
436 In the beginning the solvers are invalidated and next, the self-
437 consistent loop of thermal, electrical, gain, and optical calculations
438 are run within the root-finding algorithm until the mode is found
439 with zero optical losses.
440
441 Args:
442 save (bool or str): If `True` the computed fields are saved to the
443 HDF5 file named after the script name with the suffix denoting
444 either the batch job id or the current time if no batch system
445 is used. The filename can be overridden by setting this parameter
446 as a string.
447
448 invalidate (bool): If this flag is set, solvers are invalidated
449 in the beginning of the computations.
450
451 group (str): HDF5 group to save the data under.
452
453 stepsave (bool): If `True` the computed fields are saved to the
454 HDF5 file after each computations step.
455
456 Returns:
457 The voltage set to ``ivolt`` boundary condition for the threshold.
458 The threshold current can be then obtained by calling:
459
460 >>> solver.get_total_current()
461 123.0
462 """
463
464 if invalidate:
465 if not self.skip_thermalskip_thermal:
466 self.thermal.invalidate()
468 self.diffusion.invalidate()
469 self.optical.invalidate()
470 self._invalidate = invalidate
471 self.initialize()
472
473 self._sn = 0
475 self._max_gain = None
476
477 if (self.vmin is None) != (self.vmax is None):
478 raise ValueError("Both 'vmin' and 'vmax' must be either None or a float")
479 if self.vmin is None:
480 volt = self.electrical.voltage_boundary[self.ivb].value
481 if self.quickquick:
482 if stepsave:
483 plask.print_log('warning', "Fields cannot be saved in each step if the quick method is used")
485 lam = self._get_lam().real
486 self._quickscale = np.array([volt, lam])
487 result = scipy.optimize.root(self._quickstep, np.array([1., 1.]),
488 tol=1e-3 * min(self.vtolvtol/volt, 1e-6),
489 options=dict(maxfev=2*self.maxitermaxiter, eps=1e-6))
490 if not result.status:
491 raise plask.ComputationError(result.message)
492 self.threshold_voltage, lam = result.x * self._quickscale
493 if self._optarg is None:
494 self.modeno = self.optical.set_mode(lam, **self._optargs())
495 else:
496 _optargs = self._optargs().copy()
497 _optargs[self._optarg] = lam
498 self.modeno = self.optical.set_mode(**_optargs)
499 else:
500 self.threshold_voltage = scipy.optimize.newton(self.step, volt, args=(stepsave,),
501 tol=self.vtolvtol/volt, maxiter=self.maxitermaxiter)
502 else:
503 if self.quickquick:
504 raise RuntimeError("Quick computation only allowed with a single starting point")
505 self.threshold_voltage = scipy.optimize.brentq(self.step, self.vmin, self.vmax, args=(stepsave,),
506 xtol=self.vtolvtol * 2. / (self.vmin+self.vmax),
507 maxiter=self.maxitermaxiter, disp=True)
508
510
511 infolines = self._get_info_get_info()
512
513 if save:
514 filename = self.savesave(None if save is True else save)
515 if filename.endswith('.h5'): filename = filename[:-3]
516 plask.print_log('info', "Results saved to file '{}.txt'".format(filename))
517 with open(filename+'.txt', 'a', encoding='utf-8') as out:
518 out.writelines(line + '\n' for line in infolines)
519 out.write("\n")
520
521 plask.print_log('important', "Threshold Search Finished")
522 for line in infolines:
523 plask.print_log('important', " " + line)
524
525 return self.threshold_voltage
526
527 def _get_info(self):
528 result = self._get_defines_info() + [
529 "Threshold voltage (V): {:8.3f}".format(self.threshold_voltage),
530 "Threshold current (mA): {:8.3f}".format(self.threshold_current),
531 "Maximum temperature (K): {:8.3f}".format(max(self.thermal.outTemperature(self.thermal.mesh)))
532 ]
533 levels = list(self._iter_levels(self.diffusion.geometry, self.diffusion.mesh, 'QW', 'QD', 'gain'))
534 max_concentration = []
535 for no, mesh in levels:
536 value = self.diffusion.outCarriersConcentration(mesh)
537 max_concentration.append(max(value))
538 result.append("Maximum concentration (1/cm3): {}"
539 .format(', '.join('{:.3e}'.format(c) for c in max_concentration)))
540 lam = getattr(self.optical, self._lam0).real
541 if lam is None: lam = self._get_lam().real
542 max_gain = []
543 for no, mesh in levels:
544 value = self.gain.outGain(mesh, lam).array[:,:,0]
545 max_gain.append(max(value))
546 result.append("Maximum gain (1/cm): {}"
547 .format(', '.join('{:9.3f}'.format(g[0]) for g in max_gain)))
548 return result
549
550 def save(self, filename=None, group='ThresholdSearch', optical_resolution=None):
551 """
552 Save the computation results to the HDF5 file.
553
554 Args:
555 filename (str): The file name to save to.
556 If omitted, the file name is generated automatically based on
557 the script name with suffix denoting either the batch job id or
558 the current time if no batch system is used.
559
560 group (str): HDF5 group to save the data under.
561
562 optical_resolution (tuple of ints): Number of points in horizontal and vertical directions
563 for optical field.
564 """
565 if optical_resolution is None: optical_resolution = self.optical_resolutionoptical_resolution
566 h5file, group, filename, close = h5open(filename, group)
567 self._save_thermoelectric(h5file, group)
568 levels = list(self._iter_levels(self.diffusion.geometry, self.diffusion.mesh, 'QW', 'QD', 'gain'))
569 for no, mesh in levels:
570 value = self.diffusion.outCarriersConcentration(mesh)
571 plask.save_field(value, h5file, group + '/Junction'+no+'CarriersConcentration')
572 lam = getattr(self.optical, self._lam0).real
573 if lam is None: lam = self._get_lam().real
574 for no, mesh in levels:
575 value = self.gain.outGain(mesh, lam)
576 plask.save_field(value, h5file, group + '/Junction'+no+'Gain')
577 if self.modeno is not None:
578 obox = self.optical.geometry.bbox
579 oaxis = plask.mesh.Regular(obox.left, obox.right, optical_resolution[0])
580 omesh = plask.mesh.Rectangular2D(oaxis,
581 plask.mesh.Regular(obox.bottom, obox.top, optical_resolution[1]))
582 ofield = self.optical.outLightMagnitude(self.modeno, omesh)
583 plask.save_field(ofield/max(ofield), h5file, group + '/LightMagnitude')
584 try:
585 nrfield = self.optical.outRefractiveIndex(omesh)
586 plask.save_field(nrfield, h5file, group + '/RefractiveIndex')
587 except AttributeError:
588 epsfield = self.optical.outEpsilon(omesh)
589 plask.save_field(epsfield, h5file, group + '/Epsilon')
590 rmesh = next(self._iter_levels(self.optical.geometry, oaxis, 'QW', 'QD', 'gain'))[1]
591 orfield = self.optical.outLightMagnitude(self.modeno, rmesh)
592 plask.save_field(orfield/max(orfield), h5file, group + '/HorizontalLightMagnitude')
593
594 if close:
595 h5file.close()
596 plask.print_log('info', "Fields saved to file '{}' in group '{}'".format(filename, group))
597 return filename
598
599 def get_optical_field(self, resolution=None):
600 """
601 Get computed optical mode field at threshold.
602
603 Args:
604 resolution (tuple of ints): Number of points in horizontal and vertical directions.
605 """
606 if resolution is None: resolution = self.optical_resolutionoptical_resolution
607 box = self.optical.geometry.bbox
608 intensity_mesh = plask.mesh.Rectangular2D(plask.mesh.Regular(box.left, box.right, resolution[0]),
609 plask.mesh.Regular(box.bottom, box.top, resolution[1]))
610 field = self.optical.outLightMagnitude(self.modeno, intensity_mesh)
611 return field
612
613 def get_optical_field_horizontal(self, resolution=None, interpolation='linear'):
614 """
615 Get horizontal distribution of the computed optical mode field at threshold.
616
617 Args:
618 resolution (int): Number of points in horizontal direction.
619
620 interpolation (str): Interpolation used when retrieving current density.
621 """
622 if resolution is None:
623 resolution = self.optical_resolutionoptical_resolution[0]
624 box = self.optical.geometry.bbox
625 raxis = plask.mesh.Regular(box.left, box.right, resolution)
626 rmesh = next(self._iter_levels(self.optical.geometry, raxis, 'QW', 'QD', 'gain'))[1]
627 field = self.optical.outLightMagnitude(self.modeno, rmesh, interpolation)
628 return field
629
630 def get_optical_field_vertical(self, pos=0.01, offset=0.5, resolution=None, interpolation='linear'):
631 """
632 Plot vertical distribution of the computed optical mode field at threshold and
633 refractive index profile.
634
635 Args:
636 resolution (int): Number of points in horizontal direction.
637
638 pos (float): Horizontal position to get the field at.
639
640 offset (float): Distance above and below geometry boundary to include into
641 the plot.
642
643 interpolation (str): Interpolation used when retrieving current density.
644 """
645 if resolution is None:
646 resolution = self.optical_resolutionoptical_resolution[1]
647 box = self.optical.geometry.bbox
648 zaxis = plask.mesh.Regular(box.bottom - offset, box.top + offset, 10 * resolution)
649 zmesh = plask.mesh.Rectangular2D([pos], zaxis)
650 field = self.optical.outLightMagnitude(self.modeno, zmesh, interpolation)
651 return field
652
653 def plot_optical_field(self, resolution=None, geometry_color='0.75', geometry_alpha=0.35, geometry_lw=1.0, **kwargs):
654 """
655 Plot computed optical mode field at threshold.
656
657 Args:
658 resolution (tuple of ints): Number of points in horizontal and vertical directions.
659
660 geometry_color (str or ``None``): Matplotlib color specification
661 for the geometry. If ``None``, structure is not plotted.
662
663 geometry_alpha (float): Geometry opacity (1 — fully opaque, 0 – invisible).
664
665 geometry_lw (float): Line width for geometry.
666
667 **kwargs: Keyword arguments passed to the plot function.
668 """
669 field = self.get_optical_field(resolution)
670 plask.plot_field(field, **kwargs)
671 plask.plot_geometry(self.optical.geometry, color=geometry_color, lw=geometry_lw)
672 plask.window_title("Light Intensity")
673
674 def plot_optical_field_horizontal(self, resolution=None, bounds=True, interpolation='linear', **kwargs):
675 """
676 Plot horizontal distribution of the computed optical mode field at threshold.
677
678 Args:
679 resolution (int): Number of points in horizontal direction.
680
681 bounds (bool): If *True* then the geometry objects boundaries are
682 plotted.
683
684 interpolation (str): Interpolation used when retrieving current density.
685
686 **kwargs: Keyword arguments passed to the plot function.
687 """
688 field = self.get_optical_field_horizontal(resolution, interpolation)
689 color = kwargs.pop('color', None)
690 if color is None:
691 try:
692 color = plask.rcParams['axes.prop_cycle']
693 except KeyError:
694 color = plask.rcParams['axes.color_cycle'][1]
695 else:
696 color = color.by_key()['color'][1]
697 plask.plot_profile(field/max(field), color=color, **kwargs)
698 if bounds:
699 self._plot_hbounds(self.optical)
700 plask.ylabel("Light Intensity [arb.u.]")
701 plask.window_title("Radial Light Intensity")
702
703 def plot_optical_field_vertical(self, pos=0.01, offset=0.5, resolution=None, interpolation='linear', **kwargs):
704 """
705 Plot vertical distribution of the computed optical mode field at threshold and
706 refractive index profile.
707
708 Args:
709 resolution (int): Number of points in horizontal direction.
710
711 pos (float): Horizontal position to get the field at.
712
713 offset (float): Distance above and below geometry boundary to include into
714 the plot.
715
716 interpolation (str): Interpolation used when retrieving current density.
717
718 **kwargs: Keyword arguments passed to the plot function.
719 """
720 field = self.get_optical_field_vertical(pos, offset, resolution, interpolation)
721 try:
722 cc = plask.rcParams['axes.prop_cycle']
723 except KeyError:
724 cc = plask.rcParams['azmeshxes.color_cycle']
725 else:
726 cc = cc.by_key()['color']
727 color2 = cc[0]
728 color = kwargs.pop('color', None)
729 if color is None:
730 color = cc[1]
731 ax1 = plask.gca()
732 plask.plot_profile(field/max(field), color=color, **kwargs)
733 plask.ylabel("Light Intensity [arb.u.]")
734 ax2 = plask.twinx()
735 try:
736 plask.plot_profile(self.optical.outRefractiveIndex(field.mesh).real, comp=0, color=color2)
737 except AttributeError:
738 eps = self.optical.outEpsilon(field.mesh)
739 nr = plask.Data((np.array(eps)[:,0,0]**0.5).real, eps.mesh)
740 plask.plot_profile(nr, color=color2)
741 plask.ylabel("Refractive Index")
742 ax1.set_zorder(ax2.get_zorder()+1)
743 ax1.patch.set_visible(False)
744 ax2.patch.set_visible(True)
745 plask.xlim(field.mesh.axis1[0], field.mesh.axis1[-1])
746 plask.window_title("Vertical Light Intensity")
747
748
750 """
751 Solver for threshold search of semiconductor laser.
752
753 This solver performs thermo-electrical computations followed by
754 determination ot threshold current and optical analysis in order to
755 determine the threshold of a semiconductor laser. The search is
756 performed by ``scipy`` root finding algorithm in order to determine
757 the voltage and electric current ensuring no optical loss in the
758 laser cavity.
759
760 The computations can be executed using `compute` method, after which
761 the results may be save to the HDF5 file with `save` or presented visually
762 using ``plot_...`` methods. If ``save`` parameter of the :meth:`compute` method
763 is *True* the fields are saved automatically after the computations.
764 The file name is based on the name of the executed script with suffix denoting
765 either the launch time or the identifier of a batch job if a batch system
766 (like SLURM, OpenPBS, or SGE) is used.
767 """
768
769 _Thermal = thermal.static.StaticCyl
770 _Electrical = electrical.shockley.ShockleyCyl
771 _Diffusion = electrical.diffusion.DiffusionCyl
772 _Gain = gain.freecarrier.FreeCarrierCyl
773
774 _OPTICAL_ROOTS = {'optical-root': 'root', 'optical-stripe-root': 'stripe_root'}
775
776 outTemperature = property(lambda self: self.thermalthermal.outTemperature, doc=_Thermal.outTemperature.__doc__)
777 outHeatFlux = property(lambda self: self.thermalthermal.outHeatFlux, doc=_Thermal.outHeatFlux.__doc__)
778
779 outThermalConductivity = property(lambda self: self.thermalthermal.outThermalConductivity,
780 doc=_Thermal.outThermalConductivity.__doc__)
781 outVoltage = property(lambda self: self.electricalelectrical.outVoltage, doc=_Electrical.outVoltage.__doc__)
782 outCurrentDensity = property(lambda self: self.electricalelectrical.outCurrentDensity,
783 doc=_Electrical.outCurrentDensity.__doc__)
784 outHeat = property(lambda self: self.electricalelectrical.outHeat, doc=_Electrical.outHeat.__doc__)
785 outConductivity = property(lambda self: self.electricalelectrical.outConductivity, doc=_Electrical.outConductivity.__doc__)
786 outCarriersConcentration = property(lambda self: self.diffusiondiffusion.outCarriersConcentration,
787 doc=_Diffusion.outCarriersConcentration.__doc__)
788 outGain = property(lambda self: self.gaingain.outGain, doc=_Gain.outGain.__doc__)
789 outLightMagnitude = property(lambda self: self.opticaloptical.outLightMagnitude, doc=_doc.outLightMagnitude)
790 outLoss = property(lambda self: self.opticaloptical.outLoss, doc=_doc.outLoss)
791 outWavelength = property(lambda self: self.opticaloptical.outWavelength, doc=_doc.outWavelength)
792 outRefractiveIndex = property(lambda self: self.opticaloptical.outRefractiveIndex, doc=_doc.outRefractiveIndex)
793 outLightE = property(lambda self: self.opticaloptical.outLightE, doc=_doc.outLightE)
794
795 thermal = attribute(_Thermal.__name__+"()")
796 ":class:`thermal.static.StaticCyl` solver used for thermal calculations."
797
798 electrical = attribute(_Electrical.__name__+"()")
799 ":class:`electrical.shockley.ShockleyCyl` solver used for electrical calculations."
800
801 diffusion = attribute(_Diffusion.__name__+"()")
802 ":class:`electrical.diffusion.DiffusionCyl` solver used for electrical calculations."
803
804 gain = attribute(_Gain.__name__+"()")
805 ":class:`gain.freecarrier.FreeCarrierCyl` solver used for gain calculations."
806
807 optical = attribute("EffectiveFrequencyCyl()")
808 ":class:`optical.effective.EffectiveFrequencyCyl` solver used for optical calculations."
809
810 tfreq = 6.0
811 """
812 Number of electrical iterations per single thermal step.
813
814 As temperature tends to converge faster, it is reasonable to repeat thermal
815 solution less frequently.
816 """
817
818 vmin = None
819 """
820 Minimum voltage to search threshold for.
821
822 It should be below the threshold.
823 """
824
825 vmax = None
826 """
827 Maximum voltage to search threshold for.
828
829 It should be above the threshold.
830 """
831
832 vtol = 1e-5
833 "Tolerance on voltage in the root search."
834
835 maxiter = 50
836 "Maximum number of root finding iterations."
837
838 maxlam = attribute("optical.lam0")
839 "Maximum wavelength considered for the optical mode search."
840
841 dlam = 0.02
842 """
843 Wavelength step.
844
845 Step, by which the wavelength is swept while searching for the approximate mode.
846 """
847
848 lpm = 0
849 """
850 Angular mode number $m$.
851
852 0 for LP0x, 1 for LP1x, etc.
853 """
854
855 lpn = 1
856 """
857 Radial mode number $n$.
858
859 1 for LPx1, 2 for LPx2, etc.
860 """
861
862 optical_resolution = (800, 600)
863 """
864 Number of points along the horizontal and vertical axes for the saved
865 and plotted optical field.
866 """
867
868 skip_thermal = False
869 """
870 Skip thermal computations.
871
872 The structure is assumed to have a constant temperature.
873 This can be used to look for the threshold under pulse laser operation.
874 """
875
876 def __init__(self, name=''):
877 from optical.effective import EffectiveFrequencyCyl
878 self._Optical_Optical = EffectiveFrequencyCyl
879 super().__init__(name)
880 self.maxlam = None
881
882 # def on_initialize(self):
883 # super().on_initialize()
884
885 def get_lam(self):
886 """
887 Get approximate wavelength for optical computations.
888
889 This method returns approximate wavelength for optical computations.
890 By default if browses the wavelength range starting from :attr:`maxlam`,
891 decreasing it by :attr:`dlam` until radial mode :attr:`lpn` is found.
892
893 You can override this method or set it to a a fixed value to use custom
894 mode approximation.
895
896 Example:
897 >>> solver = ThresholdSearchCyl()
898 >>> solver.get_lam = 980.
899 >>> solver.compute()
900 """
901
902 lam = self.maxlam if self.maxlam is not None else self.opticaloptical.lam0
903 n = 0
904 prev = 0.
905 decr = False
906 while n < self.lpnlpn and lam.real > 0.:
907 curr = abs(self.opticaloptical.get_determinant(lam=lam, m=self.lpmlpm))
908 if decr and curr > prev:
909 n += 1
910 decr = curr < prev
911 prev = curr
912 lam -= self.dlamdlam
913 if n == self.lpnlpn:
914 return lam + 2. * self.dlamdlam
915 raise ValueError("Approximation of mode LP{0.lpm}{0.lpn} not found".format(self))
916
917 def _optargs(self):
918 return dict(m=self.lpmlpm)
919
920 def _parse_xpl(self, tag, manager):
921 if tag == 'optical':
922 self._read_attr(tag, 'lam0', self.opticaloptical, float)
923 self._read_attr(tag, 'vlam', self.opticaloptical, float)
924 self._read_attr(tag, 'vat', self.opticaloptical, float)
925 self._read_attr(tag, 'emission', self.opticaloptical)
926 maxlam = tag.get('maxlam')
927 if maxlam is not None:
928 try:
929 self.maxlam = complex(maxlam)
930 except ValueError:
931 raise plask.XMLError("{}: attribute maxlam has illegal value '{}'".format(tag, maxlam))
932 self.dlamdlam = float(tag.get('dlam', self.dlamdlam))
933 self.lpmlpm = int(tag.get('m', self.lpmlpm))
934 self.lpnlpn = int(tag.get('n', self.lpnlpn))
935 else:
936 if tag == 'optical-root':
937 self._read_attr(tag, 'determinant', self.opticaloptical, str, 'determinant_mode')
938 super()._parse_xpl(tag, manager)
939
941 """
942 Function computing ‘vertical determinant’ of the optical solver.
943
944 Args:
945 vlam (float or array): ‘Vertical wavelength’ to compute the vertical
946 determinant for (nm).
947
948 Returns:
949 float or array: Optical vertical determinant.
950 """
952 if self._optarg is None:
953 return self.opticaloptical.get_determinant(vlam, **self._optargs_optargs())
954 else:
955 _optargs = self._optargs_optargs().copy()
956 _optargs[self._optarg] = vlam
957 return self.opticaloptical.get_determinant(**_optargs)
958
959 def plot_vert_optical_determinant(self, vlams, **kwargs):
960 """
961 Function plotting ‘vertical determinant’ of the optical solver.
962
963 Args:
964 vlams (array): ‘Vertical wavelengths’ to plot the determinant for (nm).
965
966 **kwargs: Keyword arguments passed to the plot function.
967 """
968 vals = self.get_vert_optical_determinant(vlams)
969 plask.plot(vlams, abs(vals))
970 plask.yscale('log')
971 plask.xlabel("Vertical Wavelength (nm)")
972 plask.ylabel("Determinant [ar.u.]")
973
974 def _get_info(self):
975 return super()._get_info() + [
976 "LP{}{} mode wavelength (nm): {:8.3f}".format(self.lpmlpm, self.lpnlpn, self.opticaloptical.modes[self.modeno].lam.real)
977 ]
978
979
981 """
982 Solver for threshold search of semiconductor laser with vector optical solver.
983
984 This solver performs thermo-electrical computations followed by
985 determination ot threshold current and optical analysis in order to
986 determine the threshold of a semiconductor laser. The search is
987 performed by ``scipy`` root finding algorithm in order to determine
988 the voltage and electric current ensuring no optical loss in the
989 laser cavity.
990
991 This solver uses vector optical solver :class:`~plask.optical.modal.BesselCyl`.
992
993 The computations can be executed using `compute` method, after which
994 the results may be save to the HDF5 file with `save` or presented visually
995 using ``plot_...`` methods. If ``save`` parameter of the :meth:`compute` method
996 is *True* the fields are saved automatically after the computations.
997 The file name is based on the name of the executed script with suffix denoting
998 either the launch time or the identifier of a batch job if a batch system
999 (like SLURM, OpenPBS, or SGE) is used.
1000 """
1001
1002 _Thermal = thermal.static.StaticCyl
1003 _Electrical = electrical.shockley.ShockleyCyl
1004 _Diffusion = electrical.diffusion.DiffusionCyl
1005 _Gain = gain.freecarrier.FreeCarrierCyl
1006
1007 _OPTICAL_ROOTS = {'optical-root': 'root'}
1008
1009 outTemperature = property(lambda self: self.thermalthermal.outTemperature, doc=_Thermal.outTemperature.__doc__)
1010 outHeatFlux = property(lambda self: self.thermalthermal.outHeatFlux, doc=_Thermal.outHeatFlux.__doc__)
1011
1012 outThermalConductivity = property(lambda self: self.thermalthermal.outThermalConductivity,
1013 doc=_Thermal.outThermalConductivity.__doc__)
1014 outVoltage = property(lambda self: self.electricalelectrical.outVoltage, doc=_Electrical.outVoltage.__doc__)
1015 outCurrentDensity = property(lambda self: self.electricalelectrical.outCurrentDensity,
1016 doc=_Electrical.outCurrentDensity.__doc__)
1017 outHeat = property(lambda self: self.electricalelectrical.outHeat, doc=_Electrical.outHeat.__doc__)
1018 outConductivity = property(lambda self: self.electricalelectrical.outConductivity, doc=_Electrical.outConductivity.__doc__)
1019 outCarriersConcentration = property(lambda self: self.diffusiondiffusion.outCarriersConcentration,
1020 doc=_Diffusion.outCarriersConcentration.__doc__)
1021 outGain = property(lambda self: self.gaingain.outGain, doc=_Gain.outGain.__doc__)
1022 outLightMagnitude = property(lambda self: self.opticaloptical.outLightMagnitude, doc=_doc.outLightMagnitude)
1023 outLoss = property(lambda self: self.opticaloptical.outLoss, doc=_doc.outLoss)
1024 outWavelength = property(lambda self: self.opticaloptical.outWavelength, doc=_doc.outWavelength)
1025 outEpsilon = property(lambda self: self.opticaloptical.outEpsilon, doc=_doc.outEpsilon)
1026 outLightE = property(lambda self: self.opticaloptical.outLightE, doc=_doc.outLightE)
1027
1028 thermal = attribute(_Thermal.__name__ + "()")
1029 ":class:`thermal.static.StaticCyl` solver used for thermal calculations."
1030
1031 electrical = attribute(_Electrical.__name__ + "()")
1032 ":class:`electrical.shockley.ShockleyCyl` solver used for electrical calculations."
1033
1034 diffusion = attribute(_Diffusion.__name__ + "()")
1035 ":class:`electrical.diffusion.DiffusionCyl` solver used for electrical calculations."
1036
1037 gain = attribute(_Gain.__name__ + "()")
1038 ":class:`gain.freecarrier.FreeCarrierCyl` solver used for gain calculations."
1039
1040 optical = attribute("BesselCyl()")
1041 ":class:`optical.modal.BesselFrequencyCyl` solver used for optical calculations."
1042
1043 tfreq = 6.0
1044 """
1045 Number of electrical iterations per single thermal step.
1046
1047 As temperature tends to converge faster, it is reasonable to repeat thermal
1048 solution less frequently.
1049 """
1050
1051 vmin = None
1052 """
1053 Minimum voltage to search threshold for.
1054
1055 It should be below the threshold.
1056 """
1057
1058 vmax = None
1059 """
1060 Maximum voltage to search threshold for.
1061
1062 It should be above the threshold.
1063 """
1064
1065 vtol = 1e-5
1066 "Tolerance on voltage in the root search."
1067
1068 maxiter = 50
1069 "Maximum number of root finding iterations."
1070
1071 maxlam = attribute("optical.lam0")
1072 "Maximum wavelength considered for the optical mode search."
1073
1074 dlam = 0.05
1075 """
1076 Wavelength step.
1077
1078 Step, by which the wavelength is swept while searching for the approximate mode.
1079 """
1080
1081 hem = 1
1082 """
1083 Angular mode number $m$.
1084
1085 1 for HE1x, 2 for HE2x, etc.
1086 """
1087
1088 hen = 1
1089 """
1090 Radial mode number $n$.
1091
1092 1 for HEx1, 2 for HEx2, etc.
1093 """
1094
1095 lam = None
1096 """
1097 Initial wavelength for optical search.
1098
1099 If this value is set, the computations are started from this value. If this
1100 value is set, the radial mode number :attr:`hen` is ignored.
1101
1102 Note that it is safer to leave this empty and allow the solver to look for it
1103 automatically, however, it may increase the time of optical computations.
1104 """
1105
1106 optical_resolution = (800, 600)
1107 """
1108 Number of points along the horizontal and vertical axes for the saved
1109 and plotted optical field.
1110 """
1111
1112 skip_thermal = False
1113 """
1114 Skip thermal computations.
1115
1116 The structure is assumed to have a constant temperature.
1117 This can be used to look for the threshold under pulse laser operation.
1118 """
1119
1120 def __init__(self, name=''):
1121 from optical.modal import BesselCyl
1122 self._Optical_Optical = BesselCyl
1123 super().__init__(name)
1124 self.maxlam = None
1125
1126 # def on_initialize(self):
1127 # super().on_initialize()
1128
1129 # def on_invalidate(self):
1130 # super().on_invalidate()
1131
1132 def get_lam(self):
1133 """
1134 Get approximate wavelength for optical computations.
1135
1136 This method returns approximate wavelength for optical computations.
1137 By default if browses the wavelength range starting from :attr:`maxlam`,
1138 decreasing it by :attr:`dlam` until radial mode :attr:`hen` is found.
1139
1140 You can override this method or set it to a a fixed value to use custom
1141 mode approximation.
1142
1143 Example:
1144 >>> solver = ThresholdSearchBesselCyl()
1145 >>> solver.get_lam = 980.
1146 >>> solver.compute()
1147 """
1148
1149 if self.lam is not None:
1150 return self.lam
1151
1152 lam = self.maxlam if self.maxlam is not None else self.opticaloptical.lam0
1153 n = 0
1154 prev = 0.
1155 decr = False
1156 while n < self.henhen and lam.real > 0.:
1157 curr = abs(self.opticaloptical.get_determinant(lam=lam, m=self.hemhem))
1158 if decr and curr > prev:
1159 n += 1
1160 decr = curr < prev
1161 prev = curr
1162 lam -= self.dlamdlam
1163 if n == self.henhen:
1164 return lam + 2. * self.dlamdlam
1165 raise ValueError("Approximation of mode HE{0.hem}{0.hen} not found".format(self))
1166
1167 def _optargs(self):
1168 return dict(m=self.hemhem)
1169
1170 def _parse_xpl(self, tag, manager):
1171 if tag == 'optical':
1172 self._read_attr(tag, 'lam0', self.opticaloptical, float)
1173 self._read_attr(tag, 'update-gain', self.opticaloptical, bool, 'update_gain')
1174 self._read_attr(tag, 'domain', self.opticaloptical)
1175 self._read_attr(tag, 'size', self.opticaloptical, int, 'size')
1176 self._read_attr(tag, 'group-layers', self.opticaloptical, bool, 'group_layers')
1177 self._read_attr(tag, 'k-method', self.opticaloptical, pyattr='kmethod')
1178 self._read_attr(tag, 'k-scale', self.opticaloptical, float, 'kscale')
1179 self._read_attr(tag, 'transfer', self.opticaloptical)
1180 self._read_attr(tag, 'emission', self.opticaloptical)
1181 maxlam = tag.get('maxlam')
1182 if maxlam is not None:
1183 try:
1184 self.maxlam = complex(maxlam)
1185 except ValueError:
1186 raise plask.XMLError("{}: attribute maxlam has illegal value '{}'".format(tag, maxlam))
1187 self.dlamdlam = float(tag.get('dlam', self.dlamdlam))
1188 self.lam = tag.get('lam', self.lam)
1189 if self.lam is not None: self.lam = complex(self.lam)
1190 self.hemhem = int(tag.get('m', self.hemhem))
1191 self.henhen = int(tag.get('n', self.henhen))
1192 elif tag == 'optical-interface':
1193 attrs = {key: val for (key, val) in ((key, tag.get(key)) for key in ('position', 'object', 'path'))
1194 if val is not None}
1195 if len(attrs) > 1 and (len(attrs) > 2 or 'position' in attrs):
1196 raise plask.XMLError("{}: conflicting attributes '{}'".format(tag, "' and '".join(attrs.keys())))
1197 elif 'position' in attrs:
1198 self.opticaloptical.set_interface(attrs['position'])
1199 elif 'object' in attrs:
1200 path = attrs.get('path')
1201 if path is not None:
1202 self.opticaloptical.set_interface(manager.geo[attrs['object']], manager.pth[path])
1203 else:
1204 self.opticaloptical.set_interface(manager.geo[attrs['object']])
1205 elif tag == 'optical-vpml':
1206 self._read_attr(tag, 'factor', self.opticaloptical.vpml, complex, 'factor')
1207 self._read_attr(tag, 'dist', self.opticaloptical.vpml, float, 'dist')
1208 self._read_attr(tag, 'size', self.opticaloptical.vpml, float, 'size')
1209 elif tag == 'optical-pml':
1210 self._read_attr(tag, 'factor', self.opticaloptical.pml, complex, 'factor')
1211 self._read_attr(tag, 'shape', self.opticaloptical.pml, float, 'shape')
1212 self._read_attr(tag, 'dist', self.opticaloptical.pml, float, 'dist')
1213 self._read_attr(tag, 'size', self.opticaloptical.pml, float, 'size')
1214 else:
1215 super()._parse_xpl(tag, manager)
1216
1217 def _get_info(self):
1218 return super()._get_info() + [
1219 "HE{}{} mode wavelength (nm): {:8.3f}".format(self.hemhem, self.henhen, self.opticaloptical.modes[self.modeno].lam.real)
1220 ]
1221
1222
1224 """
1225 Solver for threshold search of semiconductor laser.
1226
1227 This solver performs thermo-electrical computations followed by
1228 determination ot threshold current and optical analysis in order to
1229 determine the threshold of a semiconductor laser. The search is
1230 performed by ``scipy`` root finding algorithm in order to determine
1231 the voltage and electric current ensuring no optical loss in the
1232 laser cavity.
1233
1234 The computations can be executed using `compute` method, after which
1235 the results may be save to the HDF5 file with `save` or presented visually
1236 using ``plot_...`` methods. If ``save`` parameter of the :meth:`compute` method
1237 is *True* the fields are saved automatically after the computations.
1238 The file name is based on the name of the executed script with suffix denoting
1239 either the launch time or the identifier of a batch job if a batch system
1240 (like SLURM, OpenPBS, or SGE) is used.
1241 """
1242
1243 _optarg = 'neff'
1244 _lam0 = 'wavelength'
1245
1246 _Thermal = thermal.static.Static2D
1247 _Electrical = electrical.shockley.Shockley2D
1248 _Diffusion = electrical.diffusion.Diffusion2D
1249 _Gain = gain.freecarrier.FreeCarrier2D
1250
1251 _OPTICAL_ROOTS = {'optical-root': 'root', 'optical-stripe-root': 'stripe_root'}
1252
1253 outTemperature = property(lambda self: self.thermalthermal.outTemperature, doc=_Thermal.outTemperature.__doc__)
1254 outHeatFlux = property(lambda self: self.thermalthermal.outHeatFlux, doc=_Thermal.outHeatFlux.__doc__)
1255
1256 outThermalConductivity = property(lambda self: self.thermalthermal.outThermalConductivity,
1257 doc=_Thermal.outThermalConductivity.__doc__)
1258 outVoltage = property(lambda self: self.electricalelectrical.outVoltage, doc=_Electrical.outVoltage.__doc__)
1259 outCurrentDensity = property(lambda self: self.electricalelectrical.outCurrentDensity,
1260 doc=_Electrical.outCurrentDensity.__doc__)
1261 outHeat = property(lambda self: self.electricalelectrical.outHeat, doc=_Electrical.outHeat.__doc__)
1262 outConductivity = property(lambda self: self.electricalelectrical.outConductivity, doc=_Electrical.outConductivity.__doc__)
1263 outCarriersConcentration = property(lambda self: self.diffusiondiffusion.outCarriersConcentration,
1264 doc=_Diffusion.outCarriersConcentration.__doc__)
1265 outGain = property(lambda self: self.gaingain.outGain, doc=_Gain.outGain.__doc__)
1266 outLightMagnitude = property(lambda self: self.opticaloptical.outLightMagnitude, doc=_doc.outLightMagnitude)
1267 outLoss = property(lambda self: self.opticaloptical.outLoss, doc=_doc.outLoss)
1268 outNeff = property(lambda self: self.opticaloptical.outNeff, doc=_doc.outNeff)
1269 outRefractiveIndex = property(lambda self: self.opticaloptical.outRefractiveIndex, doc=_doc.outRefractiveIndex)
1270 outLightE = property(lambda self: self.opticaloptical.outLightE, doc=_doc.outLightE)
1271
1272 thermal = attribute(_Thermal.__name__+"()")
1273 ":class:`thermal.static.Static2D` solver used for thermal calculations."
1274
1275 electrical = attribute(_Electrical.__name__+"()")
1276 ":class:`electrical.shockley.Shockley2D` solver used for electrical calculations."
1277
1278 diffusion = attribute(_Diffusion.__name__+"()")
1279 ":class:`electrical.diffusion.Diffusion2D` solver used for electrical calculations."
1280
1281 gain = attribute(_Gain.__name__+"()")
1282 ":class:`gain.freecarrier.FreeCarrier2D` solver used for gain calculations."
1283
1284 optical = attribute("EffectiveIndex2D()")
1285 ":class:`optical.effective.EffectiveIndex2D` solver used for optical calculations."
1286
1287 tfreq = 6.0
1288 """
1289 Number of electrical iterations per single thermal step.
1290
1291 As temperature tends to converge faster, it is reasonable to repeat thermal
1292 solution less frequently.
1293 """
1294
1295 vmin = None
1296 """
1297 Minimum voltage to search threshold for.
1298
1299 It should be below the threshold.
1300 """
1301
1302 vmax = None
1303 """
1304 Maximum voltage to search threshold for.
1305
1306 It should be above the threshold.
1307 """
1308
1309 vtol = 1e-5
1310 "Tolerance on voltage in the root search."
1311
1312 maxiter = 50
1313 "Maximum number of root finding iterations."
1314
1315 wavelength = None
1316 "Emission wavelength (nm)."
1317
1318 dneff = 0.02
1319 """
1320 Effective index step.
1321
1322 Step, by which the effective index is swept while searching for the approximate mode.
1323 """
1324
1325 mn = 1
1326 """
1327 Lateral mode number $n$.
1328 """
1329
1330 optical_resolution = (800, 600)
1331 """
1332 Number of points along the horizontal and vertical axes for the saved
1333 and plotted optical field.
1334 """
1335
1336 skip_thermal = False
1337 """
1338 Skip thermal computations.
1339
1340 The structure is assumed to have a constant temperature.
1341 This can be used to look for the threshold under pulse laser operation.
1342 """
1343
1344 def __init__(self, name=''):
1345 from optical.effective import EffectiveIndex2D
1346 self._Optical_Optical = EffectiveIndex2D
1347 super().__init__(name)
1348
1349 def on_initialize(self):
1350 super().on_initialize()
1351 points = plask.mesh.Rectangular2D.SimpleGenerator()(self.opticaloptical.geometry).elements.mesh
1352 self._maxneff = max(self.opticaloptical.geometry.get_material(point).Nr(self.opticaloptical.wavelength.real).real
1353 for point in points)
1354
1355 def get_nng(self):
1356 """
1357 Get approximate effective index for optical computations.
1358
1359 This method returns approximate wavelength for optical computations.
1360 By default if browses the wavelength range starting from :attr:`maxneff`,
1361 decreasing it by :attr:`dneff` until lateral mode :attr:`mn` is found.
1362
1363 You can override this method to use custom mode approximation.
1364
1365 Example:
1366 >>> solver = ThresholdSearch2D()
1367 >>> solver.get_nng = lambda: 3.5
1368 >>> solver.compute()
1369 """
1370
1371 neff = self._maxneff
1372 n = 0
1373 prev = 0.
1374 decr = False
1375 while n < self.mnmn and neff.real > 0.:
1376 curr = abs(self.opticaloptical.get_determinant(neff))
1377 if decr and curr > prev:
1378 n += 1
1379 decr = curr < prev
1380 prev = curr
1381 neff -= self.dneffdneff
1382 if n == self.mnmn:
1383 return neff + 2. * self.dneffdneff
1384 raise ValueError("Mode approximation not found")
1385
1386 def _parse_xpl(self, tag, manager):
1387 if tag == 'optical':
1388 self._read_attr(tag, 'lam', self.opticaloptical, float)
1389 self._read_attr(tag, 'polarization', self.opticaloptical, float)
1390 self._read_attr(tag, 'vneff', self.opticaloptical, float)
1391 self._read_attr(tag, 'vat', self.opticaloptical, float)
1392 self._read_attr(tag, 'emission', self.opticaloptical)
1393 self.dneffdneff = float(tag.get('dneff', self.dneffdneff))
1394 self.mnmn = int(tag.get('mn', self.mnmn))
1395 else:
1396 if tag == 'optical-root':
1397 self._read_attr(tag, 'determinant', self.opticaloptical, str)
1398 super()._parse_xpl(tag, manager)
1399
1401 """
1402 Function computing determinant of the optical solver.
1403
1404 Args:
1405 neff (float or array): Effective index to compute the determinant for.
1406
1407 Returns:
1408 float or array: Optical determinant.
1409 """
1411 if self._optarg_optarg is None:
1412 return self.opticaloptical.get_determinant(neff, **self._optargs())
1413 else:
1414 _optargs = self._optargs().copy()
1415 _optargs[self._optarg_optarg] = neff
1416 return self.opticaloptical.get_determinant(**_optargs)
1417
1418 def plot_optical_determinant(self, neffs, **kwargs):
1419 """
1420 Function plotting determinant of the optical solver.
1421
1422 Args:
1423 neffs (array): Array of effective indices to plot the determinant for.
1424
1425 **kwargs: Keyword arguments passed to the plot function.
1426 """
1428 plask.plot(neffs, abs(vals))
1429 plask.yscale('log')
1430 plask.xlabel("Effective Index")
1431 plask.ylabel("Determinant [ar.u.]")
1432
1434 """
1435 Function computing ‘vertical determinant’ of the optical solver.
1436
1437 Args:
1438 vneff (float or array): Effective index to compute the vertical
1439 determinant for.
1440
1441 Returns:
1442 float or array: Optical determinant.
1443 """
1445 if self._optarg_optarg is None:
1446 return self.opticaloptical.get_determinant(vneff, **self._optargs())
1447 else:
1448 _optargs = self._optargs().copy()
1449 _optargs[self._optarg_optarg] = vneff
1450 return self.opticaloptical.get_determinant(**_optargs)
1451
1452 def plot_vert_optical_determinant(self, vneffs, **kwargs):
1453 """
1454 Function plotting ‘vertical determinant’ of the optical solver.
1455
1456 Args:
1457 vneffs (array): Array of effective indices to plot the vertical
1458 determinant for.
1459
1460 **kwargs: Keyword arguments passed to the plot function.
1461 """
1462 vals = self.get_vert_optical_determinant(vneffs)
1463 plask.plot(vneffs, abs(vals))
1464 plask.yscale('log')
1465 plask.xlabel("Vertical Effective Index")
1466 plask.ylabel("Determinant [ar.u.]")
1467
1468 def _get_info(self):
1469 return super()._get_info() + [
1470 "Effective index: {:8.3f}".format(self.opticaloptical.modes[self.modeno].neff.real)
1471 ]
1472
1473
1475 """
1476 Solver for threshold search of semiconductor laser.
1477
1478 This solver performs thermo-electrical computations followed by
1479 determination ot threshold current and optical analysis in order to
1480 determine the threshold of a semiconductor laser. The search is
1481 performed by ``scipy`` root finding algorithm in order to determine
1482 the voltage and electric current ensuring no optical loss in the
1483 laser cavity.
1484
1485 This solver uses vector optical solver :class:`~plask.optical.modal.Fourier2D`.
1486
1487 The computations can be executed using `compute` method, after which
1488 the results may be save to the HDF5 file with `save` or presented visually
1489 using ``plot_...`` methods. If ``save`` parameter of the :meth:`compute` method
1490 is *True* the fields are saved automatically after the computations.
1491 The file name is based on the name of the executed script with suffix denoting
1492 either the launch time or the identifier of a batch job if a batch system
1493 (like SLURM, OpenPBS, or SGE) is used.
1494 """
1495
1496 _optarg = 'neff'
1497 _lam0 = 'lam'
1498
1499 _Thermal = thermal.static.Static2D
1500 _Electrical = electrical.shockley.Shockley2D
1501 _Diffusion = electrical.diffusion.Diffusion2D
1502 _Gain = gain.freecarrier.FreeCarrier2D
1503
1504 _OPTICAL_ROOTS = {'optical-root': 'root'}
1505
1506 outTemperature = property(lambda self: self.thermalthermal.outTemperature, doc=_Thermal.outTemperature.__doc__)
1507 outHeatFlux = property(lambda self: self.thermalthermal.outHeatFlux, doc=_Thermal.outHeatFlux.__doc__)
1508
1509 outThermalConductivity = property(lambda self: self.thermalthermal.outThermalConductivity,
1510 doc=_Thermal.outThermalConductivity.__doc__)
1511 outVoltage = property(lambda self: self.electricalelectrical.outVoltage, doc=_Electrical.outVoltage.__doc__)
1512 outCurrentDensity = property(lambda self: self.electricalelectrical.outCurrentDensity,
1513 doc=_Electrical.outCurrentDensity.__doc__)
1514 outHeat = property(lambda self: self.electricalelectrical.outHeat, doc=_Electrical.outHeat.__doc__)
1515 outConductivity = property(lambda self: self.electricalelectrical.outConductivity, doc=_Electrical.outConductivity.__doc__)
1516 outCarriersConcentration = property(lambda self: self.diffusiondiffusion.outCarriersConcentration,
1517 doc=_Diffusion.outCarriersConcentration.__doc__)
1518 outGain = property(lambda self: self.gaingain.outGain, doc=_Gain.outGain.__doc__)
1519 outLightMagnitude = property(lambda self: self.opticaloptical.outLightMagnitude, doc=_doc.outLightMagnitude)
1520 outNeff = property(lambda self: self.opticaloptical.outNeff, doc=_doc.outNeff)
1521 outEpsilon = property(lambda self: self.opticaloptical.outEpsilon, doc=_doc.outEpsilon)
1522 outLightE = property(lambda self: self.opticaloptical.outLightE, doc=_doc.outLightE)
1523
1524 thermal = attribute(_Thermal.__name__+"()")
1525 ":class:`thermal.static.Static2D` solver used for thermal calculations."
1526
1527 electrical = attribute(_Electrical.__name__+"()")
1528 ":class:`electrical.shockley.Shockley2D` solver used for electrical calculations."
1529
1530 diffusion = attribute(_Diffusion.__name__+"()")
1531 ":class:`electrical.diffusion.Diffusion2D` solver used for electrical calculations."
1532
1533 gain = attribute(_Gain.__name__+"()")
1534 ":class:`gain.freecarrier.FreeCarrier2D` solver used for gain calculations."
1535
1536 optical = attribute("Fourier2D()")
1537 ":class:`optical.modal.Fourier2D` solver used for optical calculations."
1538
1539 tfreq = 6.0
1540 """
1541 Number of electrical iterations per single thermal step.
1542
1543 As temperature tends to converge faster, it is reasonable to repeat thermal
1544 solution less frequently.
1545 """
1546
1547 vmin = None
1548 """
1549 Minimum voltage to search threshold for.
1550
1551 It should be below the threshold.
1552 """
1553
1554 vmax = None
1555 """
1556 Maximum voltage to search threshold for.
1557
1558 It should be above the threshold.
1559 """
1560
1561 vtol = 1e-5
1562 "Tolerance on voltage in the root search."
1563
1564 maxiter = 50
1565 "Maximum number of root finding iterations."
1566
1567 wavelength = None
1568 "Emission wavelength (nm)."
1569
1570 dneff = 0.02
1571 """
1572 Effective index step.
1573
1574 Step, by which the effective index is swept while searching for the approximate mode.
1575 """
1576
1577 mn = 1
1578 """
1579 Lateral mode number $n$.
1580 """
1581
1582 optical_resolution = (800, 600)
1583 """
1584 Number of points along the horizontal and vertical axes for the saved
1585 and plotted optical field.
1586 """
1587
1588 skip_thermal = False
1589 """
1590 Skip thermal computations.
1591
1592 The structure is assumed to have a constant temperature.
1593 This can be used to look for the threshold under pulse laser operation.
1594 """
1595
1596 def __init__(self, name=''):
1597 from optical.modal import Fourier2D
1598 self._Optical_Optical = Fourier2D
1599 super().__init__(name)
1600
1601 def on_initialize(self):
1602 super().on_initialize()
1603 points = plask.mesh.Rectangular2D.SimpleGenerator()(self.opticaloptical.geometry).elements.mesh
1604 self._maxneff = max(self.opticaloptical.geometry.get_material(point).Nr(self.opticaloptical.wavelength.real).real
1605 for point in points)
1606
1607 def get_nng(self):
1608 """
1609 Get approximate effective index for optical computations.
1610
1611 This method returns approximate wavelength for optical computations.
1612 By default if browses the wavelength range starting from :attr:`maxneff`,
1613 decreasing it by :attr:`dneff` until lateral mode :attr:`mn` is found.
1614
1615 You can override this method to use custom mode approximation.
1616
1617 Example:
1618 >>> solver = ThresholdSearchFourier2D()
1619 >>> solver.get_nng = lambda: 3.5
1620 >>> solver.compute()
1621 """
1622
1623 neff = self._maxneff
1624 n = 0
1625 prev = 0.
1626 decr = False
1627 while n < self.mnmn and neff.real > 0.:
1628 curr = abs(self.opticaloptical.get_determinant(neff=neff))
1629 if decr and curr > prev:
1630 n += 1
1631 decr = curr < prev
1632 prev = curr
1633 neff -= self.dneffdneff
1634 if n == self.mnmn:
1635 return neff + 2. * self.dneffdneff
1636 raise ValueError("Mode approximation not found")
1637
1638 def _parse_xpl(self, tag, manager):
1639 if tag == 'optical':
1640 self._read_attr(tag, 'size', self.opticaloptical, int)
1641 self._read_attr(tag, 'refine', self.opticaloptical, int)
1642 self._read_attr(tag, 'smooth', self.opticaloptical, float)
1643 self._read_attr(tag, 'symmetry', self.opticaloptical)
1644 self._read_attr(tag, 'group-layers', self.opticaloptical, bool, 'group_layers')
1645 self._read_attr(tag, 'transfer', self.opticaloptical)
1646 self._read_attr(tag, 'lam', self.opticaloptical, float)
1647 self.dneffdneff = float(tag.get('dneff', self.dneffdneff))
1648 self.mnmn = int(tag.get('mn', self.mnmn))
1649 elif tag == 'optical-interface':
1650 attrs = {key: val for (key, val) in ((key, tag.get(key)) for key in ('position', 'object', 'path'))
1651 if val is not None}
1652 if len(attrs) > 1 and (len(attrs) > 2 or 'position' in attrs):
1653 raise plask.XMLError("{}: conflicting attributes '{}'".format(tag, "' and '".join(attrs.keys())))
1654 elif 'position' in attrs:
1655 self.opticaloptical.set_interface(attrs['position'])
1656 elif 'object' in attrs:
1657 path = attrs.get('path')
1658 if path is not None:
1659 self.opticaloptical.set_interface(manager.geo[attrs['object']], manager.pth[path])
1660 else:
1661 self.opticaloptical.set_interface(manager.geo[attrs['object']])
1662 elif tag == 'optical-vpml':
1663 self._read_attr(tag, 'factor', self.opticaloptical.vpml, complex, 'factor')
1664 self._read_attr(tag, 'dist', self.opticaloptical.vpml, float, 'dist')
1665 self._read_attr(tag, 'size', self.opticaloptical.vpml, float, 'size')
1666 elif tag == 'optical-pml':
1667 self._read_attr(tag, 'factor', self.opticaloptical.pml, complex, 'factor')
1668 self._read_attr(tag, 'shape', self.opticaloptical.pml, float, 'shape')
1669 self._read_attr(tag, 'dist', self.opticaloptical.pml, float, 'dist')
1670 self._read_attr(tag, 'size', self.opticaloptical.pml, float, 'size')
1671 else:
1672 super()._parse_xpl(tag, manager)
1673
1675 """
1676 Function computing determinant of the optical solver.
1677
1678 Args:
1679 neff (float or array): Effective index to compute the determinant for.
1680
1681 Returns:
1682 float or array: Optical determinant.
1683 """
1685 if self._optarg_optarg is None:
1686 return self.opticaloptical.get_determinant(neff, **self._optargs())
1687 else:
1688 _optargs = self._optargs().copy()
1689 _optargs[self._optarg_optarg] = neff
1690 return self.opticaloptical.get_determinant(**_optargs)
1691
1692 def plot_optical_determinant(self, neffs, **kwargs):
1693 """
1694 Function plotting determinant of the optical solver.
1695
1696 Args:
1697 neffs (array): Array of effective indices to plot the determinant for.
1698
1699 **kwargs: Keyword arguments passed to the plot function.
1700 """
1702 plask.plot(neffs, abs(vals))
1703 plask.yscale('log')
1704 plask.xlabel("Effective Index")
1705 plask.ylabel("Determinant [ar.u.]")
1706
1707 def plot_vert_optical_determinant(self, vneffs, **kwargs):
1708 """
1709 Function plotting ‘vertical determinant’ of the optical solver.
1710
1711 Args:
1712 vneffs (array): Array of effective indices to plot the vertical
1713 determinant for.
1714
1715 **kwargs: Keyword arguments passed to the plot function.
1716 """
1717 vals = self.get_vert_optical_determinant(vneffs)
1718 plask.plot(vneffs, abs(vals))
1719 plask.yscale('log')
1720 plask.xlabel("Vertical Effective Index")
1721 plask.ylabel("Determinant [ar.u.]")
1722
1723 def _get_info(self):
1724 return super()._get_info() + [
1725 "Effective index: {:8.3f}".format(self.opticaloptical.modes[self.modeno].neff.real)
1726 ]
1727
1728
1729__all__ = 'ThresholdSearchCyl', 'ThresholdSearchBesselCyl', 'ThresholdSearch2D', 'ThresholdSearchFourier2D'