freepaths 2.4.3__tar.gz → 2.5.1__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.
- {freepaths-2.4.3 → freepaths-2.5.1}/PKG-INFO +1 -2
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/__main__.py +1 -1
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/animation.py +15 -16
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/config.py +12 -2
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/data.py +64 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/default_config.py +12 -42
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/electron.py +4 -3
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/flight.py +30 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/interface_smmm.py +14 -28
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/main_mfp_sampling.py +64 -18
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/main_tracing.py +131 -40
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/maps.py +148 -18
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/materials.py +320 -102
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/output_info.py +55 -7
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/output_plots.py +18 -32
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/output_structure.py +1 -6
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/particle.py +14 -1
- freepaths-2.5.1/freepaths/phonon.py +314 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/run_particle.py +18 -14
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/scatterers.py +4 -194
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/scattering.py +0 -6
- freepaths-2.5.1/freepaths/scattering_interfaces.py +84 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/PKG-INFO +1 -2
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/requires.txt +0 -1
- {freepaths-2.4.3 → freepaths-2.5.1}/setup.py +1 -1
- freepaths-2.4.3/freepaths/phonon.py +0 -167
- freepaths-2.4.3/freepaths/scattering_interfaces.py +0 -264
- {freepaths-2.4.3 → freepaths-2.5.1}/README.md +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/move.py +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/options.py +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/output_electron_plots.py +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/post_computations.py +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/progress.py +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/scattering_primitives.py +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/scattering_semicircle.py +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/scattering_types.py +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/sources.py +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/SOURCES.txt +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/dependency_links.txt +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/entry_points.txt +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/top_level.txt +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/pyproject.toml +0 -0
- {freepaths-2.4.3 → freepaths-2.5.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: freepaths
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.1
|
|
4
4
|
Summary: Phonon Monte Carlo simulator
|
|
5
5
|
Home-page: https://github.com/anufrievroman/freepaths
|
|
6
6
|
Author: Roman Anufriev
|
|
@@ -21,7 +21,6 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
Requires-Dist: numpy
|
|
22
22
|
Requires-Dist: matplotlib
|
|
23
23
|
Requires-Dist: scipy
|
|
24
|
-
Requires-Dist: imageio
|
|
25
24
|
Requires-Dist: colorama
|
|
26
25
|
Dynamic: author
|
|
27
26
|
Dynamic: author-email
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import sys
|
|
4
4
|
import os
|
|
5
5
|
import shutil
|
|
6
|
-
import imageio
|
|
7
6
|
import numpy as np
|
|
8
7
|
import matplotlib.pyplot as plt
|
|
8
|
+
from PIL import Image
|
|
9
9
|
|
|
10
10
|
from freepaths.config import cf
|
|
11
11
|
from freepaths.output_structure import draw_structure_top_view
|
|
@@ -14,7 +14,7 @@ from freepaths.output_structure import draw_structure_top_view
|
|
|
14
14
|
def generate_frames_xy():
|
|
15
15
|
"""Generate animation frames with phonon paths"""
|
|
16
16
|
|
|
17
|
-
data = np.genfromtxt("Data/
|
|
17
|
+
data = np.genfromtxt("Data/Particle paths.csv",
|
|
18
18
|
unpack=False,
|
|
19
19
|
delimiter=',',
|
|
20
20
|
skip_header=1,
|
|
@@ -77,21 +77,20 @@ def generate_frames_xy():
|
|
|
77
77
|
def generate_animation_xy():
|
|
78
78
|
"""Generate animation of phonon path in XY plane"""
|
|
79
79
|
sys.stdout.write(f"\rAnimation: creating animation file")
|
|
80
|
-
images = []
|
|
81
|
-
filenames = os.listdir("Frames/")
|
|
82
|
-
for filename in filenames:
|
|
83
|
-
images.append(imageio.imread(f"Frames/{filename}"))
|
|
84
80
|
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
# Sort the frames so the GIF plays in the order they were rendered.
|
|
82
|
+
# os.listdir returns entries in arbitrary filesystem order (unsorted on
|
|
83
|
+
# macOS/APFS), which produced out-of-order animations. Filtering to the
|
|
84
|
+
# frame_*.png files also guards against stray files (e.g. .DS_Store).
|
|
85
|
+
filenames = sorted(f for f in os.listdir("Frames/") if f.endswith(".png"))
|
|
86
|
+
|
|
87
|
+
# Assemble the GIF with Pillow (already a matplotlib dependency), so no
|
|
88
|
+
# extra imaging library is required. Frames are converted to a palette
|
|
89
|
+
# image for GIF; duration is milliseconds per frame, loop=0 loops forever.
|
|
90
|
+
frames = [Image.open(f"Frames/{name}").convert("RGB").convert("P", palette=Image.ADAPTIVE)
|
|
91
|
+
for name in filenames]
|
|
92
|
+
frames[0].save("Animated paths XY.gif", save_all=True, append_images=frames[1:],
|
|
93
|
+
duration=1000 / cf.output_animation_fps, loop=0, optimize=True)
|
|
95
94
|
|
|
96
95
|
|
|
97
96
|
def delete_frames():
|
|
@@ -91,6 +91,7 @@ class Config:
|
|
|
91
91
|
self.number_of_virtual_timesteps = NUMBER_OF_VIRTUAL_TIMESTEPS
|
|
92
92
|
self.ignore_faulty_particles = IGNORE_FAULTY_PARTICLES
|
|
93
93
|
self.gradient_fit_range = GRADIENT_FIT_RANGE
|
|
94
|
+
self.temperature_profile_x_range = TEMPERATURE_PROFILE_X_RANGE
|
|
94
95
|
|
|
95
96
|
# Material parameters:
|
|
96
97
|
self.media = MEDIA
|
|
@@ -98,7 +99,12 @@ class Config:
|
|
|
98
99
|
|
|
99
100
|
# Internal scattering:
|
|
100
101
|
self.include_internal_scattering = INCLUDE_INTERNAL_SCATTERING
|
|
101
|
-
self.
|
|
102
|
+
self.phonon_hydrodynamic = PHONON_HYDRODYNAMIC
|
|
103
|
+
self.number_of_hydrodynamic_preruns = NUMBER_OF_HYDRODYNAMIC_PRERUNS
|
|
104
|
+
self.hydrodynamic_preruns_weight = HYDRODYNAMIC_PRERUNS_WEIGHT
|
|
105
|
+
self.number_of_hydrodynamic_prerun_particles = NUMBER_OF_HYDRODYNAMIC_PRERUN_PARTICLES
|
|
106
|
+
self.hydrodynamic_normal_resistive = HYDRODYNAMIC_NORMAL_RESISTIVE
|
|
107
|
+
self.isotope_c13_concentration = ISOTOPE_C13_CONCENTRATION
|
|
102
108
|
self.max_number_of_scattering_events = MAX_NUMBER_OF_SCATTERING_EVENTS
|
|
103
109
|
self.grain_size = GRAIN_SIZE
|
|
104
110
|
self.grain_size_std = GRAIN_SIZE_STD
|
|
@@ -143,7 +149,6 @@ class Config:
|
|
|
143
149
|
self.holes = HOLES
|
|
144
150
|
self.pillars = PILLARS
|
|
145
151
|
self.interfaces = INTERFACES
|
|
146
|
-
self.bulks = BULKS
|
|
147
152
|
|
|
148
153
|
# Multiprocessing:
|
|
149
154
|
self.num_workers = NUMBER_OF_PROCESSES
|
|
@@ -171,6 +176,11 @@ class Config:
|
|
|
171
176
|
f"See the documentation at {WEBSITE}")
|
|
172
177
|
sys.exit()
|
|
173
178
|
|
|
179
|
+
if not (0.0 <= self.temperature_profile_x_range[0] < self.temperature_profile_x_range[1] <= 1.0):
|
|
180
|
+
logging.error("Parameter TEMPERATURE_PROFILE_X_RANGE must be a pair (start, end) with 0 <= start < end <= 1.\n" +
|
|
181
|
+
f"See the documentation at {WEBSITE}")
|
|
182
|
+
sys.exit()
|
|
183
|
+
|
|
174
184
|
if self.number_of_timeframes <= self.number_of_stabilization_timeframes:
|
|
175
185
|
logging.error("Parameter NUMBER_OF_STABILIZATION_TIMEFRAMES exceeds or equal to NUMBER_OF_TIMEFRAMES.\n" +
|
|
176
186
|
"Leave at least one timeframe after NUMBER_OF_STABILIZATION_TIMEFRAMES.\n" +
|
|
@@ -67,6 +67,10 @@ class GeneralData(Data):
|
|
|
67
67
|
self.final_frequencies = []
|
|
68
68
|
self.initial_energies = []
|
|
69
69
|
self.group_velocities = []
|
|
70
|
+
# Wavevector of each particle, row-aligned with final_frequencies/group_velocities.
|
|
71
|
+
# Recorded separately because k cannot be recovered from f and v_g on a curved
|
|
72
|
+
# dispersion, and the physical wavelength is 2*pi/k (see Phonon.wavelength):
|
|
73
|
+
self.wavevectors = []
|
|
70
74
|
self.travel_times = []
|
|
71
75
|
self.mean_free_paths = []
|
|
72
76
|
self.thermal_conductivity = []
|
|
@@ -86,6 +90,7 @@ class GeneralData(Data):
|
|
|
86
90
|
else:
|
|
87
91
|
self.final_frequencies.append(pt.f)
|
|
88
92
|
self.group_velocities.append(pt.speed)
|
|
93
|
+
self.wavevectors.append(pt.k)
|
|
89
94
|
self.branches.append(pt.branch_number)
|
|
90
95
|
|
|
91
96
|
def save_flight_data(self, flight, mode=None):
|
|
@@ -120,6 +125,7 @@ class GeneralData(Data):
|
|
|
120
125
|
else:
|
|
121
126
|
np.savetxt("Data/All final frequencies.csv", self.final_frequencies, fmt='%2.4e', header="f [Hz]", encoding='utf-8')
|
|
122
127
|
np.savetxt("Data/All group velocities.csv", self.group_velocities, fmt='%.4f', header="Vg [m//s]", encoding='utf-8')
|
|
128
|
+
np.savetxt("Data/All wavevectors.csv", self.wavevectors, fmt='%2.4e', header="k [1//m]", encoding='utf-8')
|
|
123
129
|
if mode is SimulationMode.PHONON_TRACING:
|
|
124
130
|
np.savetxt("Data/All exit angles.csv", self.exit_angles, fmt='%.4f', header="Angle [rad]", encoding='utf-8')
|
|
125
131
|
np.savetxt("Data/All initial angles.csv", self.initial_angles, fmt='%.4f', header="Angle [rad]", encoding='utf-8')
|
|
@@ -147,6 +153,7 @@ class GeneralData(Data):
|
|
|
147
153
|
'free_paths': self.free_paths,
|
|
148
154
|
'final_frequencies': self.final_frequencies,
|
|
149
155
|
'group_velocities': self.group_velocities,
|
|
156
|
+
'wavevectors': self.wavevectors,
|
|
150
157
|
'travel_times': self.travel_times,
|
|
151
158
|
'mean_free_paths': self.mean_free_paths,
|
|
152
159
|
'thermal_conductivity': self.thermal_conductivity,
|
|
@@ -157,6 +164,63 @@ class GeneralData(Data):
|
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
|
|
167
|
+
class SpecularityData(Data):
|
|
168
|
+
"""
|
|
169
|
+
Running sums behind the specular scattering probability report (MFP-sampling mode).
|
|
170
|
+
|
|
171
|
+
The reported kappa-weighted specularity is a weighted mean over modes,
|
|
172
|
+
sum(kappa_i * f_i) / sum(kappa_i) with f_i = specular_i / (specular_i + diffuse_i),
|
|
173
|
+
so it accumulates in four fixed-size arrays and never needs the per-mode tallies
|
|
174
|
+
kept around. That matters: storing them was one 10-integer row per phonon in memory
|
|
175
|
+
and on disk, which scales with NUMBER_OF_PARTICLES for a result that is 10 numbers.
|
|
176
|
+
Arrays are indexed by flight.BOUNDARY_CATEGORIES and merged across the branch
|
|
177
|
+
workers by Data.read_data, whose "+" is elementwise on numpy arrays.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(self):
|
|
181
|
+
"""Initialize the accumulators, one entry per boundary category"""
|
|
182
|
+
from freepaths.flight import BOUNDARY_CATEGORIES
|
|
183
|
+
self.specular_events = np.zeros(len(BOUNDARY_CATEGORIES))
|
|
184
|
+
self.diffuse_events = np.zeros(len(BOUNDARY_CATEGORIES))
|
|
185
|
+
self.kappa_weighted_specular = np.zeros(len(BOUNDARY_CATEGORIES))
|
|
186
|
+
self.kappa_of_scattered = np.zeros(len(BOUNDARY_CATEGORIES))
|
|
187
|
+
|
|
188
|
+
def add_particle(self, flight):
|
|
189
|
+
"""Fold one finished flight into the running sums"""
|
|
190
|
+
events = np.array(flight.boundary_events, dtype=float).reshape(-1, 2)
|
|
191
|
+
specular, diffuse = events[:, 0], events[:, 1]
|
|
192
|
+
total = specular + diffuse
|
|
193
|
+
self.specular_events += specular
|
|
194
|
+
self.diffuse_events += diffuse
|
|
195
|
+
# Only modes that actually hit a given boundary enter that boundary's mean:
|
|
196
|
+
scattered = total > 0
|
|
197
|
+
kappa = flight.thermal_conductivity
|
|
198
|
+
self.kappa_weighted_specular[scattered] += kappa * specular[scattered] / total[scattered]
|
|
199
|
+
self.kappa_of_scattered[scattered] += kappa
|
|
200
|
+
|
|
201
|
+
def write_into_files(self):
|
|
202
|
+
"""Write the specular scattering probabilities into a file"""
|
|
203
|
+
from freepaths.flight import BOUNDARY_CATEGORIES
|
|
204
|
+
total = self.specular_events + self.diffuse_events
|
|
205
|
+
with np.errstate(invalid='ignore', divide='ignore'):
|
|
206
|
+
per_event = 100 * self.specular_events / total
|
|
207
|
+
per_kappa = 100 * self.kappa_weighted_specular / self.kappa_of_scattered
|
|
208
|
+
rows = [[per_event[i], per_kappa[i], total[i]] for i in range(len(BOUNDARY_CATEGORIES))]
|
|
209
|
+
np.savetxt("Data/Specular scattering probability.csv", np.array(rows), fmt='%2.4e', delimiter=',',
|
|
210
|
+
header="Boundary: " + ", ".join(BOUNDARY_CATEGORIES)
|
|
211
|
+
+ "\nSpecular per event [%], Specular kappa-weighted [%], Total events",
|
|
212
|
+
encoding='utf-8')
|
|
213
|
+
|
|
214
|
+
def dump_data(self):
|
|
215
|
+
"""Return data of a process in the form of a dictionary to be attached to the global data"""
|
|
216
|
+
return {
|
|
217
|
+
'specular_events': self.specular_events,
|
|
218
|
+
'diffuse_events': self.diffuse_events,
|
|
219
|
+
'kappa_weighted_specular': self.kappa_weighted_specular,
|
|
220
|
+
'kappa_of_scattered': self.kappa_of_scattered,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
160
224
|
class ScatteringData(Data):
|
|
161
225
|
"""Statistics of particle scattering events"""
|
|
162
226
|
|
|
@@ -55,9 +55,6 @@ MEAN_MAPPING_CONSTANT = 5e-6 # [m²]
|
|
|
55
55
|
|
|
56
56
|
FERMI_LEVEL_LOWER_BOUND = -0.2 # [eV] lower end of post-processing Fermi level sweep
|
|
57
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
58
|
|
|
62
59
|
# Animation:
|
|
63
60
|
OUTPUT_PATH_ANIMATION = False
|
|
@@ -68,6 +65,7 @@ NUMBER_OF_PIXELS_X = 7
|
|
|
68
65
|
NUMBER_OF_PIXELS_Y = 67
|
|
69
66
|
IGNORE_FAULTY_PARTICLES = False
|
|
70
67
|
GRADIENT_FIT_RANGE = (0.1, 0.9)
|
|
68
|
+
TEMPERATURE_PROFILE_X_RANGE = (0.0, 1.0)
|
|
71
69
|
|
|
72
70
|
# Material parameters:
|
|
73
71
|
MEDIA = "Si"
|
|
@@ -76,47 +74,20 @@ MEDIA_FERMI_LEVEL = None
|
|
|
76
74
|
# Internal scattering:
|
|
77
75
|
INCLUDE_INTERNAL_SCATTERING = True
|
|
78
76
|
|
|
79
|
-
# Phonon
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
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):
|
|
77
|
+
# Phonon hydrodynamic transport.
|
|
78
|
+
PHONON_HYDRODYNAMIC = False
|
|
79
|
+
NUMBER_OF_HYDRODYNAMIC_PRERUNS = 5
|
|
80
|
+
HYDRODYNAMIC_PRERUNS_WEIGHT = 0.8
|
|
81
|
+
NUMBER_OF_HYDRODYNAMIC_PRERUN_PARTICLES = None
|
|
82
|
+
|
|
83
|
+
# Diagnostic control: when True, Normal events still fire but redraw the direction
|
|
84
|
+
# randomly instead of biased toward the local drift
|
|
85
|
+
HYDRODYNAMIC_NORMAL_RESISTIVE = False
|
|
86
|
+
ISOTOPE_C13_CONCENTRATION = 0.0
|
|
87
|
+
|
|
110
88
|
MAX_NUMBER_OF_SCATTERING_EVENTS = 1000
|
|
111
89
|
|
|
112
90
|
# 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
91
|
GRAIN_SIZE = None
|
|
121
92
|
GRAIN_SIZE_STD = 0.0
|
|
122
93
|
GRAIN_ROUGHNESS = 1e-9
|
|
@@ -158,7 +129,6 @@ INTERFACE_ROUGHNESS = 0.2e-9
|
|
|
158
129
|
HOLES = []
|
|
159
130
|
PILLARS = []
|
|
160
131
|
INTERFACES = []
|
|
161
|
-
BULKS = []
|
|
162
132
|
|
|
163
133
|
# Multiprocessing:
|
|
164
134
|
NUMBER_OF_PROCESSES = 10
|
|
@@ -127,15 +127,16 @@ class Electron(Particle):
|
|
|
127
127
|
"""
|
|
128
128
|
self.f = self.energy / h
|
|
129
129
|
|
|
130
|
-
def
|
|
130
|
+
def speed_in(self, material):
|
|
131
131
|
"""
|
|
132
|
-
|
|
132
|
+
Speed of an electron in the given material from the parabolic energy-k relation,
|
|
133
|
+
with the conduction minimum at 0
|
|
133
134
|
"""
|
|
134
135
|
if self.is_electron_carrier:
|
|
135
136
|
effective_mass = material.effective_electron_dos_mass
|
|
136
137
|
else:
|
|
137
138
|
effective_mass = material.effective_hole_dos_mass
|
|
138
|
-
|
|
139
|
+
return (2*self.energy/(effective_mass))**(0.5)
|
|
139
140
|
|
|
140
141
|
def assign_internal_scattering_time(self, material):
|
|
141
142
|
"""
|
|
@@ -4,6 +4,13 @@ from math import cos
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
6
|
from freepaths.config import cf
|
|
7
|
+
from freepaths.scattering_types import Scattering
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Boundary categories tallied per particle by Flight.record_boundary_events. The order
|
|
11
|
+
# fixes the index order of Flight.boundary_events, of the data.SpecularityData
|
|
12
|
+
# accumulators, and of the rows of "Data/Specular scattering probability.csv".
|
|
13
|
+
BOUNDARY_CATEGORIES = ('Side walls', 'Top and bottom walls', 'Hole walls', 'Pillar walls', 'Interfaces')
|
|
7
14
|
|
|
8
15
|
|
|
9
16
|
class Path:
|
|
@@ -50,6 +57,16 @@ class Flight:
|
|
|
50
57
|
# impurity) and boundary (diffuse surface) spectra in the MFP-sampling mode:
|
|
51
58
|
self._internal_count = 0
|
|
52
59
|
self._boundary_count = 0
|
|
60
|
+
# Specular and diffuse boundary event counts of THIS particle, as flat
|
|
61
|
+
# [specular, diffuse] pairs per entry of BOUNDARY_CATEGORIES. The aggregate
|
|
62
|
+
# ScatteringData counters cannot answer "how specular is a given mode", because
|
|
63
|
+
# they sum events over all particles: a mode that scatters diffusely lingers in
|
|
64
|
+
# the structure and accumulates far more events than one that reflects
|
|
65
|
+
# specularly and streams straight out. Folded into data.SpecularityData as soon
|
|
66
|
+
# as the flight ends and then discarded with the Flight, so nothing per-mode
|
|
67
|
+
# accumulates; only filled in the MFP-sampling mode, which is where it is
|
|
68
|
+
# reported (see output_info.weighted_specularity_information).
|
|
69
|
+
self.boundary_events = [0] * (2 * len(BOUNDARY_CATEGORIES))
|
|
53
70
|
self.hole_diff_scattering_angles = []
|
|
54
71
|
self.hole_spec_scattering_angles = []
|
|
55
72
|
self.thermal_conductivity = 0.0
|
|
@@ -97,6 +114,19 @@ class Flight:
|
|
|
97
114
|
if not cf.low_memory_usage:
|
|
98
115
|
self.free_paths.append(self.free_path)
|
|
99
116
|
|
|
117
|
+
def record_boundary_events(self, scattering_types):
|
|
118
|
+
"""Tally this timestep's boundary events by category and by specularity."""
|
|
119
|
+
categories = (scattering_types.walls, scattering_types.top_bottom,
|
|
120
|
+
scattering_types.holes, scattering_types.pillars,
|
|
121
|
+
# interfaces covers reflection AND transmission, so counting
|
|
122
|
+
# interfaces_transmission as well would double-count them:
|
|
123
|
+
scattering_types.interfaces)
|
|
124
|
+
for index, scattering in enumerate(categories):
|
|
125
|
+
if scattering is Scattering.SPECULAR:
|
|
126
|
+
self.boundary_events[2 * index] += 1
|
|
127
|
+
elif scattering is Scattering.DIFFUSE:
|
|
128
|
+
self.boundary_events[2 * index + 1] += 1
|
|
129
|
+
|
|
100
130
|
def record_scattering_channel(self, is_internal, is_boundary):
|
|
101
131
|
"""Tally the channel of a diffuse event that ends a free-path segment, so the
|
|
102
132
|
total scattering rate can later be split into internal and boundary spectra."""
|
|
@@ -31,6 +31,19 @@ def alpha_spec(theta_i, vg_i, vg_j, rho_i, rho_j):
|
|
|
31
31
|
"""
|
|
32
32
|
Specular transmission coefficient (AMM) between two materials.
|
|
33
33
|
|
|
34
|
+
Ran & Cao, PRB 110, 024302 (2024), Eq. (20):
|
|
35
|
+
|
|
36
|
+
alpha = 4*A*B / (A + B)**2, A = Z_j/Z_i, B = cos(theta_j)/cos(theta_i)
|
|
37
|
+
|
|
38
|
+
which is the standard oblique-incidence acoustic result 4*W_i*W_j/(W_i + W_j)**2
|
|
39
|
+
with the angle-corrected impedances W = Z/cos(theta), i.e. the COSINES IN THE
|
|
40
|
+
DENOMINATOR ARE CROSSED: (Z_i*cos(theta_j) + Z_j*cos(theta_i))**2. Writing them
|
|
41
|
+
uncrossed agrees at normal incidence but over-estimates transmission at oblique
|
|
42
|
+
incidence (Si/SiGe LA at 1 THz: 0.990 vs 0.928 at 60 deg).
|
|
43
|
+
|
|
44
|
+
Note this is Eq. (20), not Eq. (25): the MTM detailed-balance prefactor
|
|
45
|
+
min{1, P_j*C_j*vg_j**3 / (P_i*C_i*vg_i**3)} of Eq. (25) is deliberately not applied.
|
|
46
|
+
|
|
34
47
|
Parameters:
|
|
35
48
|
theta_i -- incidence angle
|
|
36
49
|
vg_i, vg_j -- group velocities of media i and j
|
|
@@ -49,7 +62,7 @@ def alpha_spec(theta_i, vg_i, vg_j, rho_i, rho_j):
|
|
|
49
62
|
Z_j = rho_j * vg_j
|
|
50
63
|
|
|
51
64
|
num = 4 * Z_i * Z_j * abs(math.cos(theta_i)) * abs(math.cos(theta_j))
|
|
52
|
-
den = (Z_i * abs(math.cos(
|
|
65
|
+
den = (Z_i * abs(math.cos(theta_j)) + Z_j * abs(math.cos(theta_i))) ** 2
|
|
53
66
|
return num / den
|
|
54
67
|
except:
|
|
55
68
|
return 0.0
|
|
@@ -114,30 +127,3 @@ def alpha_total_2T(theta_i, vg_i, vg_j, rho_i, rho_j, omega_i, omega_j, material
|
|
|
114
127
|
T2 = P_j * a_s2 + (1 - P_j) * a_d2
|
|
115
128
|
|
|
116
129
|
return T1 * T2
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def alpha_total_1T(theta_i, vg_i, vg_j, rho_i, rho_j, omega_i, omega_j, material_i, material_j, branch_number, roughness):
|
|
120
|
-
"""Transmission model for 1 transmission : ex: Si → Ge for small rectangle inside single crystal Si"""
|
|
121
|
-
|
|
122
|
-
k_i = get_k_from_omega(omega_i, material_i, branch_number)
|
|
123
|
-
k_j = get_k_from_omega(omega_j, material_j, branch_number)
|
|
124
|
-
|
|
125
|
-
wavelength_i = lambda_from_k(k_i)
|
|
126
|
-
wavelength_j = lambda_from_k(k_j)
|
|
127
|
-
|
|
128
|
-
P_i = math.exp(-16 * math.pi**2 * roughness**2 / wavelength_i**2)
|
|
129
|
-
P_j = math.exp(-16 * math.pi**2 * roughness**2 / wavelength_j**2)
|
|
130
|
-
|
|
131
|
-
try:
|
|
132
|
-
sin_theta_t = (vg_i / vg_j) * math.sin(theta_i)
|
|
133
|
-
if abs(sin_theta_t) > 1:
|
|
134
|
-
return 0.0 # Totle reflection at the entry
|
|
135
|
-
except:
|
|
136
|
-
return 0.0
|
|
137
|
-
|
|
138
|
-
a_s = alpha_spec(theta_i, vg_i, vg_j, rho_i, rho_j)
|
|
139
|
-
a_d = alpha_diff(k_i, k_j, P_i, P_j)
|
|
140
|
-
T_1T = P_i * a_s + (1 - P_i) * a_d
|
|
141
|
-
|
|
142
|
-
return T_1T
|
|
143
|
-
|
|
@@ -20,11 +20,10 @@ from freepaths.run_particle import run_particle
|
|
|
20
20
|
from freepaths.phonon import Phonon
|
|
21
21
|
from freepaths.flight import Flight
|
|
22
22
|
from freepaths.options import SimulationMode
|
|
23
|
-
from freepaths.data import ScatteringData, GeneralData, SegmentData, PathData, TriangleScatteringData
|
|
24
|
-
from freepaths.materials import
|
|
23
|
+
from freepaths.data import ScatteringData, GeneralData, SegmentData, PathData, TriangleScatteringData, SpecularityData
|
|
24
|
+
from freepaths.materials import create_material, get_media_class
|
|
25
25
|
from freepaths.maps import ScatteringMap
|
|
26
26
|
from freepaths.output_info import output_general_information, output_scattering_information, output_parameter_warnings
|
|
27
|
-
from freepaths.materials import get_media_class
|
|
28
27
|
from freepaths.output_plots import plot_data
|
|
29
28
|
|
|
30
29
|
|
|
@@ -78,17 +77,8 @@ def _run_branch(branch_number, shared_list, shared_progress):
|
|
|
78
77
|
# Each worker builds its own material instance so the dispersion is
|
|
79
78
|
# sampled with the correct number of points (number_of_particles + 1
|
|
80
79
|
# intervals → number_of_particles midpoints):
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
elif cf.media == "SiGe":
|
|
84
|
-
material = SiGe(cf.temp, num_points=cf.number_of_particles + 1)
|
|
85
|
-
elif cf.media == "SiC":
|
|
86
|
-
material = SiC(cf.temp, num_points=cf.number_of_particles + 1)
|
|
87
|
-
elif cf.media == "Graphite":
|
|
88
|
-
material = Graphite(cf.temp, num_points=cf.number_of_particles + 1)
|
|
89
|
-
else:
|
|
90
|
-
logging.error(f"Material {cf.media} is not supported")
|
|
91
|
-
return
|
|
80
|
+
material = create_material(cf.media, cf.temp, num_points=cf.number_of_particles + 1,
|
|
81
|
+
isotope_c13_concentration=cf.isotope_c13_concentration)
|
|
92
82
|
|
|
93
83
|
# Local data structures — merged into the global ones after all workers finish:
|
|
94
84
|
scatter_stats = ScatteringData()
|
|
@@ -97,9 +87,13 @@ def _run_branch(branch_number, shared_list, shared_progress):
|
|
|
97
87
|
places_stats = TriangleScatteringData()
|
|
98
88
|
path_stats = PathData()
|
|
99
89
|
scatter_maps = ScatteringMap()
|
|
90
|
+
specularity_stats = SpecularityData()
|
|
100
91
|
|
|
101
92
|
total_thermal_conductivity = 0.0
|
|
102
93
|
total_kappa_variance = 0.0
|
|
94
|
+
total_kappa1 = 0.0 # Callaway kappa1 accumulator (uses combined tau_C)
|
|
95
|
+
total_k2_num = 0.0 # Callaway kappa2 numerator sum: w * tau_C / tau_N
|
|
96
|
+
total_k2_den = 0.0 # Callaway kappa2 denominator sum: w * tau_C / (tau_N * tau_R)
|
|
103
97
|
last_pct = -1
|
|
104
98
|
|
|
105
99
|
for index in range(cf.number_of_particles):
|
|
@@ -122,11 +116,31 @@ def _run_branch(branch_number, shared_list, shared_progress):
|
|
|
122
116
|
part = scipy.constants.hbar * omega / (scipy.constants.k * cf.temp)
|
|
123
117
|
c_p = scipy.constants.k * part**2 * math.exp(part) / (math.exp(part) - 1)**2
|
|
124
118
|
|
|
125
|
-
# Thermal conductivity contribution from this k-point (Ref. Phys. Rev. 132 2461, 1963)
|
|
119
|
+
# Thermal conductivity contribution from this k-point (Ref. Phys. Rev. 132 2461, 1963).
|
|
120
|
+
# The measured mean free path is limited by resistive (Umklapp + isotope) and boundary
|
|
121
|
+
# scattering, so mean_relax_time is the Callaway resistive time tau_R (Normal scattering
|
|
122
|
+
# does not fire in the MFP-sampling mode). weight is the common per-mode integrand factor.
|
|
126
123
|
mean_relax_time = flight.mean_free_path / phonon.speed
|
|
127
|
-
|
|
124
|
+
weight = (1 / (6 * math.pi**2)) * c_p * phonon.speed**2 * k_vector**2 * d_k_vector
|
|
125
|
+
flight.thermal_conductivity = weight * mean_relax_time # single-mode RTA (SMRT)
|
|
128
126
|
total_thermal_conductivity += flight.thermal_conductivity
|
|
129
127
|
|
|
128
|
+
# Callaway dual-relaxation correction (Callaway, Phys. Rev. 113, 1046 (1959)):
|
|
129
|
+
# kappa = kappa1 + kappa2, with 1/tau_C = 1/tau_R + 1/tau_N. kappa1 uses the combined
|
|
130
|
+
# tau_C (Normal provisionally counted as resistive); kappa2 is the momentum-recovery term
|
|
131
|
+
# kappa2 = (sum w*tau_C/tau_N)^2 / (sum w*tau_C/(tau_N*tau_R)), accumulated linearly here
|
|
132
|
+
# and combined in main(). tau_C/tau_N = tau_C*normal_rate. Only computed when
|
|
133
|
+
# PHONON_HYDRODYNAMIC is on (the Normal / momentum-recovery treatment); otherwise
|
|
134
|
+
# normal_rate = 0 and kappa1 collapses to the standard SMRT conductivity.
|
|
135
|
+
normal_rate = material.phonon_normal_rate(omega) if cf.phonon_hydrodynamic else 0.0
|
|
136
|
+
if normal_rate > 0 and mean_relax_time > 0:
|
|
137
|
+
tau_C = 1.0 / (1.0 / mean_relax_time + normal_rate)
|
|
138
|
+
total_kappa1 += weight * tau_C
|
|
139
|
+
total_k2_num += weight * tau_C * normal_rate
|
|
140
|
+
total_k2_den += weight * tau_C * normal_rate / mean_relax_time
|
|
141
|
+
else:
|
|
142
|
+
total_kappa1 += weight * mean_relax_time
|
|
143
|
+
|
|
130
144
|
# This mode's kappa is linear in its mean free path, so its variance
|
|
131
145
|
# propagates directly through the same proportionality (kappa_i / mfp_i):
|
|
132
146
|
if flight.mean_free_path > 0:
|
|
@@ -135,6 +149,7 @@ def _run_branch(branch_number, shared_list, shared_progress):
|
|
|
135
149
|
|
|
136
150
|
general_stats.save_particle_data(phonon, SimulationMode.PHONON_MFP_SAMPLING)
|
|
137
151
|
general_stats.save_flight_data(flight, SimulationMode.PHONON_MFP_SAMPLING)
|
|
152
|
+
specularity_stats.add_particle(flight)
|
|
138
153
|
|
|
139
154
|
if index < cf.output_trajectories_of_first:
|
|
140
155
|
path_stats.save_particle_path(flight)
|
|
@@ -143,12 +158,16 @@ def _run_branch(branch_number, shared_list, shared_progress):
|
|
|
143
158
|
shared_list.append({
|
|
144
159
|
'total_thermal_conductivity': total_thermal_conductivity,
|
|
145
160
|
'total_kappa_variance': total_kappa_variance,
|
|
161
|
+
'total_kappa1': total_kappa1,
|
|
162
|
+
'total_k2_num': total_k2_num,
|
|
163
|
+
'total_k2_den': total_k2_den,
|
|
146
164
|
'scatter_stats': scatter_stats.dump_data(),
|
|
147
165
|
'general_stats': general_stats.dump_data(),
|
|
148
166
|
'segment_stats': segment_stats.dump_data(),
|
|
149
167
|
'places_stats': places_stats.dump_data(),
|
|
150
168
|
'path_stats': path_stats.dump_data(),
|
|
151
169
|
'scatter_maps': scatter_maps.dump_data(),
|
|
170
|
+
'specularity_stats': specularity_stats.dump_data(),
|
|
152
171
|
})
|
|
153
172
|
|
|
154
173
|
|
|
@@ -211,18 +230,26 @@ def main(input_file, mode: SimulationMode):
|
|
|
211
230
|
places_stats = TriangleScatteringData()
|
|
212
231
|
path_stats = PathData()
|
|
213
232
|
scatter_maps = ScatteringMap()
|
|
233
|
+
specularity_stats = SpecularityData()
|
|
214
234
|
total_thermal_conductivity = 0.0
|
|
215
235
|
total_kappa_variance = 0.0
|
|
236
|
+
total_kappa1 = 0.0
|
|
237
|
+
total_k2_num = 0.0
|
|
238
|
+
total_k2_den = 0.0
|
|
216
239
|
|
|
217
240
|
for result in shared_list:
|
|
218
241
|
total_thermal_conductivity += result['total_thermal_conductivity']
|
|
219
242
|
total_kappa_variance += result['total_kappa_variance']
|
|
243
|
+
total_kappa1 += result['total_kappa1']
|
|
244
|
+
total_k2_num += result['total_k2_num']
|
|
245
|
+
total_k2_den += result['total_k2_den']
|
|
220
246
|
scatter_stats.read_data(result['scatter_stats'])
|
|
221
247
|
general_stats.read_data(result['general_stats'])
|
|
222
248
|
segment_stats.read_data(result['segment_stats'])
|
|
223
249
|
places_stats.read_data(result['places_stats'])
|
|
224
250
|
path_stats.read_data(result['path_stats'])
|
|
225
251
|
scatter_maps.read_data(result['scatter_maps'])
|
|
252
|
+
specularity_stats.read_data(result['specularity_stats'])
|
|
226
253
|
|
|
227
254
|
# Create the output folder and copy input file:
|
|
228
255
|
if not os.path.exists("Results/" + cf.output_folder_name):
|
|
@@ -235,6 +262,7 @@ def main(input_file, mode: SimulationMode):
|
|
|
235
262
|
# Save data and generate plots:
|
|
236
263
|
general_stats.write_into_files(mode)
|
|
237
264
|
scatter_stats.write_into_files()
|
|
265
|
+
specularity_stats.write_into_files()
|
|
238
266
|
segment_stats.write_into_files()
|
|
239
267
|
if cf.output_scattering_map:
|
|
240
268
|
scatter_maps.write_into_files()
|
|
@@ -267,14 +295,32 @@ def main(input_file, mode: SimulationMode):
|
|
|
267
295
|
fmt='%2.4e', delimiter=',',
|
|
268
296
|
header="K_material SEM [W/mK], K_effective SEM [W/mK]", encoding='utf-8')
|
|
269
297
|
|
|
298
|
+
# Callaway dual-relaxation conductivity kappa = kappa1 + kappa2 (momentum-recovery term).
|
|
299
|
+
# Only written in hydrodynamic mode (the Normal-process treatment); in the standard RTA the
|
|
300
|
+
# -s output is unchanged. Kept in a separate file so the positional indexers of
|
|
301
|
+
# "Thermal conductivity from MFP.csv" are not disturbed. See main_mfp_sampling._run_branch.
|
|
302
|
+
kappa2 = kappa_callaway = None
|
|
303
|
+
if cf.phonon_hydrodynamic:
|
|
304
|
+
kappa2 = (total_k2_num ** 2 / total_k2_den) if total_k2_den > 0 else 0.0
|
|
305
|
+
kappa_callaway = total_kappa1 + kappa2
|
|
306
|
+
np.savetxt("Data/Thermal conductivity Callaway.csv",
|
|
307
|
+
np.array([[total_kappa1, kappa2, kappa_callaway]]),
|
|
308
|
+
fmt='%2.4e', delimiter=',',
|
|
309
|
+
header="kappa1 [W/mK], kappa2 [W/mK], kappa_Callaway [W/mK]", encoding='utf-8')
|
|
310
|
+
|
|
270
311
|
sys.stdout.write("\rAnalyzing the data...")
|
|
271
312
|
plot_data(mode, cf)
|
|
272
313
|
|
|
273
314
|
output_general_information(start_time, general_stats, mode)
|
|
274
|
-
output_scattering_information(scatter_stats)
|
|
315
|
+
output_scattering_information(scatter_stats, specularity_stats, mode)
|
|
275
316
|
output_parameter_warnings(mode, general_stats)
|
|
276
317
|
sys.stdout.write(f'\rSee the results in {Fore.GREEN}Results/{cf.output_folder_name}{Style.RESET_ALL}\n')
|
|
277
|
-
|
|
318
|
+
if cf.phonon_hydrodynamic:
|
|
319
|
+
sys.stdout.write(f"\rMaterial thermal conductivity (SMRT) = {Fore.GREEN}{total_thermal_conductivity:.5f} ± {kappa_sem:.5f}{Style.RESET_ALL} W/m·K\n")
|
|
320
|
+
sys.stdout.write(f"\rMaterial thermal conductivity (Callaway) = {Fore.GREEN}{kappa_callaway:.5f}{Style.RESET_ALL} W/m·K "
|
|
321
|
+
f"(kappa1 {total_kappa1:.2f} + kappa2 {kappa2:.2f})\n")
|
|
322
|
+
else:
|
|
323
|
+
sys.stdout.write(f"\rMaterial thermal conductivity = {Fore.GREEN}{total_thermal_conductivity:.5f} ± {kappa_sem:.5f}{Style.RESET_ALL} W/m·K\n")
|
|
278
324
|
if cf.holes:
|
|
279
325
|
sys.stdout.write(f"\rEffective thermal conductivity = {Fore.GREEN}{effective_thermal_conductivity:.5f} ± {effective_kappa_sem:.5f}{Style.RESET_ALL} W/m·K "
|
|
280
326
|
f"(porosity {porosity:.1%}, Eucken factor {eucken_factor:.3f})\n")
|