freepaths 2.3.4__tar.gz → 3.4__tar.gz

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.
Files changed (44) hide show
  1. {freepaths-2.3.4 → freepaths-3.4}/PKG-INFO +1 -1
  2. {freepaths-2.3.4 → freepaths-3.4}/freepaths/__main__.py +1 -1
  3. {freepaths-2.3.4 → freepaths-3.4}/freepaths/config.py +16 -2
  4. {freepaths-2.3.4 → freepaths-3.4}/freepaths/data.py +14 -4
  5. freepaths-3.4/freepaths/default_config.py +164 -0
  6. freepaths-3.4/freepaths/electron.py +149 -0
  7. {freepaths-2.3.4 → freepaths-3.4}/freepaths/flight.py +34 -15
  8. {freepaths-2.3.4 → freepaths-3.4}/freepaths/main_mfp_sampling.py +57 -7
  9. {freepaths-2.3.4 → freepaths-3.4}/freepaths/main_tracing.py +16 -6
  10. {freepaths-2.3.4 → freepaths-3.4}/freepaths/maps.py +30 -11
  11. {freepaths-2.3.4 → freepaths-3.4}/freepaths/materials.py +206 -72
  12. {freepaths-2.3.4 → freepaths-3.4}/freepaths/output_electron_plots.py +13 -0
  13. {freepaths-2.3.4 → freepaths-3.4}/freepaths/output_info.py +32 -15
  14. {freepaths-2.3.4 → freepaths-3.4}/freepaths/output_plots.py +74 -41
  15. freepaths-3.4/freepaths/phonon.py +167 -0
  16. {freepaths-2.3.4 → freepaths-3.4}/freepaths/post_computations.py +14 -13
  17. {freepaths-2.3.4 → freepaths-3.4}/freepaths/run_particle.py +31 -6
  18. {freepaths-2.3.4 → freepaths-3.4}/freepaths/scatterers.py +60 -6
  19. {freepaths-2.3.4 → freepaths-3.4}/freepaths/scattering.py +24 -19
  20. {freepaths-2.3.4 → freepaths-3.4}/freepaths.egg-info/PKG-INFO +1 -1
  21. freepaths-2.3.4/freepaths/default_config.py +0 -97
  22. freepaths-2.3.4/freepaths/electron.py +0 -75
  23. freepaths-2.3.4/freepaths/phonon.py +0 -120
  24. {freepaths-2.3.4 → freepaths-3.4}/README.md +0 -0
  25. {freepaths-2.3.4 → freepaths-3.4}/freepaths/animation.py +0 -0
  26. {freepaths-2.3.4 → freepaths-3.4}/freepaths/interface_smmm.py +0 -0
  27. {freepaths-2.3.4 → freepaths-3.4}/freepaths/move.py +0 -0
  28. {freepaths-2.3.4 → freepaths-3.4}/freepaths/options.py +0 -0
  29. {freepaths-2.3.4 → freepaths-3.4}/freepaths/output_structure.py +0 -0
  30. {freepaths-2.3.4 → freepaths-3.4}/freepaths/particle.py +0 -0
  31. {freepaths-2.3.4 → freepaths-3.4}/freepaths/progress.py +0 -0
  32. {freepaths-2.3.4 → freepaths-3.4}/freepaths/scattering_interfaces.py +0 -0
  33. {freepaths-2.3.4 → freepaths-3.4}/freepaths/scattering_primitives.py +0 -0
  34. {freepaths-2.3.4 → freepaths-3.4}/freepaths/scattering_semicircle.py +0 -0
  35. {freepaths-2.3.4 → freepaths-3.4}/freepaths/scattering_types.py +0 -0
  36. {freepaths-2.3.4 → freepaths-3.4}/freepaths/sources.py +0 -0
  37. {freepaths-2.3.4 → freepaths-3.4}/freepaths.egg-info/SOURCES.txt +0 -0
  38. {freepaths-2.3.4 → freepaths-3.4}/freepaths.egg-info/dependency_links.txt +0 -0
  39. {freepaths-2.3.4 → freepaths-3.4}/freepaths.egg-info/entry_points.txt +0 -0
  40. {freepaths-2.3.4 → freepaths-3.4}/freepaths.egg-info/requires.txt +0 -0
  41. {freepaths-2.3.4 → freepaths-3.4}/freepaths.egg-info/top_level.txt +0 -0
  42. {freepaths-2.3.4 → freepaths-3.4}/pyproject.toml +0 -0
  43. {freepaths-2.3.4 → freepaths-3.4}/setup.cfg +0 -0
  44. {freepaths-2.3.4 → freepaths-3.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freepaths
3
- Version: 2.3.4
3
+ Version: 3.4
4
4
  Summary: Phonon Monte Carlo simulator
5
5
  Home-page: https://github.com/anufrievroman/freepaths
6
6
  Author: Roman Anufriev
@@ -7,7 +7,7 @@ from colorama import Fore, Style
7
7
 
8
8
  from freepaths.options import SimulationMode
9
9
 
10
- __version__ = "2.3.4"
10
+ __version__ = "3.4"
11
11
 
12
12
  colorama.init()
13
13
 
@@ -71,6 +71,11 @@ class Config:
71
71
  self.energy_lower_bound = ENERGY_LOWER_BOUND
72
72
  self.energy_step = ENERGY_STEP
73
73
  self.electron_mfp = ELECTRON_MFP
74
+ self.doping_concentration = DOPING_CONCENTRATION
75
+ self.surface_potential = SURFACE_POTENTIAL
76
+ self.depletion_doping = DEPLETION_DOPING # doping for the dead-layer width only (falls back to DOPING_CONCENTRATION)
77
+ self.depletion_width = 0.0 # [m] carrier dead-layer width; set from the material at simulation start
78
+ self.electron_mfp_energy_exponent = ELECTRON_MFP_ENERGY_EXPONENT
74
79
  self.mean_mapping_constant = MEAN_MAPPING_CONSTANT
75
80
  self.is_carrier_electron = IS_CARRIER_ELECTRON
76
81
  self.fermi_level_lower_bound = FERMI_LEVEL_LOWER_BOUND
@@ -85,6 +90,7 @@ class Config:
85
90
  self.number_of_pixels_y = NUMBER_OF_PIXELS_Y
86
91
  self.number_of_virtual_timesteps = NUMBER_OF_VIRTUAL_TIMESTEPS
87
92
  self.ignore_faulty_particles = IGNORE_FAULTY_PARTICLES
93
+ self.gradient_fit_range = GRADIENT_FIT_RANGE
88
94
 
89
95
  # Material parameters:
90
96
  self.media = MEDIA
@@ -92,8 +98,11 @@ class Config:
92
98
 
93
99
  # Internal scattering:
94
100
  self.include_internal_scattering = INCLUDE_INTERNAL_SCATTERING
95
- self.use_gray_approximation_mfp = USE_GRAY_APPROXIMATION_MFP
96
- self.gray_approximation_mfp = GRAY_APPROXIMATION_MFP
101
+ self.use_dispersion_heat_capacity = USE_DISPERSION_HEAT_CAPACITY
102
+ self.max_number_of_scattering_events = MAX_NUMBER_OF_SCATTERING_EVENTS
103
+ self.grain_size = GRAIN_SIZE
104
+ self.grain_size_std = GRAIN_SIZE_STD
105
+ self.grain_roughness = GRAIN_ROUGHNESS
97
106
 
98
107
  # System dimensions:
99
108
  self.thickness = THICKNESS
@@ -157,6 +166,11 @@ class Config:
157
166
  if self.number_of_particles < self.output_trajectories_of_first:
158
167
  self.output_trajectories_of_first = self.number_of_particles
159
168
 
169
+ if not (0.0 <= self.gradient_fit_range[0] < self.gradient_fit_range[1] <= 1.0):
170
+ logging.error("Parameter GRADIENT_FIT_RANGE must be a pair (start, end) with 0 <= start < end <= 1.\n" +
171
+ f"See the documentation at {WEBSITE}")
172
+ sys.exit()
173
+
160
174
  if self.number_of_timeframes <= self.number_of_stabilization_timeframes:
161
175
  logging.error("Parameter NUMBER_OF_STABILIZATION_TIMEFRAMES exceeds or equal to NUMBER_OF_TIMEFRAMES.\n" +
162
176
  "Leave at least one timeframe after NUMBER_OF_STABILIZATION_TIMEFRAMES.\n" +
@@ -57,10 +57,14 @@ class GeneralData(Data):
57
57
  """Initialize arrays for writing various properties"""
58
58
  self.initial_angles = []
59
59
  self.exit_angles = []
60
+ self.launch_frequencies = []
61
+ self.exit_frequencies = []
60
62
  self.hole_diff_scattering_angles = []
61
63
  self.hole_spec_scattering_angles = []
62
64
  self.free_paths = []
63
- self.frequencies = []
65
+ # Last-known frequency of each particle, paired with group_velocities below
66
+ # (used for the frequency-velocity/MFP correlation plots, not a true launch value):
67
+ self.final_frequencies = []
64
68
  self.initial_energies = []
65
69
  self.group_velocities = []
66
70
  self.travel_times = []
@@ -72,7 +76,7 @@ class GeneralData(Data):
72
76
  if mode is SimulationMode.ELECTRON:
73
77
  self.initial_energies.append(pt.energy)
74
78
  else:
75
- self.frequencies.append(pt.f)
79
+ self.final_frequencies.append(pt.f)
76
80
  self.group_velocities.append(pt.speed)
77
81
 
78
82
  def save_flight_data(self, flight, mode=None):
@@ -83,6 +87,8 @@ class GeneralData(Data):
83
87
  if mode is SimulationMode.PHONON_TRACING:
84
88
  self.initial_angles.append(flight.initial_theta)
85
89
  self.exit_angles.append(flight.exit_theta)
90
+ self.launch_frequencies.append(flight.initial_frequency)
91
+ self.exit_frequencies.append(flight.exit_frequency)
86
92
  self.hole_diff_scattering_angles.extend(flight.hole_diff_scattering_angles)
87
93
  self.hole_spec_scattering_angles.extend(flight.hole_spec_scattering_angles)
88
94
  self.travel_times.append(flight.travel_time)
@@ -101,11 +107,13 @@ class GeneralData(Data):
101
107
  if mode is SimulationMode.ELECTRON:
102
108
  np.savetxt("Data/All initial energies.csv", self.initial_energies, fmt='%2.4e', header="Energy [J]", encoding='utf-8')
103
109
  else:
104
- np.savetxt("Data/All initial frequencies.csv", self.frequencies, fmt='%2.4e', header="f [Hz]", encoding='utf-8')
110
+ np.savetxt("Data/All final frequencies.csv", self.final_frequencies, fmt='%2.4e', header="f [Hz]", encoding='utf-8')
105
111
  np.savetxt("Data/All group velocities.csv", self.group_velocities, fmt='%.4f', header="Vg [m//s]", encoding='utf-8')
106
112
  if mode is SimulationMode.PHONON_TRACING:
107
113
  np.savetxt("Data/All exit angles.csv", self.exit_angles, fmt='%.4f', header="Angle [rad]", encoding='utf-8')
108
114
  np.savetxt("Data/All initial angles.csv", self.initial_angles, fmt='%.4f', header="Angle [rad]", encoding='utf-8')
115
+ np.savetxt("Data/All launch frequencies.csv", self.launch_frequencies, fmt='%2.4e', header="f [Hz]", encoding='utf-8')
116
+ np.savetxt("Data/All exit frequencies.csv", self.exit_frequencies, fmt='%2.4e', header="f [Hz]", encoding='utf-8')
109
117
  if cf.holes:
110
118
  np.savetxt("Data/All hole diffuse scattering angles.csv", self.hole_diff_scattering_angles, fmt='%.4f', header="Angle [rad]", encoding='utf-8')
111
119
  np.savetxt("Data/All hole specular scattering angles.csv", self.hole_spec_scattering_angles, fmt='%.4f', header="Angle [rad]", encoding='utf-8')
@@ -118,10 +126,12 @@ class GeneralData(Data):
118
126
  return {
119
127
  'initial_angles': self.initial_angles,
120
128
  'exit_angles': self.exit_angles,
129
+ 'launch_frequencies': self.launch_frequencies,
130
+ 'exit_frequencies': self.exit_frequencies,
121
131
  'hole_diff_scattering_angles': self.hole_diff_scattering_angles,
122
132
  'hole_spec_scattering_angles': self.hole_spec_scattering_angles,
123
133
  'free_paths': self.free_paths,
124
- 'frequencies': self.frequencies,
134
+ 'final_frequencies': self.final_frequencies,
125
135
  'group_velocities': self.group_velocities,
126
136
  'travel_times': self.travel_times,
127
137
  'mean_free_paths': self.mean_free_paths,
@@ -0,0 +1,164 @@
1
+ """Default config file"""
2
+
3
+ import numpy as np
4
+ from scipy.constants import k, electron_volt
5
+ from freepaths.sources import Source
6
+
7
+
8
+ # General parameters:
9
+ OUTPUT_FOLDER_NAME = "Si nanowire at 300 K"
10
+ NUMBER_OF_PARTICLES = 10000
11
+ NUMBER_OF_NODES = 400
12
+ T = 300
13
+ OUTPUT_SCATTERING_MAP = False
14
+ OUTPUT_TRAJECTORIES_OF_FIRST = 20
15
+ OUTPUT_STRUCTURE_COLOR = "#F0F0F0"
16
+ NUMBER_OF_LENGTH_SEGMENTS = 10
17
+ LOW_MEMORY_USAGE = False
18
+
19
+ # Time parameters:
20
+ TIMESTEP = 2e-12
21
+ NUMBER_OF_TIMESTEPS = 200000
22
+ NUMBER_OF_VIRTUAL_TIMESTEPS = NUMBER_OF_TIMESTEPS * 3
23
+ NUMBER_OF_TIMEFRAMES = 8
24
+ NUMBER_OF_STABILIZATION_TIMEFRAMES = 5
25
+
26
+ # Electron parameters [eV]
27
+ IS_CARRIER_ELECTRON = True
28
+ ENERGY_UPPER_BOUND = 350e-3
29
+ ENERGY_LOWER_BOUND = 0
30
+ ENERGY_STEP = 5e-3
31
+
32
+ ELECTRON_MFP = 10e-9 # [m] crystal (acoustic-phonon-limited) MFP, energy-independent
33
+ # Ionized-impurity scattering: if DOPING_CONCENTRATION > 0, the MFP becomes energy-
34
+ # and doping-dependent via Matthiessen's rule, 1/Lambda_tot(E) = 1/ELECTRON_MFP +
35
+ # 1/Lambda_ii(E), where Lambda_ii is the Brooks-Herring contribution from N_I ionized
36
+ # dopants (screened Coulomb scattering). This shortens the MFP with doping and raises
37
+ # the Seebeck coefficient. 0 -> pure crystal MFP.
38
+ DOPING_CONCENTRATION = 0.0 # [m^-3] ionized-impurity (dopant) concentration N_I
39
+ # Surface depletion of carriers: with SURFACE_POTENTIAL > 0, a dead layer of width
40
+ # W = sqrt(2*eps_s*phi_s/(e*N_D)) (abrupt-junction depletion) is excluded around every
41
+ # free surface (walls, top/bottom, hole edges) for CARRIERS ONLY. phi_s is the surface
42
+ # band bending set by Fermi-level pinning at the native oxide / surface states. Needs
43
+ # a doping (DEPLETION_DOPING or DOPING_CONCENTRATION) and the material dielectric
44
+ # constant. 0 -> no depletion.
45
+ SURFACE_POTENTIAL = 0.0 # [eV] surface band bending phi_s (0 = off)
46
+ # Doping used ONLY for the depletion-width formula. If 0/None it falls back to
47
+ # DOPING_CONCENTRATION. Set this (and leave DOPING_CONCENTRATION = 0) to add a surface
48
+ # dead layer from the real doping WITHOUT engaging Brooks-Herring, i.e. to isolate the
49
+ # depletion effect on top of a constant-MFP transport calibration.
50
+ DEPLETION_DOPING = 0.0 # [m^-3] dopant concentration for the dead-layer width only
51
+ # Legacy phenomenological energy dependence (only used when DOPING_CONCENTRATION = 0):
52
+ # Lambda(E) = ELECTRON_MFP * (E/kT)^p. p=0 -> constant; p=2 -> ionized-impurity-like.
53
+ ELECTRON_MFP_ENERGY_EXPONENT = 0.0
54
+ MEAN_MAPPING_CONSTANT = 5e-6 # [m²]
55
+
56
+ FERMI_LEVEL_LOWER_BOUND = -0.2 # [eV] lower end of post-processing Fermi level sweep
57
+ FERMI_LEVEL_UPPER_BOUND = 0.1 # [eV] upper end of post-processing Fermi level sweep;
58
+ # far above the band edge the Fermi window leaves the sampled energy range and
59
+ # the MC results become noisy, so there is little point in sweeping further
60
+
61
+
62
+ # Animation:
63
+ OUTPUT_PATH_ANIMATION = False
64
+ OUTPUT_ANIMATION_FPS = 24
65
+
66
+ # Map & profiles parameters:
67
+ NUMBER_OF_PIXELS_X = 7
68
+ NUMBER_OF_PIXELS_Y = 67
69
+ IGNORE_FAULTY_PARTICLES = False
70
+ GRADIENT_FIT_RANGE = (0.1, 0.9)
71
+
72
+ # Material parameters:
73
+ MEDIA = "Si"
74
+ MEDIA_FERMI_LEVEL = None
75
+
76
+ # Internal scattering:
77
+ INCLUDE_INTERNAL_SCATTERING = True
78
+
79
+ # Phonon frequencies and branches are always sampled from the tabulated dispersion
80
+ # (weight k^2 dk per bin, heat-capacity weighting, group-velocity weighting at the
81
+ # source), so particles are equal-energy deviational bundles and the thermal maps
82
+ # record a constant weight per phonon. At inelastic (anharmonic) internal scattering
83
+ # events the branch and frequency are re-drawn from the collision-rate-weighted
84
+ # distribution (Peraud & Hadjiconstantinou, PRB 84, 205331 (2011)), restoring local
85
+ # thermal equilibrium of the phonon population. Elastic internal events (impurity/alloy
86
+ # scattering) conserve the mode and only randomize the direction. This applies to the
87
+ # phonon tracing mode; MFP sampling and electron modes keep the particle identity by
88
+ # construction.
89
+
90
+ # Convert deposited particle energy into the temperature profile using the
91
+ # dispersion-only heat capacity (Material.dispersion_heat_capacity, summed only
92
+ # over the branches in the tabulated dispersion) instead of the experimental
93
+ # heat capacity fit (Material.assign_heat_capacity). On by default because it
94
+ # makes the reported temperature and kappa self-consistent with the
95
+ # dispersion-based sampling/rethermalization scheme and the model's own RTA
96
+ # integral. Set to False to use the real material heat capacity instead (e.g.
97
+ # including optical branches absent from the tabulated dispersion):
98
+ USE_DISPERSION_HEAT_CAPACITY = True
99
+
100
+ # MFP sampling mode only (no effect in phonon tracing, where particles must keep
101
+ # going for the full budget to build the flux map): end a phonon's flight early
102
+ # once it has accumulated this many free-path segments, instead of always running
103
+ # to NUMBER_OF_TIMESTEPS. A short-tau, low-velocity phonon takes tiny hops and
104
+ # diffuses so slowly that it essentially never reaches a domain boundary, but its
105
+ # mean free path already converges after a modest number of scattering events, so
106
+ # running it to the full timestep budget is wasted compute. 1000 is on by default,
107
+ # since it was validated (bulk Si MFP-sampling convergence study, Data/BulkSi_Phonon_MFP/)
108
+ # to give the same result as 5000 while being far cheaper. Set to None to disable
109
+ # (run to the timestep budget or a boundary, as before):
110
+ MAX_NUMBER_OF_SCATTERING_EVENTS = 1000
111
+
112
+ # Grain boundary scattering (polycrystalline materials):
113
+ # GRAIN_SIZE sets the mean grain diameter [m]. None disables grain boundary scattering entirely.
114
+ # GRAIN_SIZE_STD sets the standard deviation [m]; the grain size is drawn per phonon from a
115
+ # lognormal distribution with this mean and std. Set to 0 for monodisperse (single grain size).
116
+ # GRAIN_ROUGHNESS is the RMS disorder width of the grain boundary [m], used in a Soffer-type
117
+ # specularity factor: at low frequencies (long wavelengths) the boundary appears smooth and
118
+ # phonons pass through; at high frequencies it acts as a classical diffuse scatterer.
119
+ # Typical values: GRAIN_SIZE 100 nm – 10 µm, GRAIN_ROUGHNESS 0.5–2 nm.
120
+ GRAIN_SIZE = None
121
+ GRAIN_SIZE_STD = 0.0
122
+ GRAIN_ROUGHNESS = 1e-9
123
+
124
+ # System dimensions [m]:
125
+ THICKNESS = 150e-9
126
+ WIDTH = 200e-9
127
+ LENGTH = 2000e-9
128
+ IS_TWO_DIMENSIONAL_MATERIAL = False
129
+ INCLUDE_RIGHT_SIDEWALL = True
130
+ INCLUDE_LEFT_SIDEWALL = True
131
+ INCLUDE_TOP_SIDEWALL = False
132
+ INCLUDE_BOTTOM_SIDEWALL = False
133
+
134
+ # Hot and cold sides [m]:
135
+ COLD_SIDE_POSITION_TOP = True
136
+ COLD_SIDE_POSITION_BOTTOM = False
137
+ COLD_SIDE_POSITION_RIGHT = False
138
+ COLD_SIDE_POSITION_LEFT = False
139
+ HOT_SIDE_POSITION_TOP = False
140
+ HOT_SIDE_POSITION_BOTTOM = True
141
+ HOT_SIDE_POSITION_RIGHT = False
142
+ HOT_SIDE_POSITION_LEFT = False
143
+ RETHERMALIZATION_ON_HOT_SIDES = True
144
+
145
+ # Particle source:
146
+ PARTICLE_SOURCES = [Source(x=0, y=0, z=0, size_x=0, size_y=0, size_z=0, angle_distribution="random", angle=0)]
147
+
148
+ # Roughness [m]:
149
+ SIDE_WALL_ROUGHNESS = 2e-9
150
+ HOLE_ROUGHNESS = 2e-9
151
+ PILLAR_ROUGHNESS = 2e-9
152
+ TOP_ROUGHNESS = 0.2e-9
153
+ BOTTOM_ROUGHNESS = 0.2e-9
154
+ PILLAR_TOP_ROUGHNESS = 0.2e-9
155
+ INTERFACE_ROUGHNESS = 0.2e-9
156
+
157
+ # Holes and pillars:
158
+ HOLES = []
159
+ PILLARS = []
160
+ INTERFACES = []
161
+ BULKS = []
162
+
163
+ # Multiprocessing:
164
+ NUMBER_OF_PROCESSES = 10
@@ -0,0 +1,149 @@
1
+ """This module provides electron class which generates and moves an electron"""
2
+
3
+ from random import choice, random
4
+ from scipy.constants import h, k, hbar, electron_volt, elementary_charge, epsilon_0
5
+ from freepaths.config import cf
6
+ from freepaths.particle import Particle
7
+ from freepaths.options import ParticleType
8
+
9
+ import numpy as np
10
+
11
+
12
+ def _carrier_dos_mass(material):
13
+ """DOS effective mass of the active carrier (electron or hole)."""
14
+ return material.effective_electron_dos_mass if cf.is_carrier_electron else material.effective_hole_dos_mass
15
+
16
+
17
+ def depletion_width(material):
18
+ """Surface-depletion (dead-layer) width for carriers, from the abrupt-junction
19
+ depletion approximation W = sqrt(2*eps_s*phi_s/(e*N_D)), with phi_s = SURFACE_POTENTIAL
20
+ the surface band bending (eV, used as volts), N_D the doping, and eps_s the static
21
+ permittivity of the material. Carriers are excluded within W of every free surface
22
+ (Fermi-level pinning by surface states / native oxide). Returns 0 if there is no
23
+ doping or no surface potential. Reference: any semiconductor-device text (e.g. Sze),
24
+ surface-depletion / dead-layer model for nanostructure conductivity.
25
+
26
+ N_D is DEPLETION_DOPING if set, otherwise DOPING_CONCENTRATION. The separate
27
+ DEPLETION_DOPING lets the dead layer be driven by the real doping WITHOUT also
28
+ engaging the Brooks-Herring ionized-impurity MFP (which keys off DOPING_CONCENTRATION),
29
+ i.e. to isolate the depletion effect on top of a constant-MFP transport calibration."""
30
+ N_I = cf.depletion_doping if cf.depletion_doping else cf.doping_concentration
31
+ phi = cf.surface_potential
32
+ eps_r = getattr(material, 'dielectric_constant', None)
33
+ if not N_I or phi <= 0 or not eps_r:
34
+ return 0.0
35
+ eps = eps_r * epsilon_0
36
+ return np.sqrt(2.0 * eps * phi / (elementary_charge * N_I))
37
+
38
+
39
+ def electron_mfp(energy, material):
40
+ """Energy- and doping-dependent carrier mean free path via Matthiessen's rule:
41
+ 1/Lambda_tot(E) = 1/Lambda_ac + 1/Lambda_ii(E, N_I).
42
+
43
+ Lambda_ac = ELECTRON_MFP is the crystal (acoustic-phonon-limited) MFP, taken
44
+ energy-independent (for acoustic deformation-potential scattering tau ~ E^-1/2
45
+ cancels v ~ E^1/2, so Lambda = v*tau ~ E^0). Lambda_ii is the ionized-impurity
46
+ contribution from the Brooks-Herring model: screened Coulomb scattering off the
47
+ DOPING_CONCENTRATION ionized dopants, with tau_ii ~ E^{3/2} in the weak-screening
48
+ limit. Ionized impurities thus scatter LOW-energy carriers most, shortening the
49
+ MFP with doping and making scattering energy-selective, which raises the Seebeck
50
+ coefficient. DOPING_CONCENTRATION = 0 recovers the crystal MFP (optionally with
51
+ the legacy phenomenological exponent ELECTRON_MFP_ENERGY_EXPONENT).
52
+
53
+ Applies identically to electrons and holes: the carrier's own DOS effective mass
54
+ is used (chosen by IS_CARRIER_ELECTRON) and N_I is the density of ionized donors
55
+ (n-type) or acceptors (p-type). Reference: Brooks-Herring; Chattopadhyay &
56
+ Queisser, Rev. Mod. Phys. 53, 745 (1981). `energy` in J, scalar or array."""
57
+ lambda_ac = cf.electron_mfp
58
+ N_I = cf.doping_concentration
59
+
60
+ if not N_I:
61
+ # No ionized impurities: crystal MFP, with optional legacy power law.
62
+ p = cf.electron_mfp_energy_exponent
63
+ if p == 0:
64
+ return lambda_ac
65
+ return lambda_ac * (energy / (k * cf.temp)) ** p
66
+
67
+ # Brooks-Herring ionized-impurity MFP (Matthiessen-combined with the acoustic MFP):
68
+ energy = np.maximum(energy, 1e-4 * elementary_charge) # guard against E -> 0
69
+ m_star = _carrier_dos_mass(material)
70
+ eps = material.dielectric_constant * epsilon_0
71
+ T = cf.temp
72
+ # Debye screening from the free carriers (n = N_I, full activation): 1/L_D^2 = e^2 n / (eps kB T)
73
+ inv_screen_length_sq = elementary_charge**2 * N_I / (eps * k * T)
74
+ b = 8.0 * m_star * energy / (hbar**2 * inv_screen_length_sq) # b = (2k/beta)^2
75
+ screening = np.maximum(np.log1p(b) - b / (1.0 + b), 1e-12)
76
+ inv_tau_ii = (N_I * elementary_charge**4) / (16.0 * np.pi * np.sqrt(2.0 * m_star) * eps**2 * energy**1.5) * screening
77
+ v = np.sqrt(2.0 * energy / m_star)
78
+ lambda_ii = v / inv_tau_ii # = v * tau_ii
79
+ return 1.0 / (1.0 / lambda_ac + 1.0 / lambda_ii)
80
+
81
+
82
+ class Electron(Particle):
83
+ """An electron particle"""
84
+
85
+ def __init__(self, material):
86
+ super().__init__()
87
+
88
+ # Assign particle type
89
+ self.type = ParticleType.ELECTRON
90
+ self.is_electron_carrier = cf.is_carrier_electron # assign electron or hole behavior
91
+
92
+ source = choice(cf.particles_sources)
93
+ while True:
94
+ self.x, self.y, self.z = source.generate_coordinates()
95
+ is_in_hole = any(hole.is_inside(self.x, self.y, None, cf) for hole in cf.holes)
96
+ if not is_in_hole:
97
+ break
98
+
99
+ # Assign initial angles
100
+ self.theta, self.phi = source.generate_angles()
101
+ self.correct_angle()
102
+ if cf.is_two_dimensional_material:
103
+ self.phi, self.z = 0.0, 0.0
104
+
105
+ self.assign_energy()
106
+ self.assign_frequency(material)
107
+ self.assign_speed(material)
108
+ self.assign_internal_scattering_time(material)
109
+
110
+ def assign_energy(self):
111
+ """
112
+ Assign energy uniformly to an electron based on temperature.
113
+ Conduction minimum is considered at 0 energy by convention.
114
+ """
115
+ num_energy_points = int((cf.energy_upper_bound-cf.energy_lower_bound)/cf.energy_step)
116
+ # Keep energy in J for other computations
117
+ self.energy = np.random.choice(np.linspace(cf.energy_lower_bound, cf.energy_upper_bound - cf.energy_step, num_energy_points)) * electron_volt + cf.energy_step * electron_volt
118
+
119
+
120
+ @property
121
+ def wavelength(self):
122
+ raise Exception("Electron wavelength undefined")
123
+
124
+ def assign_frequency(self, material):
125
+ """
126
+ Conduction minimum is 0 by convention
127
+ """
128
+ self.f = self.energy / h
129
+
130
+ def assign_speed(self, material):
131
+ """
132
+ Assign speed to an electron using group velocity and considering parabolic energy-k relation and conduction minimum at 0
133
+ """
134
+ if self.is_electron_carrier:
135
+ effective_mass = material.effective_electron_dos_mass
136
+ else:
137
+ effective_mass = material.effective_hole_dos_mass
138
+ self.speed = (2*self.energy/(effective_mass))**(0.5)
139
+
140
+ def assign_internal_scattering_time(self, material):
141
+ """
142
+ Determine relaxation time after which this particle will undergo internal scattering.
143
+ Considering only elasctic scattering between electrons and phonons and thus a constant MFP.
144
+ With ELECTRON_MFP_ENERGY_EXPONENT != 0 the MFP becomes energy-dependent
145
+ (see electron_mfp_at_energy); scattering stays elastic so the electron's energy,
146
+ and hence its MFP, is fixed for the whole flight.
147
+ """
148
+ mfp = electron_mfp(self.energy, material)
149
+ self.time_of_internal_scattering = -np.log(random()) * (mfp / self.speed)
@@ -3,21 +3,23 @@
3
3
  from math import cos
4
4
  import numpy as np
5
5
 
6
+ from freepaths.config import cf
7
+
6
8
 
7
9
  class Path:
8
10
  """Particle path coordinates"""
9
11
 
10
12
  def __init__(self, x_init, y_init, z_init):
11
13
  """Initialize a path"""
12
- self.x = np.array([x_init])
13
- self.y = np.array([y_init])
14
- self.z = np.array([z_init])
14
+ self.x = [x_init]
15
+ self.y = [y_init]
16
+ self.z = [z_init]
15
17
 
16
18
  def add_point(self, x_new, y_new, z_new):
17
19
  """Add a point to the particle path"""
18
- self.x = np.append(self.x, x_new)
19
- self.y = np.append(self.y, y_new)
20
- self.z = np.append(self.z, z_new)
20
+ self.x.append(x_new)
21
+ self.y.append(y_new)
22
+ self.z.append(z_new)
21
23
 
22
24
  @property
23
25
  def number_of_path_points(self):
@@ -36,21 +38,30 @@ class Flight:
36
38
  self.initial_theta = self.particle.theta
37
39
  self.path = Path(self.particle.x, self.particle.y, self.particle.z)
38
40
  self.exit_theta = 0.0
41
+ self.exit_frequency = 0.0
39
42
  self.free_path = 0.0
40
43
  self.travel_time = 0.0
41
44
  self.time_since_previous_scattering = 0.0
42
45
  self.free_paths = []
46
+ self._mfp_sum = 0.0
47
+ self._mfp_count = 0
43
48
  self.hole_diff_scattering_angles = []
44
49
  self.hole_spec_scattering_angles = []
45
50
  self.thermal_conductivity = 0.0
46
51
  @property
47
52
  def mean_free_path(self):
48
53
  """Mean value of all free flights"""
49
- try:
50
- mfp = sum(self.free_paths)/len(self.free_paths)
51
- except ZeroDivisionError:
52
- mfp = 0
53
- return mfp
54
+ return self._mfp_sum / self._mfp_count if self._mfp_count else 0
55
+
56
+ @property
57
+ def mean_free_path_sem(self):
58
+ """Standard error of the mean free path (MFP sampling mode only). Assumes
59
+ individual free-path segments are exponentially distributed (the natural
60
+ assumption for Poisson-process intrinsic scattering: coefficient of
61
+ variation = 1, i.e. std = mean), so SEM = mean_free_path / sqrt(n)."""
62
+ if self._mfp_count < 1:
63
+ return 0.0
64
+ return self.mean_free_path / (self._mfp_count ** 0.5)
54
65
 
55
66
  def add_point_to_path(self):
56
67
  """Add a scattering point to the path"""
@@ -58,7 +69,10 @@ class Flight:
58
69
 
59
70
  def save_free_paths(self):
60
71
  """Save current free path to the list of free paths"""
61
- self.free_paths.append(self.free_path)
72
+ self._mfp_sum += self.free_path
73
+ self._mfp_count += 1
74
+ if not cf.low_memory_usage:
75
+ self.free_paths.append(self.free_path)
62
76
 
63
77
  def save_hole_diff_scattering_angle(self, angle):
64
78
  """Save angle of diffuse scattering from the hole"""
@@ -73,10 +87,15 @@ class Flight:
73
87
  self.time_since_previous_scattering = 0.0
74
88
  self.free_path = 0.0
75
89
 
76
- def finish(self, step, timestep):
77
- """Finish the flight and record final state"""
90
+ def finish(self, step, timestep, reached_cold_side=False):
91
+ """Finish the flight and record final state. travel_time is only set when
92
+ the particle genuinely reached the cold side - it means "time to cold side",
93
+ so leaving it at 0 for other exits (e.g. the MFP-sampling scattering-event
94
+ cap) keeps that meaning intact instead of recording a meaningless duration."""
78
95
  self.exit_theta = self.particle.theta
79
- self.travel_time = step * timestep
96
+ self.exit_frequency = self.particle.f
97
+ if reached_cold_side:
98
+ self.travel_time = step * timestep
80
99
 
81
100
  def add_step(self, timestep):
82
101
  """Increase parameters of the flight by length of one step"""
@@ -99,6 +99,7 @@ def _run_branch(branch_number, shared_list, shared_progress):
99
99
  scatter_maps = ScatteringMap()
100
100
 
101
101
  total_thermal_conductivity = 0.0
102
+ total_kappa_variance = 0.0
102
103
  last_pct = -1
103
104
 
104
105
  for index in range(cf.number_of_particles):
@@ -112,9 +113,9 @@ def _run_branch(branch_number, shared_list, shared_progress):
112
113
  d_k_vector = material.dispersion[index + 1, 0] - material.dispersion[index, 0]
113
114
 
114
115
  # Initialise phonon at this k-point and run it through the geometry:
115
- phonon = Phonon(material, branch_number, index)
116
+ phonon = Phonon(material, SimulationMode.PHONON_MFP_SAMPLING, branch_number, index)
116
117
  flight = Flight(phonon)
117
- run_particle(phonon, flight, scatter_stats, places_stats, segment_stats, None, scatter_maps, material)
118
+ run_particle(phonon, flight, scatter_stats, places_stats, segment_stats, None, scatter_maps, material, SimulationMode.PHONON_MFP_SAMPLING)
118
119
 
119
120
  # Mode heat capacity (Ref. PRB 88 155318, 2013):
120
121
  omega = 2 * math.pi * phonon.f
@@ -126,6 +127,12 @@ def _run_branch(branch_number, shared_list, shared_progress):
126
127
  flight.thermal_conductivity = (1 / (6 * math.pi**2)) * c_p * phonon.speed**2 * mean_relax_time * k_vector**2 * d_k_vector
127
128
  total_thermal_conductivity += flight.thermal_conductivity
128
129
 
130
+ # This mode's kappa is linear in its mean free path, so its variance
131
+ # propagates directly through the same proportionality (kappa_i / mfp_i):
132
+ if flight.mean_free_path > 0:
133
+ kappa_relative_sem = flight.mean_free_path_sem / flight.mean_free_path
134
+ total_kappa_variance += (flight.thermal_conductivity * kappa_relative_sem) ** 2
135
+
129
136
  general_stats.save_particle_data(phonon, SimulationMode.PHONON_MFP_SAMPLING)
130
137
  general_stats.save_flight_data(flight, SimulationMode.PHONON_MFP_SAMPLING)
131
138
 
@@ -135,6 +142,7 @@ def _run_branch(branch_number, shared_list, shared_progress):
135
142
  # Push this branch's results to the shared list for the main process to collect:
136
143
  shared_list.append({
137
144
  'total_thermal_conductivity': total_thermal_conductivity,
145
+ 'total_kappa_variance': total_kappa_variance,
138
146
  'scatter_stats': scatter_stats.dump_data(),
139
147
  'general_stats': general_stats.dump_data(),
140
148
  'segment_stats': segment_stats.dump_data(),
@@ -144,6 +152,21 @@ def _run_branch(branch_number, shared_list, shared_progress):
144
152
  })
145
153
 
146
154
 
155
+ def calculate_porosity(grid_points=300):
156
+ """Calculate the porosity (hole area fraction) of the structure by sampling
157
+ the hole shapes on a regular grid. Holes partially outside the simulation
158
+ domain are accounted for with their inside part only."""
159
+ if not cf.holes:
160
+ return 0.0
161
+ xs = np.linspace(-cf.width / 2, cf.width / 2, grid_points)
162
+ ys = np.linspace(0, cf.length, grid_points)
163
+ inside = 0
164
+ for x in xs:
165
+ for y in ys:
166
+ inside += any(hole.is_inside(x, y, None, cf) for hole in cf.holes)
167
+ return inside / (grid_points * grid_points)
168
+
169
+
147
170
  def main(input_file, mode: SimulationMode):
148
171
  """Integrate the phonon dispersion over all three branches in parallel to get
149
172
  the bulk thermal conductivity via the MFP-sampling method."""
@@ -151,7 +174,7 @@ def main(input_file, mode: SimulationMode):
151
174
  print(f'Mean free path sampling of {Fore.GREEN}{cf.output_folder_name}{Style.RESET_ALL}')
152
175
  start_time = time.time()
153
176
 
154
- branch_names = get_media_class(cf.media).branch_names
177
+ branch_names = get_media_class(cf.media).dispersion_branch_names
155
178
  n_branches = len(branch_names)
156
179
 
157
180
  # Spawn one worker process per polarization branch, all running concurrently:
@@ -189,9 +212,11 @@ def main(input_file, mode: SimulationMode):
189
212
  path_stats = PathData()
190
213
  scatter_maps = ScatteringMap()
191
214
  total_thermal_conductivity = 0.0
215
+ total_kappa_variance = 0.0
192
216
 
193
217
  for result in shared_list:
194
218
  total_thermal_conductivity += result['total_thermal_conductivity']
219
+ total_kappa_variance += result['total_kappa_variance']
195
220
  scatter_stats.read_data(result['scatter_stats'])
196
221
  general_stats.read_data(result['general_stats'])
197
222
  segment_stats.read_data(result['segment_stats'])
@@ -218,14 +243,39 @@ def main(input_file, mode: SimulationMode):
218
243
  if cf.output_path_animation:
219
244
  create_animation()
220
245
 
221
- np.savetxt("Data/Thermal conductivity from MFP.csv", np.array([total_thermal_conductivity]), fmt='%2.4e', header="K [W/mK]", encoding='utf-8')
246
+ # The MFP-sampling integral uses the bulk DoS with boundary-limited free paths,
247
+ # so total_thermal_conductivity is the conductivity of the solid material
248
+ # (MFP suppression only, no volume removed by the holes). To obtain the
249
+ # effective conductivity of the porous structure, it is scaled by the Eucken
250
+ # factor F = (1 - phi) / (1 + phi/2), where phi is the porosity:
251
+ porosity = calculate_porosity()
252
+ eucken_factor = (1 - porosity) / (1 + porosity / 2)
253
+ effective_thermal_conductivity = total_thermal_conductivity * eucken_factor
254
+
255
+ header = "K_material [W/mK], Porosity, Eucken factor, K_effective [W/mK]"
256
+ np.savetxt("Data/Thermal conductivity from MFP.csv",
257
+ np.array([[total_thermal_conductivity, porosity, eucken_factor, effective_thermal_conductivity]]),
258
+ fmt='%2.4e', delimiter=',', header=header, encoding='utf-8')
259
+
260
+ # Kept in its own file rather than appended to "Thermal conductivity from MFP.csv",
261
+ # since several plotting scripts index into that file positionally (incl. by
262
+ # [-1] for the last column), which a trailing column would silently break:
263
+ kappa_sem = total_kappa_variance ** 0.5
264
+ effective_kappa_sem = kappa_sem * eucken_factor
265
+ np.savetxt("Data/Thermal conductivity from MFP uncertainty.csv",
266
+ np.array([[kappa_sem, effective_kappa_sem]]),
267
+ fmt='%2.4e', delimiter=',',
268
+ header="K_material SEM [W/mK], K_effective SEM [W/mK]", encoding='utf-8')
222
269
 
223
270
  sys.stdout.write("\rAnalyzing the data...")
224
271
  plot_data(mode, cf)
225
272
 
226
- output_general_information(start_time)
273
+ output_general_information(start_time, general_stats, mode)
227
274
  output_scattering_information(scatter_stats)
228
- output_parameter_warnings(mode)
275
+ output_parameter_warnings(mode, general_stats)
229
276
  sys.stdout.write(f'\rSee the results in {Fore.GREEN}Results/{cf.output_folder_name}{Style.RESET_ALL}\n')
230
- sys.stdout.write(f"\rThermal conductivity = {Fore.GREEN}{total_thermal_conductivity:.5f}{Style.RESET_ALL} W/m·K\n")
277
+ sys.stdout.write(f"\rMaterial thermal conductivity = {Fore.GREEN}{total_thermal_conductivity:.5f} ± {kappa_sem:.5f}{Style.RESET_ALL} W/m·K\n")
278
+ if cf.holes:
279
+ sys.stdout.write(f"\rEffective thermal conductivity = {Fore.GREEN}{effective_thermal_conductivity:.5f} ± {effective_kappa_sem:.5f}{Style.RESET_ALL} W/m·K "
280
+ f"(porosity {porosity:.1%}, Eucken factor {eucken_factor:.3f})\n")
231
281
  sys.stdout.write(f"\r{Fore.BLUE}Thank you for using FreePATHS{Style.RESET_ALL}\n\n")