freepaths 2.5.0__tar.gz → 2.6__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 (56) hide show
  1. {freepaths-2.5.0 → freepaths-2.6}/PKG-INFO +16 -6
  2. {freepaths-2.5.0 → freepaths-2.6}/README.md +10 -0
  3. {freepaths-2.5.0 → freepaths-2.6}/freepaths/__main__.py +1 -1
  4. {freepaths-2.5.0 → freepaths-2.6}/freepaths/config.py +0 -1
  5. {freepaths-2.5.0 → freepaths-2.6}/freepaths/data.py +64 -0
  6. {freepaths-2.5.0 → freepaths-2.6}/freepaths/default_config.py +0 -1
  7. {freepaths-2.5.0 → freepaths-2.6}/freepaths/electron.py +4 -3
  8. {freepaths-2.5.0 → freepaths-2.6}/freepaths/flight.py +30 -0
  9. {freepaths-2.5.0 → freepaths-2.6}/freepaths/interface_smmm.py +0 -27
  10. {freepaths-2.5.0 → freepaths-2.6}/freepaths/main_mfp_sampling.py +8 -2
  11. {freepaths-2.5.0 → freepaths-2.6}/freepaths/maps.py +13 -3
  12. {freepaths-2.5.0 → freepaths-2.6}/freepaths/materials.py +15 -5
  13. {freepaths-2.5.0 → freepaths-2.6}/freepaths/output_info.py +55 -7
  14. {freepaths-2.5.0 → freepaths-2.6}/freepaths/output_plots.py +14 -6
  15. {freepaths-2.5.0 → freepaths-2.6}/freepaths/output_structure.py +6 -11
  16. {freepaths-2.5.0 → freepaths-2.6}/freepaths/particle.py +14 -1
  17. {freepaths-2.5.0 → freepaths-2.6}/freepaths/phonon.py +42 -11
  18. {freepaths-2.5.0 → freepaths-2.6}/freepaths/run_particle.py +7 -2
  19. {freepaths-2.5.0 → freepaths-2.6}/freepaths/scatterers.py +48 -237
  20. {freepaths-2.5.0 → freepaths-2.6}/freepaths/scattering.py +33 -11
  21. freepaths-2.6/freepaths/scattering_interfaces.py +88 -0
  22. {freepaths-2.5.0 → freepaths-2.6}/freepaths/scattering_primitives.py +51 -36
  23. {freepaths-2.5.0 → freepaths-2.6}/freepaths/sources.py +3 -2
  24. {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/PKG-INFO +16 -6
  25. {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/SOURCES.txt +15 -1
  26. freepaths-2.6/license +626 -0
  27. {freepaths-2.5.0 → freepaths-2.6}/setup.py +3 -4
  28. freepaths-2.6/tests/test_cauchy_mean_chord.py +115 -0
  29. freepaths-2.6/tests/test_containment.py +84 -0
  30. freepaths-2.6/tests/test_depletion_geometry.py +51 -0
  31. freepaths-2.6/tests/test_dispersion.py +76 -0
  32. freepaths-2.6/tests/test_electrons.py +221 -0
  33. freepaths-2.6/tests/test_end_to_end.py +222 -0
  34. freepaths-2.6/tests/test_free_path.py +88 -0
  35. freepaths-2.6/tests/test_interface_transmission.py +74 -0
  36. freepaths-2.6/tests/test_lambert_scattering.py +114 -0
  37. freepaths-2.6/tests/test_sampling_tables.py +173 -0
  38. freepaths-2.6/tests/test_smmm.py +126 -0
  39. freepaths-2.6/tests/test_soffer.py +99 -0
  40. freepaths-2.6/tests/test_specular_scattering.py +60 -0
  41. freepaths-2.5.0/freepaths/scattering_interfaces.py +0 -264
  42. {freepaths-2.5.0 → freepaths-2.6}/freepaths/animation.py +0 -0
  43. {freepaths-2.5.0 → freepaths-2.6}/freepaths/main_tracing.py +0 -0
  44. {freepaths-2.5.0 → freepaths-2.6}/freepaths/move.py +0 -0
  45. {freepaths-2.5.0 → freepaths-2.6}/freepaths/options.py +0 -0
  46. {freepaths-2.5.0 → freepaths-2.6}/freepaths/output_electron_plots.py +0 -0
  47. {freepaths-2.5.0 → freepaths-2.6}/freepaths/post_computations.py +0 -0
  48. {freepaths-2.5.0 → freepaths-2.6}/freepaths/progress.py +0 -0
  49. {freepaths-2.5.0 → freepaths-2.6}/freepaths/scattering_semicircle.py +0 -0
  50. {freepaths-2.5.0 → freepaths-2.6}/freepaths/scattering_types.py +0 -0
  51. {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/dependency_links.txt +0 -0
  52. {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/entry_points.txt +0 -0
  53. {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/requires.txt +0 -0
  54. {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/top_level.txt +0 -0
  55. {freepaths-2.5.0 → freepaths-2.6}/pyproject.toml +0 -0
  56. {freepaths-2.5.0 → freepaths-2.6}/setup.cfg +0 -0
@@ -1,23 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freepaths
3
- Version: 2.5.0
3
+ Version: 2.6
4
4
  Summary: Phonon Monte Carlo simulator
5
5
  Home-page: https://github.com/anufrievroman/freepaths
6
6
  Author: Roman Anufriev
7
7
  Author-email: anufriev.roman@protonmail.com
8
- License: GPL
8
+ License-Expression: GPL-3.0-only
9
9
  Classifier: Development Status :: 4 - Beta
10
- Classifier: License :: OSI Approved :: GNU General Public License (GPL)
11
10
  Classifier: Environment :: Console
12
11
  Classifier: Natural Language :: English
13
12
  Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3.9
15
- Classifier: Programming Language :: Python :: 3.10
16
13
  Classifier: Programming Language :: Python :: 3.11
17
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
18
16
  Classifier: Topic :: Utilities
19
17
  Requires-Python: ~=3.11
20
18
  Description-Content-Type: text/markdown
19
+ License-File: license
21
20
  Requires-Dist: numpy
22
21
  Requires-Dist: matplotlib
23
22
  Requires-Dist: scipy
@@ -28,7 +27,8 @@ Dynamic: classifier
28
27
  Dynamic: description
29
28
  Dynamic: description-content-type
30
29
  Dynamic: home-page
31
- Dynamic: license
30
+ Dynamic: license-file
31
+ Dynamic: license-expression
32
32
  Dynamic: requires-dist
33
33
  Dynamic: requires-python
34
34
  Dynamic: summary
@@ -94,6 +94,16 @@ Alternatively, you can run FreePATHS in the mean free path sampling mode, which
94
94
  The calculated thermal conductivity will be output in the terminal. However, other statistical quantities and plots will still be calculated and output in the `Results` folder.
95
95
 
96
96
 
97
+ ## Testing
98
+
99
+ The `tests` folder holds the validation tests that must pass before a new version is published. They run against the source tree, not an installed copy. From the repository root:
100
+
101
+ ```
102
+ pip install pytest
103
+ pytest tests
104
+ ```
105
+
106
+
97
107
  ## Troubleshooting
98
108
 
99
109
  - [Troubles with installation](https://anufrievroman.gitbook.io/freepaths/installation)
@@ -59,6 +59,16 @@ Alternatively, you can run FreePATHS in the mean free path sampling mode, which
59
59
  The calculated thermal conductivity will be output in the terminal. However, other statistical quantities and plots will still be calculated and output in the `Results` folder.
60
60
 
61
61
 
62
+ ## Testing
63
+
64
+ The `tests` folder holds the validation tests that must pass before a new version is published. They run against the source tree, not an installed copy. From the repository root:
65
+
66
+ ```
67
+ pip install pytest
68
+ pytest tests
69
+ ```
70
+
71
+
62
72
  ## Troubleshooting
63
73
 
64
74
  - [Troubles with installation](https://anufrievroman.gitbook.io/freepaths/installation)
@@ -7,7 +7,7 @@ from colorama import Fore, Style
7
7
 
8
8
  from freepaths.options import SimulationMode
9
9
 
10
- __version__ = "2.5.0"
10
+ __version__ = "2.6"
11
11
 
12
12
  colorama.init()
13
13
 
@@ -149,7 +149,6 @@ class Config:
149
149
  self.holes = HOLES
150
150
  self.pillars = PILLARS
151
151
  self.interfaces = INTERFACES
152
- self.bulks = BULKS
153
152
 
154
153
  # Multiprocessing:
155
154
  self.num_workers = NUMBER_OF_PROCESSES
@@ -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
 
@@ -129,7 +129,6 @@ INTERFACE_ROUGHNESS = 0.2e-9
129
129
  HOLES = []
130
130
  PILLARS = []
131
131
  INTERFACES = []
132
- BULKS = []
133
132
 
134
133
  # Multiprocessing:
135
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."""
@@ -127,30 +127,3 @@ def alpha_total_2T(theta_i, vg_i, vg_j, rho_i, rho_j, omega_i, omega_j, material
127
127
  T2 = P_j * a_s2 + (1 - P_j) * a_d2
128
128
 
129
129
  return T1 * T2
130
-
131
-
132
- 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):
133
- """Transmission model for 1 transmission : ex: Si → Ge for small rectangle inside single crystal Si"""
134
-
135
- k_i = get_k_from_omega(omega_i, material_i, branch_number)
136
- k_j = get_k_from_omega(omega_j, material_j, branch_number)
137
-
138
- wavelength_i = lambda_from_k(k_i)
139
- wavelength_j = lambda_from_k(k_j)
140
-
141
- P_i = math.exp(-16 * math.pi**2 * roughness**2 / wavelength_i**2)
142
- P_j = math.exp(-16 * math.pi**2 * roughness**2 / wavelength_j**2)
143
-
144
- try:
145
- sin_theta_t = (vg_i / vg_j) * math.sin(theta_i)
146
- if abs(sin_theta_t) > 1:
147
- return 0.0 # Totle reflection at the entry
148
- except:
149
- return 0.0
150
-
151
- a_s = alpha_spec(theta_i, vg_i, vg_j, rho_i, rho_j)
152
- a_d = alpha_diff(k_i, k_j, P_i, P_j)
153
- T_1T = P_i * a_s + (1 - P_i) * a_d
154
-
155
- return T_1T
156
-
@@ -20,7 +20,7 @@ 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
23
+ from freepaths.data import ScatteringData, GeneralData, SegmentData, PathData, TriangleScatteringData, SpecularityData
24
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
@@ -87,6 +87,7 @@ def _run_branch(branch_number, shared_list, shared_progress):
87
87
  places_stats = TriangleScatteringData()
88
88
  path_stats = PathData()
89
89
  scatter_maps = ScatteringMap()
90
+ specularity_stats = SpecularityData()
90
91
 
91
92
  total_thermal_conductivity = 0.0
92
93
  total_kappa_variance = 0.0
@@ -148,6 +149,7 @@ def _run_branch(branch_number, shared_list, shared_progress):
148
149
 
149
150
  general_stats.save_particle_data(phonon, SimulationMode.PHONON_MFP_SAMPLING)
150
151
  general_stats.save_flight_data(flight, SimulationMode.PHONON_MFP_SAMPLING)
152
+ specularity_stats.add_particle(flight)
151
153
 
152
154
  if index < cf.output_trajectories_of_first:
153
155
  path_stats.save_particle_path(flight)
@@ -165,6 +167,7 @@ def _run_branch(branch_number, shared_list, shared_progress):
165
167
  'places_stats': places_stats.dump_data(),
166
168
  'path_stats': path_stats.dump_data(),
167
169
  'scatter_maps': scatter_maps.dump_data(),
170
+ 'specularity_stats': specularity_stats.dump_data(),
168
171
  })
169
172
 
170
173
 
@@ -227,6 +230,7 @@ def main(input_file, mode: SimulationMode):
227
230
  places_stats = TriangleScatteringData()
228
231
  path_stats = PathData()
229
232
  scatter_maps = ScatteringMap()
233
+ specularity_stats = SpecularityData()
230
234
  total_thermal_conductivity = 0.0
231
235
  total_kappa_variance = 0.0
232
236
  total_kappa1 = 0.0
@@ -245,6 +249,7 @@ def main(input_file, mode: SimulationMode):
245
249
  places_stats.read_data(result['places_stats'])
246
250
  path_stats.read_data(result['path_stats'])
247
251
  scatter_maps.read_data(result['scatter_maps'])
252
+ specularity_stats.read_data(result['specularity_stats'])
248
253
 
249
254
  # Create the output folder and copy input file:
250
255
  if not os.path.exists("Results/" + cf.output_folder_name):
@@ -257,6 +262,7 @@ def main(input_file, mode: SimulationMode):
257
262
  # Save data and generate plots:
258
263
  general_stats.write_into_files(mode)
259
264
  scatter_stats.write_into_files()
265
+ specularity_stats.write_into_files()
260
266
  segment_stats.write_into_files()
261
267
  if cf.output_scattering_map:
262
268
  scatter_maps.write_into_files()
@@ -306,7 +312,7 @@ def main(input_file, mode: SimulationMode):
306
312
  plot_data(mode, cf)
307
313
 
308
314
  output_general_information(start_time, general_stats, mode)
309
- output_scattering_information(scatter_stats)
315
+ output_scattering_information(scatter_stats, specularity_stats, mode)
310
316
  output_parameter_warnings(mode, general_stats)
311
317
  sys.stdout.write(f'\rSee the results in {Fore.GREEN}Results/{cf.output_folder_name}{Style.RESET_ALL}\n')
312
318
  if cf.phonon_hydrodynamic:
@@ -5,6 +5,7 @@ from scipy.constants import k as k_B
5
5
  import numpy as np
6
6
  from freepaths.config import cf
7
7
  from freepaths.options import SimulationMode
8
+ import freepaths.move
8
9
 
9
10
  class Maps:
10
11
  """Parent maps class with functions common to all classes below"""
@@ -182,9 +183,18 @@ class ThermalMaps(Maps):
182
183
  This first time step is unique for each phonon. This is done to simulate more relistic continious heat flow.
183
184
  """
184
185
 
186
+ # The phonon is recorded at the middle of the step it is about to make, not at its start.
187
+ # A sample stands for the whole step, and the start of the step is a first-order (left
188
+ # rectangle) rule: in a temperature gradient it undercounts the heat flux by a fraction
189
+ # step / (2 * MFP), which made the Fourier kappa low by -10% at MFP/step = 5. The midpoint
190
+ # is second-order accurate, so the bias disappears (tests/test_end_to_end.py, gray medium):
191
+ d_x, d_y, _ = freepaths.move.step(pt.theta, pt.phi, pt.speed, cf.timestep)
192
+ x = pt.x + d_x / 2
193
+ y = pt.y + d_y / 2
194
+
185
195
  # Calculate the index of the pixel in which we record this phonon:
186
- index_x = int(((pt.x + cf.width / 2) * cf.number_of_pixels_x) // cf.width)
187
- index_y = int(pt.y // (cf.length / cf.number_of_pixels_y))
196
+ index_x = int(((x + cf.width / 2) * cf.number_of_pixels_x) // cf.width)
197
+ index_y = int(y // (cf.length / cf.number_of_pixels_y))
188
198
 
189
199
  # Prevent error if the phonon is outside the structure:
190
200
  if (0 <= index_x < cf.number_of_pixels_x) and (0 <= index_y < cf.number_of_pixels_y):
@@ -215,7 +225,7 @@ class ThermalMaps(Maps):
215
225
  # is formed as the ratio to the thermal map (calculate_drift_velocity).
216
226
  if self.record_momentum:
217
227
  cos_phi = abs(cos(pt.phi))
218
- k_over_omega = material.wavevector(pt.branch_number, pt.f) / (2 * np.pi * pt.f)
228
+ k_over_omega = pt.k / (2 * np.pi * pt.f)
219
229
  p = energy * k_over_omega
220
230
  self.crystal_momentum_map_x[index_y, index_x] += p * sin(pt.theta) * cos_phi
221
231
  self.crystal_momentum_map_y[index_y, index_x] += p * cos(pt.theta) * cos_phi
@@ -123,10 +123,17 @@ class Material(ABC):
123
123
  return d_omega / d_k
124
124
 
125
125
  def wavevector(self, branch_number, f):
126
- """Wavevector magnitude k [1/m] at ordinary frequency f [Hz] on the given branch"""
127
- f_branch = self.dispersion[:, branch_number + 1]
126
+ """
127
+ Wavevector magnitude k [1/m] at ordinary frequency f [Hz] on the given branch,
128
+ taken from the tabulated point closest to f. Nearest-point search rather than
129
+ interpolation for the same reason as in group_velocity(): the SiC/Graphite TA
130
+ branches are non-monotonic in f. The k = 0 point is excluded from the search so
131
+ that a finite frequency can never map onto k = 0, which would make the phonon
132
+ wavelength 2*pi/k infinite.
133
+ """
134
+ f_branch = self.dispersion[1:, branch_number + 1]
128
135
  nearest = np.abs(f_branch - f).argmin()
129
- return self.dispersion[nearest, 0]
136
+ return self.dispersion[nearest + 1, 0]
130
137
 
131
138
  def mean_inverse_phase_velocity_sq(self):
132
139
  """
@@ -198,6 +205,7 @@ class Material(ABC):
198
205
  Assigned attributes (each a list of 3 per-branch arrays, except the
199
206
  branch probabilities, which are single arrays of 3 cumulative values):
200
207
  - frequencies_table: bin-midpoint frequency [Hz] of each dispersion bin;
208
+ - wavevector_table: bin-midpoint wavevector k [1/m] of each bin;
201
209
  - group_velocity_table: group velocity dw/dk [m/s] of each bin;
202
210
  - emission_frequency_probabilities, scattering_frequency_probabilities:
203
211
  cumulative probabilities of the frequency bins within each branch;
@@ -206,6 +214,7 @@ class Material(ABC):
206
214
  a branch before picking a frequency.
207
215
  """
208
216
  self.frequencies_table = []
217
+ self.wavevector_table = []
209
218
  self.group_velocity_table = []
210
219
  self.emission_frequency_probabilities = []
211
220
  self.scattering_frequency_probabilities = []
@@ -234,6 +243,7 @@ class Material(ABC):
234
243
  dos = k_mid[valid]**2 * d_k[valid]
235
244
 
236
245
  self.frequencies_table.append(freqs[valid])
246
+ self.wavevector_table.append(k_mid[valid])
237
247
  self.group_velocity_table.append(group_velocity[valid])
238
248
 
239
249
  # Cumulative bin weights; the last element is the total weight of
@@ -534,7 +544,7 @@ class SiGe(Material):
534
544
  Relaxation time - Umklapp coefficient carried over from the pre-refit Si values
535
545
  (Maire et al., Sci. Rep. 7, 41794 (2017) era), impurity/alloy coefficient an
536
546
  ad-hoc adaptation of the same; NOT refit against bulk SiGe kappa data, unlike
537
- Si (see Data/Fitting_BulkSi/). Treat absolute SiGe kappa values with caution.
547
+ Si (see FreePATHS_Paper/Data/Fitting_BulkSi/). Treat absolute SiGe kappa values with caution.
538
548
  Heat capacity - Fit based on Desai P.D., J. Phys. Chem. Ref. Data 13, 1069 (1984) and Wunderlich, Thermophysical Properties of Materials, Springer (2005)
539
549
  Effective masses - linearly interpolated between Si and Ge at x=0.2 (Schaffler, 2001)
540
550
  """
@@ -590,7 +600,7 @@ class Diamond(Material):
590
600
  momentum-conserving Normal rate. The Umklapp and Normal constants come from a joint
591
601
  Callaway (kappa_1 + kappa_2) fit to the natural type-IIa kappa(T) curve of Onn et al.,
592
602
  PRL 68, 2806 (1992) over 10-400 K plus three anchors from Wei et al., PRL 70, 3764
593
- (1993); see Data/Fitting_BulkDiamond/fit_bulk_diamond_callaway.py.
603
+ (1993); see FreePATHS_Paper/Data/Fitting_BulkDiamond/fit_bulk_diamond_callaway.py.
594
604
  """
595
605
 
596
606
  # Carbon isotope masses [amu] for the Tamura mass-variance parameter (12-C defines
@@ -35,12 +35,12 @@ def output_general_information(start_time, general_stats, mode):
35
35
  f'\nLength = {cf.length * 1e9:.1f} nm',
36
36
  f'\nWidth = {cf.width * 1e9:.1f} nm',
37
37
  f'\nThickness = {cf.thickness * 1e9:.1f} nm\n',
38
- f'\nSide wall roughness = {cf.side_wall_roughness * 1e9:.1f} nm',
39
- f'\nTop roughness = {cf.top_roughness * 1e9:.1f} nm',
40
- f'\nBottom roughness = {cf.bottom_roughness * 1e9:.1f} nm',
41
- *([ f'\nHole roughness = {cf.hole_roughness * 1e9:.1f} nm'] if cf.holes else []),
42
- *([ f'\nInterface roughness = {cf.interface_roughness * 1e9:.1f} nm'] if cf.interfaces else []),
43
- *([ f'\nGrain size = {cf.grain_size * 1e9:.1f} nm, std = {cf.grain_size_std * 1e9:.1f} nm, roughness = {cf.grain_roughness * 1e9:.1f} nm'] if cf.grain_size else []),
38
+ f'\nSide wall roughness = {cf.side_wall_roughness * 1e9:.3g} nm',
39
+ f'\nTop roughness = {cf.top_roughness * 1e9:.3g} nm',
40
+ f'\nBottom roughness = {cf.bottom_roughness * 1e9:.3g} nm',
41
+ *([ f'\nHole roughness = {cf.hole_roughness * 1e9:.3g} nm'] if cf.holes else []),
42
+ *([ f'\nInterface roughness = {cf.interface_roughness * 1e9:.3g} nm'] if cf.interfaces else []),
43
+ *([ f'\nGrain size = {cf.grain_size * 1e9:.1f} nm, std = {cf.grain_size_std * 1e9:.1f} nm, roughness = {cf.grain_roughness * 1e9:.3g} nm'] if cf.grain_size else []),
44
44
  '\n',
45
45
  f'\n{percentage:.0f}% of particles reached the cold side\n'
46
46
  ]
@@ -48,8 +48,51 @@ def output_general_information(start_time, general_stats, mode):
48
48
  file.writelines(info)
49
49
 
50
50
 
51
- def output_scattering_information(scatter_stats):
51
+ def weighted_specularity_information(specularity_stats):
52
+ """
53
+ Specular scattering probability per boundary category (MFP-sampling mode), reported
54
+ two ways: averaged over events, and over each mode's own specular fraction weighted
55
+ by that mode's contribution to kappa.
56
+
57
+ Fed by the running sums in data.SpecularityData, so nothing per-mode is retained.
58
+
59
+ Read the kappa-weighted number. The per-event average answers a different question,
60
+ and in this mode a misleading one. The particles here are not a thermal population:
61
+ _run_branch sweeps the dispersion k-grid with one phonon per k-interval and equal
62
+ weight, and the temperature enters only afterwards, as the heat capacity in the
63
+ kappa integral. So an average over events is an unweighted average over the whole
64
+ Brillouin zone, with no temperature dependence whatsoever - half the modes sit
65
+ beyond k_max/2, where the wavelength is a few angstrom and the Soffer specularity is
66
+ ~0. It is skewed a second time by residence time: a mode that scatters diffusely
67
+ lingers in the structure and accumulates hundreds of events, while a specular one
68
+ streams straight out after a handful.
69
+
70
+ Taking each mode's own specular fraction removes the residence-time bias, and
71
+ weighting those by kappa gives the specularity actually seen by the heat carriers,
72
+ which does depend on temperature (low T shifts the population to long wavelengths,
73
+ where surfaces look smooth).
74
+ """
75
+ from freepaths.flight import BOUNDARY_CATEGORIES
76
+ total_events = specularity_stats.specular_events + specularity_stats.diffuse_events
77
+
78
+ rows = []
79
+ for index, name in enumerate(BOUNDARY_CATEGORIES):
80
+ if total_events[index] == 0:
81
+ continue
82
+ per_event = 100 * specularity_stats.specular_events[index] / total_events[index]
83
+ kappa_total = specularity_stats.kappa_of_scattered[index]
84
+ per_kappa = (100 * specularity_stats.kappa_weighted_specular[index] / kappa_total
85
+ if kappa_total > 0 else float('nan'))
86
+ rows.append(f'\n {name + ":":24s}{per_event:6.2f}% per event, {per_kappa:6.2f}% kappa-weighted')
87
+ if not rows:
88
+ return []
89
+
90
+ return ['\n\nSpecular scattering probability:', *rows, '\n']
91
+
92
+
93
+ def output_scattering_information(scatter_stats, specularity_stats=None, mode=None):
52
94
  """Calculate and output general statistics on scattering events"""
95
+ from freepaths.options import SimulationMode
53
96
 
54
97
  # Calculate the percentage of different scattering events:
55
98
  total_wall = np.sum(scatter_stats.wall_diffuse) + np.sum(scatter_stats.wall_specular)
@@ -131,6 +174,11 @@ def output_scattering_information(scatter_stats):
131
174
  f'{refl_on_interf:.2f}% - reflection',
132
175
  ])
133
176
 
177
+ # Per-mode and kappa-weighted specularity, which the per-event percentages above
178
+ # cannot express (MFP-sampling mode only - it needs the per-mode kappa contributions):
179
+ if mode is SimulationMode.PHONON_MFP_SAMPLING and specularity_stats is not None:
180
+ info.extend(weighted_specularity_information(specularity_stats))
181
+
134
182
  # Write the file:
135
183
  with open("Information.txt", "a", encoding="utf-8") as file:
136
184
  file.writelines(info)
@@ -93,11 +93,16 @@ def launch_exit_frequency_distribution_calculation(number_of_nodes):
93
93
 
94
94
 
95
95
  def wavelength_distribution_calculation(number_of_nodes):
96
- """Calculate particle wavelength distribution from their frequencies and velocities"""
97
- frequencies = np.loadtxt("Data/All final frequencies.csv", encoding='utf-8')
98
- speeds = np.loadtxt("Data/All group velocities.csv", encoding='utf-8')
99
- wavelengths = np.zeros((len(speeds)))
100
- wavelengths[:] = speeds[:] / frequencies[:]
96
+ """Calculate particle wavelength distribution, lam = 2*pi/k (see Phonon.wavelength)"""
97
+ try:
98
+ wavevectors = np.loadtxt("Data/All wavevectors.csv", encoding='utf-8')
99
+ wavelengths = 2 * np.pi / wavevectors
100
+ except OSError:
101
+ # Result folders written before the wavevector was recorded: fall back to the old
102
+ # v_g/f estimate, which underestimates the wavelength away from the Gamma point.
103
+ frequencies = np.loadtxt("Data/All final frequencies.csv", encoding='utf-8')
104
+ speeds = np.loadtxt("Data/All group velocities.csv", encoding='utf-8')
105
+ wavelengths = speeds / frequencies
101
106
  data_range = np.amax(wavelengths)
102
107
  distribution = np.zeros((number_of_nodes, 2))
103
108
  distribution[:, 0] = np.linspace(0, data_range, number_of_nodes)
@@ -490,7 +495,8 @@ def plot_thermal_map():
490
495
  """Plot thermal map as color map"""
491
496
  fig = plt.figure()
492
497
  thermal_map = np.genfromtxt("Data/Thermal map.csv", unpack=False, delimiter=',', skip_header=0, encoding='utf-8')
493
- thermal_map = np.flipud(thermal_map)
498
+ # A map one pixel wide or long is read back as a 1D array, which imshow rejects:
499
+ thermal_map = np.flipud(np.reshape(thermal_map, (cf.number_of_pixels_y, cf.number_of_pixels_x)))
494
500
  minimum_of_colorbar = 1e-20 # Cannot be zero!
495
501
  boundaries = [(-cf.width / 2) * 1e6, (cf.width / 2) * 1e6, 0, cf.length * 1e6]
496
502
  plt.imshow(thermal_map, cmap='hot', interpolation='none', extent=boundaries,
@@ -508,6 +514,8 @@ def plot_heat_flux_map(file, label, units="a.u."):
508
514
  """Plot heat flux map as color map"""
509
515
  fig = plt.figure()
510
516
  heat_flux_map = np.genfromtxt(file, unpack=False, delimiter=',', skip_header=0, encoding='utf-8')
517
+ # A map one pixel wide or long is read back as a 1D array, which imshow rejects:
518
+ heat_flux_map = np.reshape(heat_flux_map, (cf.number_of_pixels_y, cf.number_of_pixels_x))
511
519
  heat_flux_map = np.abs(np.flipud(heat_flux_map))
512
520
  minimum_of_colorbar = 1e5
513
521
  boundaries = [(-cf.width / 2) * 1e6, (cf.width / 2) * 1e6, 0, cf.length * 1e6]
@@ -2,7 +2,7 @@
2
2
 
3
3
  from matplotlib.patches import Rectangle
4
4
  from freepaths.config import cf
5
- from freepaths.scatterers import HorizontalPlane, VerticalPlane, Bulk
5
+ from freepaths.scatterers import VerticalPlane
6
6
 
7
7
  def draw_structure_top_view(cf, color_holes="white", color_back="gray"):
8
8
  """Draw shape of the structure using patches from matplotlib"""
@@ -34,11 +34,6 @@ def draw_structure_top_view(cf, color_holes="white", color_back="gray"):
34
34
  patches.extend(patch if isinstance(patch, list) else [patch])
35
35
 
36
36
 
37
- # Bulks as white patches:
38
- for bulk in cf.bulks:
39
- patch = bulk.get_patch(color_holes, cf)
40
- patches.extend(patch if isinstance(patch, list) else [patch])
41
-
42
37
  # Particle source areas as red patches:
43
38
  for source in cf.particles_sources:
44
39
  width_x = 1e6 * cf.width / 49 if source.size_x == 0 else 1e6 * source.size_x
@@ -66,10 +61,10 @@ def draw_structure_side_view(cf, color_holes="white", color_back="gray"):
66
61
  )
67
62
  ]
68
63
 
69
- # Interfaces as white patches:
70
- for interface in cf.interfaces:
71
- if isinstance(interface, HorizontalPlane):
72
- patch = interface.get_patch(color_holes, cf)
73
- patches.extend(patch if isinstance(patch, list) else [patch])
64
+ # Interfaces as white patches (only HorizontalPlane is visible in this view; it is disabled for now):
65
+ # for interface in cf.interfaces:
66
+ # if isinstance(interface, HorizontalPlane):
67
+ # patch = interface.get_patch(color_holes, cf)
68
+ # patches.extend(patch if isinstance(patch, list) else [patch])
74
69
 
75
70
  return patches