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.
- {freepaths-2.5.0 → freepaths-2.6}/PKG-INFO +16 -6
- {freepaths-2.5.0 → freepaths-2.6}/README.md +10 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/__main__.py +1 -1
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/config.py +0 -1
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/data.py +64 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/default_config.py +0 -1
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/electron.py +4 -3
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/flight.py +30 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/interface_smmm.py +0 -27
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/main_mfp_sampling.py +8 -2
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/maps.py +13 -3
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/materials.py +15 -5
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/output_info.py +55 -7
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/output_plots.py +14 -6
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/output_structure.py +6 -11
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/particle.py +14 -1
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/phonon.py +42 -11
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/run_particle.py +7 -2
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/scatterers.py +48 -237
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/scattering.py +33 -11
- freepaths-2.6/freepaths/scattering_interfaces.py +88 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/scattering_primitives.py +51 -36
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/sources.py +3 -2
- {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/PKG-INFO +16 -6
- {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/SOURCES.txt +15 -1
- freepaths-2.6/license +626 -0
- {freepaths-2.5.0 → freepaths-2.6}/setup.py +3 -4
- freepaths-2.6/tests/test_cauchy_mean_chord.py +115 -0
- freepaths-2.6/tests/test_containment.py +84 -0
- freepaths-2.6/tests/test_depletion_geometry.py +51 -0
- freepaths-2.6/tests/test_dispersion.py +76 -0
- freepaths-2.6/tests/test_electrons.py +221 -0
- freepaths-2.6/tests/test_end_to_end.py +222 -0
- freepaths-2.6/tests/test_free_path.py +88 -0
- freepaths-2.6/tests/test_interface_transmission.py +74 -0
- freepaths-2.6/tests/test_lambert_scattering.py +114 -0
- freepaths-2.6/tests/test_sampling_tables.py +173 -0
- freepaths-2.6/tests/test_smmm.py +126 -0
- freepaths-2.6/tests/test_soffer.py +99 -0
- freepaths-2.6/tests/test_specular_scattering.py +60 -0
- freepaths-2.5.0/freepaths/scattering_interfaces.py +0 -264
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/animation.py +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/main_tracing.py +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/move.py +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/options.py +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/output_electron_plots.py +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/post_computations.py +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/progress.py +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/scattering_semicircle.py +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths/scattering_types.py +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/dependency_links.txt +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/entry_points.txt +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/requires.txt +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/freepaths.egg-info/top_level.txt +0 -0
- {freepaths-2.5.0 → freepaths-2.6}/pyproject.toml +0 -0
- {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.
|
|
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)
|
|
@@ -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
|
|
|
@@ -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."""
|
|
@@ -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(((
|
|
187
|
-
index_y = int(
|
|
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 =
|
|
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
|
-
"""
|
|
127
|
-
|
|
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:.
|
|
39
|
-
f'\nTop roughness = {cf.top_roughness * 1e9:.
|
|
40
|
-
f'\nBottom roughness = {cf.bottom_roughness * 1e9:.
|
|
41
|
-
*([ f'\nHole roughness = {cf.hole_roughness * 1e9:.
|
|
42
|
-
*([ f'\nInterface roughness = {cf.interface_roughness * 1e9:.
|
|
43
|
-
*([ f'\nGrain size = {cf.grain_size * 1e9:.1f} nm, std = {cf.grain_size_std * 1e9:.1f} nm, roughness = {cf.grain_roughness * 1e9:.
|
|
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
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|