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.
Files changed (43) hide show
  1. {freepaths-2.4.3 → freepaths-2.5.1}/PKG-INFO +1 -2
  2. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/__main__.py +1 -1
  3. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/animation.py +15 -16
  4. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/config.py +12 -2
  5. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/data.py +64 -0
  6. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/default_config.py +12 -42
  7. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/electron.py +4 -3
  8. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/flight.py +30 -0
  9. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/interface_smmm.py +14 -28
  10. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/main_mfp_sampling.py +64 -18
  11. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/main_tracing.py +131 -40
  12. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/maps.py +148 -18
  13. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/materials.py +320 -102
  14. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/output_info.py +55 -7
  15. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/output_plots.py +18 -32
  16. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/output_structure.py +1 -6
  17. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/particle.py +14 -1
  18. freepaths-2.5.1/freepaths/phonon.py +314 -0
  19. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/run_particle.py +18 -14
  20. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/scatterers.py +4 -194
  21. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/scattering.py +0 -6
  22. freepaths-2.5.1/freepaths/scattering_interfaces.py +84 -0
  23. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/PKG-INFO +1 -2
  24. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/requires.txt +0 -1
  25. {freepaths-2.4.3 → freepaths-2.5.1}/setup.py +1 -1
  26. freepaths-2.4.3/freepaths/phonon.py +0 -167
  27. freepaths-2.4.3/freepaths/scattering_interfaces.py +0 -264
  28. {freepaths-2.4.3 → freepaths-2.5.1}/README.md +0 -0
  29. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/move.py +0 -0
  30. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/options.py +0 -0
  31. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/output_electron_plots.py +0 -0
  32. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/post_computations.py +0 -0
  33. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/progress.py +0 -0
  34. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/scattering_primitives.py +0 -0
  35. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/scattering_semicircle.py +0 -0
  36. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/scattering_types.py +0 -0
  37. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths/sources.py +0 -0
  38. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/SOURCES.txt +0 -0
  39. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/dependency_links.txt +0 -0
  40. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/entry_points.txt +0 -0
  41. {freepaths-2.4.3 → freepaths-2.5.1}/freepaths.egg-info/top_level.txt +0 -0
  42. {freepaths-2.4.3 → freepaths-2.5.1}/pyproject.toml +0 -0
  43. {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.4.3
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
@@ -7,7 +7,7 @@ from colorama import Fore, Style
7
7
 
8
8
  from freepaths.options import SimulationMode
9
9
 
10
- __version__ = "2.4.3"
10
+ __version__ = "2.5.1"
11
11
 
12
12
  colorama.init()
13
13
 
@@ -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/Phonon paths.csv",
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
- # Create a GIF file:
86
- imageio.mimsave("Animated paths XY.gif", images,
87
- duration=1000 * 1/cf.output_animation_fps, subrectangles=True)
88
-
89
- # Create an MP4 file:
90
- # writer = imageio.get_writer('Animated paths XY.mp4', fps=cf.output_animation_fps)
91
-
92
- # for im in images:
93
- # writer.append_data(imageio.imread(im))
94
- # writer.close()
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.use_dispersion_heat_capacity = USE_DISPERSION_HEAT_CAPACITY
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 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):
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 assign_speed(self, material):
130
+ def speed_in(self, material):
131
131
  """
132
- Assign speed to an electron using group velocity and considering parabolic energy-k relation and conduction minimum at 0
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
- self.speed = (2*self.energy/(effective_mass))**(0.5)
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(theta_i)) + Z_j * abs(math.cos(theta_j))) ** 2
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 Si, SiC, Graphite, SiGe
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
- if cf.media == "Si":
82
- material = Si(cf.temp, num_points=cf.number_of_particles + 1)
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
- flight.thermal_conductivity = (1 / (6 * math.pi**2)) * c_p * phonon.speed**2 * mean_relax_time * k_vector**2 * d_k_vector
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
- sys.stdout.write(f"\rMaterial thermal conductivity = {Fore.GREEN}{total_thermal_conductivity:.5f} ± {kappa_sem:.5f}{Style.RESET_ALL} W/m·K\n")
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")