freepaths 2.4.2__tar.gz → 2.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {freepaths-2.4.2 → freepaths-2.5.0}/PKG-INFO +1 -2
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/__main__.py +1 -1
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/animation.py +15 -16
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/config.py +12 -1
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/data.py +7 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/default_config.py +12 -41
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/interface_smmm.py +14 -1
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/main_mfp_sampling.py +56 -16
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/main_tracing.py +131 -40
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/maps.py +148 -18
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/materials.py +309 -101
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/output_plots.py +84 -59
- freepaths-2.5.0/freepaths/phonon.py +283 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/run_particle.py +16 -14
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths.egg-info/PKG-INFO +1 -2
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths.egg-info/requires.txt +0 -1
- {freepaths-2.4.2 → freepaths-2.5.0}/setup.py +1 -1
- freepaths-2.4.2/freepaths/phonon.py +0 -167
- {freepaths-2.4.2 → freepaths-2.5.0}/README.md +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/electron.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/flight.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/move.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/options.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/output_electron_plots.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/output_info.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/output_structure.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/particle.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/post_computations.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/progress.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/scatterers.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/scattering.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/scattering_interfaces.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/scattering_primitives.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/scattering_semicircle.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/scattering_types.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths/sources.py +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths.egg-info/SOURCES.txt +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths.egg-info/dependency_links.txt +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths.egg-info/entry_points.txt +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/freepaths.egg-info/top_level.txt +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/pyproject.toml +0 -0
- {freepaths-2.4.2 → freepaths-2.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: freepaths
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Phonon Monte Carlo simulator
|
|
5
5
|
Home-page: https://github.com/anufrievroman/freepaths
|
|
6
6
|
Author: Roman Anufriev
|
|
@@ -21,7 +21,6 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
Requires-Dist: numpy
|
|
22
22
|
Requires-Dist: matplotlib
|
|
23
23
|
Requires-Dist: scipy
|
|
24
|
-
Requires-Dist: imageio
|
|
25
24
|
Requires-Dist: colorama
|
|
26
25
|
Dynamic: author
|
|
27
26
|
Dynamic: author-email
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import sys
|
|
4
4
|
import os
|
|
5
5
|
import shutil
|
|
6
|
-
import imageio
|
|
7
6
|
import numpy as np
|
|
8
7
|
import matplotlib.pyplot as plt
|
|
8
|
+
from PIL import Image
|
|
9
9
|
|
|
10
10
|
from freepaths.config import cf
|
|
11
11
|
from freepaths.output_structure import draw_structure_top_view
|
|
@@ -14,7 +14,7 @@ from freepaths.output_structure import draw_structure_top_view
|
|
|
14
14
|
def generate_frames_xy():
|
|
15
15
|
"""Generate animation frames with phonon paths"""
|
|
16
16
|
|
|
17
|
-
data = np.genfromtxt("Data/
|
|
17
|
+
data = np.genfromtxt("Data/Particle paths.csv",
|
|
18
18
|
unpack=False,
|
|
19
19
|
delimiter=',',
|
|
20
20
|
skip_header=1,
|
|
@@ -77,21 +77,20 @@ def generate_frames_xy():
|
|
|
77
77
|
def generate_animation_xy():
|
|
78
78
|
"""Generate animation of phonon path in XY plane"""
|
|
79
79
|
sys.stdout.write(f"\rAnimation: creating animation file")
|
|
80
|
-
images = []
|
|
81
|
-
filenames = os.listdir("Frames/")
|
|
82
|
-
for filename in filenames:
|
|
83
|
-
images.append(imageio.imread(f"Frames/{filename}"))
|
|
84
80
|
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
# Sort the frames so the GIF plays in the order they were rendered.
|
|
82
|
+
# os.listdir returns entries in arbitrary filesystem order (unsorted on
|
|
83
|
+
# macOS/APFS), which produced out-of-order animations. Filtering to the
|
|
84
|
+
# frame_*.png files also guards against stray files (e.g. .DS_Store).
|
|
85
|
+
filenames = sorted(f for f in os.listdir("Frames/") if f.endswith(".png"))
|
|
86
|
+
|
|
87
|
+
# Assemble the GIF with Pillow (already a matplotlib dependency), so no
|
|
88
|
+
# extra imaging library is required. Frames are converted to a palette
|
|
89
|
+
# image for GIF; duration is milliseconds per frame, loop=0 loops forever.
|
|
90
|
+
frames = [Image.open(f"Frames/{name}").convert("RGB").convert("P", palette=Image.ADAPTIVE)
|
|
91
|
+
for name in filenames]
|
|
92
|
+
frames[0].save("Animated paths XY.gif", save_all=True, append_images=frames[1:],
|
|
93
|
+
duration=1000 / cf.output_animation_fps, loop=0, optimize=True)
|
|
95
94
|
|
|
96
95
|
|
|
97
96
|
def delete_frames():
|
|
@@ -91,6 +91,7 @@ class Config:
|
|
|
91
91
|
self.number_of_virtual_timesteps = NUMBER_OF_VIRTUAL_TIMESTEPS
|
|
92
92
|
self.ignore_faulty_particles = IGNORE_FAULTY_PARTICLES
|
|
93
93
|
self.gradient_fit_range = GRADIENT_FIT_RANGE
|
|
94
|
+
self.temperature_profile_x_range = TEMPERATURE_PROFILE_X_RANGE
|
|
94
95
|
|
|
95
96
|
# Material parameters:
|
|
96
97
|
self.media = MEDIA
|
|
@@ -98,7 +99,12 @@ class Config:
|
|
|
98
99
|
|
|
99
100
|
# Internal scattering:
|
|
100
101
|
self.include_internal_scattering = INCLUDE_INTERNAL_SCATTERING
|
|
101
|
-
self.
|
|
102
|
+
self.phonon_hydrodynamic = PHONON_HYDRODYNAMIC
|
|
103
|
+
self.number_of_hydrodynamic_preruns = NUMBER_OF_HYDRODYNAMIC_PRERUNS
|
|
104
|
+
self.hydrodynamic_preruns_weight = HYDRODYNAMIC_PRERUNS_WEIGHT
|
|
105
|
+
self.number_of_hydrodynamic_prerun_particles = NUMBER_OF_HYDRODYNAMIC_PRERUN_PARTICLES
|
|
106
|
+
self.hydrodynamic_normal_resistive = HYDRODYNAMIC_NORMAL_RESISTIVE
|
|
107
|
+
self.isotope_c13_concentration = ISOTOPE_C13_CONCENTRATION
|
|
102
108
|
self.max_number_of_scattering_events = MAX_NUMBER_OF_SCATTERING_EVENTS
|
|
103
109
|
self.grain_size = GRAIN_SIZE
|
|
104
110
|
self.grain_size_std = GRAIN_SIZE_STD
|
|
@@ -171,6 +177,11 @@ class Config:
|
|
|
171
177
|
f"See the documentation at {WEBSITE}")
|
|
172
178
|
sys.exit()
|
|
173
179
|
|
|
180
|
+
if not (0.0 <= self.temperature_profile_x_range[0] < self.temperature_profile_x_range[1] <= 1.0):
|
|
181
|
+
logging.error("Parameter TEMPERATURE_PROFILE_X_RANGE must be a pair (start, end) with 0 <= start < end <= 1.\n" +
|
|
182
|
+
f"See the documentation at {WEBSITE}")
|
|
183
|
+
sys.exit()
|
|
184
|
+
|
|
174
185
|
if self.number_of_timeframes <= self.number_of_stabilization_timeframes:
|
|
175
186
|
logging.error("Parameter NUMBER_OF_STABILIZATION_TIMEFRAMES exceeds or equal to NUMBER_OF_TIMEFRAMES.\n" +
|
|
176
187
|
"Leave at least one timeframe after NUMBER_OF_STABILIZATION_TIMEFRAMES.\n" +
|
|
@@ -74,6 +74,10 @@ class GeneralData(Data):
|
|
|
74
74
|
# the two channels of the total rate v/MFP resolved as a function of frequency:
|
|
75
75
|
self.internal_scattering_rates = []
|
|
76
76
|
self.boundary_scattering_rates = []
|
|
77
|
+
# Per-phonon dispersion branch index (0=LA, 1=TA1, 2=TA2, ...). In MFP-sampling
|
|
78
|
+
# mode each phonon keeps its launch branch (no rethermalization), so this lets the
|
|
79
|
+
# scattering-rate spectra be resolved per branch. Row-aligned with the arrays above.
|
|
80
|
+
self.branches = []
|
|
77
81
|
def save_particle_data(self, pt, mode=None):
|
|
78
82
|
"""Add information about the particle to the dataset"""
|
|
79
83
|
from freepaths.options import SimulationMode
|
|
@@ -82,6 +86,7 @@ class GeneralData(Data):
|
|
|
82
86
|
else:
|
|
83
87
|
self.final_frequencies.append(pt.f)
|
|
84
88
|
self.group_velocities.append(pt.speed)
|
|
89
|
+
self.branches.append(pt.branch_number)
|
|
85
90
|
|
|
86
91
|
def save_flight_data(self, flight, mode=None):
|
|
87
92
|
"""Add information about the particle flight to the dataset"""
|
|
@@ -129,6 +134,7 @@ class GeneralData(Data):
|
|
|
129
134
|
np.savetxt("Data/All thermal conductivities.csv", self.thermal_conductivity, fmt='%2.4e', header="K [W/mK]", encoding='utf-8')
|
|
130
135
|
np.savetxt("Data/All internal scattering rates.csv", self.internal_scattering_rates, fmt='%2.4e', header="1/tau_internal [1/s]", encoding='utf-8')
|
|
131
136
|
np.savetxt("Data/All boundary scattering rates.csv", self.boundary_scattering_rates, fmt='%2.4e', header="1/tau_boundary [1/s]", encoding='utf-8')
|
|
137
|
+
np.savetxt("Data/All branches.csv", self.branches, fmt='%d', header="Branch index (0=LA, 1=TA1, 2=TA2)", encoding='utf-8')
|
|
132
138
|
def dump_data(self):
|
|
133
139
|
"""Return data of a process in the form of a dictionary to be attached to the global data"""
|
|
134
140
|
return {
|
|
@@ -146,6 +152,7 @@ class GeneralData(Data):
|
|
|
146
152
|
'thermal_conductivity': self.thermal_conductivity,
|
|
147
153
|
'internal_scattering_rates': self.internal_scattering_rates,
|
|
148
154
|
'boundary_scattering_rates': self.boundary_scattering_rates,
|
|
155
|
+
'branches': self.branches,
|
|
149
156
|
'initial_energies': self.initial_energies,
|
|
150
157
|
}
|
|
151
158
|
|
|
@@ -55,9 +55,6 @@ MEAN_MAPPING_CONSTANT = 5e-6 # [m²]
|
|
|
55
55
|
|
|
56
56
|
FERMI_LEVEL_LOWER_BOUND = -0.2 # [eV] lower end of post-processing Fermi level sweep
|
|
57
57
|
FERMI_LEVEL_UPPER_BOUND = 0.1 # [eV] upper end of post-processing Fermi level sweep;
|
|
58
|
-
# far above the band edge the Fermi window leaves the sampled energy range and
|
|
59
|
-
# the MC results become noisy, so there is little point in sweeping further
|
|
60
|
-
|
|
61
58
|
|
|
62
59
|
# Animation:
|
|
63
60
|
OUTPUT_PATH_ANIMATION = False
|
|
@@ -68,6 +65,7 @@ NUMBER_OF_PIXELS_X = 7
|
|
|
68
65
|
NUMBER_OF_PIXELS_Y = 67
|
|
69
66
|
IGNORE_FAULTY_PARTICLES = False
|
|
70
67
|
GRADIENT_FIT_RANGE = (0.1, 0.9)
|
|
68
|
+
TEMPERATURE_PROFILE_X_RANGE = (0.0, 1.0)
|
|
71
69
|
|
|
72
70
|
# Material parameters:
|
|
73
71
|
MEDIA = "Si"
|
|
@@ -76,47 +74,20 @@ MEDIA_FERMI_LEVEL = None
|
|
|
76
74
|
# Internal scattering:
|
|
77
75
|
INCLUDE_INTERNAL_SCATTERING = True
|
|
78
76
|
|
|
79
|
-
# Phonon
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# Convert deposited particle energy into the temperature profile using the
|
|
91
|
-
# dispersion-only heat capacity (Material.dispersion_heat_capacity, summed only
|
|
92
|
-
# over the branches in the tabulated dispersion) instead of the experimental
|
|
93
|
-
# heat capacity fit (Material.assign_heat_capacity). On by default because it
|
|
94
|
-
# makes the reported temperature and kappa self-consistent with the
|
|
95
|
-
# dispersion-based sampling/rethermalization scheme and the model's own RTA
|
|
96
|
-
# integral. Set to False to use the real material heat capacity instead (e.g.
|
|
97
|
-
# including optical branches absent from the tabulated dispersion):
|
|
98
|
-
USE_DISPERSION_HEAT_CAPACITY = True
|
|
99
|
-
|
|
100
|
-
# MFP sampling mode only (no effect in phonon tracing, where particles must keep
|
|
101
|
-
# going for the full budget to build the flux map): end a phonon's flight early
|
|
102
|
-
# once it has accumulated this many free-path segments, instead of always running
|
|
103
|
-
# to NUMBER_OF_TIMESTEPS. A short-tau, low-velocity phonon takes tiny hops and
|
|
104
|
-
# diffuses so slowly that it essentially never reaches a domain boundary, but its
|
|
105
|
-
# mean free path already converges after a modest number of scattering events, so
|
|
106
|
-
# running it to the full timestep budget is wasted compute. 1000 is on by default,
|
|
107
|
-
# since it was validated (bulk Si MFP-sampling convergence study, Data/BulkSi_Phonon_MFP/)
|
|
108
|
-
# to give the same result as 5000 while being far cheaper. Set to None to disable
|
|
109
|
-
# (run to the timestep budget or a boundary, as before):
|
|
77
|
+
# Phonon hydrodynamic transport.
|
|
78
|
+
PHONON_HYDRODYNAMIC = False
|
|
79
|
+
NUMBER_OF_HYDRODYNAMIC_PRERUNS = 5
|
|
80
|
+
HYDRODYNAMIC_PRERUNS_WEIGHT = 0.8
|
|
81
|
+
NUMBER_OF_HYDRODYNAMIC_PRERUN_PARTICLES = None
|
|
82
|
+
|
|
83
|
+
# Diagnostic control: when True, Normal events still fire but redraw the direction
|
|
84
|
+
# randomly instead of biased toward the local drift
|
|
85
|
+
HYDRODYNAMIC_NORMAL_RESISTIVE = False
|
|
86
|
+
ISOTOPE_C13_CONCENTRATION = 0.0
|
|
87
|
+
|
|
110
88
|
MAX_NUMBER_OF_SCATTERING_EVENTS = 1000
|
|
111
89
|
|
|
112
90
|
# Grain boundary scattering (polycrystalline materials):
|
|
113
|
-
# GRAIN_SIZE sets the mean grain diameter [m]. None disables grain boundary scattering entirely.
|
|
114
|
-
# GRAIN_SIZE_STD sets the standard deviation [m]; the grain size is drawn per phonon from a
|
|
115
|
-
# lognormal distribution with this mean and std. Set to 0 for monodisperse (single grain size).
|
|
116
|
-
# GRAIN_ROUGHNESS is the RMS disorder width of the grain boundary [m], used in a Soffer-type
|
|
117
|
-
# specularity factor: at low frequencies (long wavelengths) the boundary appears smooth and
|
|
118
|
-
# phonons pass through; at high frequencies it acts as a classical diffuse scatterer.
|
|
119
|
-
# Typical values: GRAIN_SIZE 100 nm – 10 µm, GRAIN_ROUGHNESS 0.5–2 nm.
|
|
120
91
|
GRAIN_SIZE = None
|
|
121
92
|
GRAIN_SIZE_STD = 0.0
|
|
122
93
|
GRAIN_ROUGHNESS = 1e-9
|
|
@@ -31,6 +31,19 @@ def alpha_spec(theta_i, vg_i, vg_j, rho_i, rho_j):
|
|
|
31
31
|
"""
|
|
32
32
|
Specular transmission coefficient (AMM) between two materials.
|
|
33
33
|
|
|
34
|
+
Ran & Cao, PRB 110, 024302 (2024), Eq. (20):
|
|
35
|
+
|
|
36
|
+
alpha = 4*A*B / (A + B)**2, A = Z_j/Z_i, B = cos(theta_j)/cos(theta_i)
|
|
37
|
+
|
|
38
|
+
which is the standard oblique-incidence acoustic result 4*W_i*W_j/(W_i + W_j)**2
|
|
39
|
+
with the angle-corrected impedances W = Z/cos(theta), i.e. the COSINES IN THE
|
|
40
|
+
DENOMINATOR ARE CROSSED: (Z_i*cos(theta_j) + Z_j*cos(theta_i))**2. Writing them
|
|
41
|
+
uncrossed agrees at normal incidence but over-estimates transmission at oblique
|
|
42
|
+
incidence (Si/SiGe LA at 1 THz: 0.990 vs 0.928 at 60 deg).
|
|
43
|
+
|
|
44
|
+
Note this is Eq. (20), not Eq. (25): the MTM detailed-balance prefactor
|
|
45
|
+
min{1, P_j*C_j*vg_j**3 / (P_i*C_i*vg_i**3)} of Eq. (25) is deliberately not applied.
|
|
46
|
+
|
|
34
47
|
Parameters:
|
|
35
48
|
theta_i -- incidence angle
|
|
36
49
|
vg_i, vg_j -- group velocities of media i and j
|
|
@@ -49,7 +62,7 @@ def alpha_spec(theta_i, vg_i, vg_j, rho_i, rho_j):
|
|
|
49
62
|
Z_j = rho_j * vg_j
|
|
50
63
|
|
|
51
64
|
num = 4 * Z_i * Z_j * abs(math.cos(theta_i)) * abs(math.cos(theta_j))
|
|
52
|
-
den = (Z_i * abs(math.cos(
|
|
65
|
+
den = (Z_i * abs(math.cos(theta_j)) + Z_j * abs(math.cos(theta_i))) ** 2
|
|
53
66
|
return num / den
|
|
54
67
|
except:
|
|
55
68
|
return 0.0
|
|
@@ -21,10 +21,9 @@ from freepaths.phonon import Phonon
|
|
|
21
21
|
from freepaths.flight import Flight
|
|
22
22
|
from freepaths.options import SimulationMode
|
|
23
23
|
from freepaths.data import ScatteringData, GeneralData, SegmentData, PathData, TriangleScatteringData
|
|
24
|
-
from freepaths.materials import
|
|
24
|
+
from freepaths.materials import create_material, get_media_class
|
|
25
25
|
from freepaths.maps import ScatteringMap
|
|
26
26
|
from freepaths.output_info import output_general_information, output_scattering_information, output_parameter_warnings
|
|
27
|
-
from freepaths.materials import get_media_class
|
|
28
27
|
from freepaths.output_plots import plot_data
|
|
29
28
|
|
|
30
29
|
|
|
@@ -78,17 +77,8 @@ def _run_branch(branch_number, shared_list, shared_progress):
|
|
|
78
77
|
# Each worker builds its own material instance so the dispersion is
|
|
79
78
|
# sampled with the correct number of points (number_of_particles + 1
|
|
80
79
|
# intervals → number_of_particles midpoints):
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
elif cf.media == "SiGe":
|
|
84
|
-
material = SiGe(cf.temp, num_points=cf.number_of_particles + 1)
|
|
85
|
-
elif cf.media == "SiC":
|
|
86
|
-
material = SiC(cf.temp, num_points=cf.number_of_particles + 1)
|
|
87
|
-
elif cf.media == "Graphite":
|
|
88
|
-
material = Graphite(cf.temp, num_points=cf.number_of_particles + 1)
|
|
89
|
-
else:
|
|
90
|
-
logging.error(f"Material {cf.media} is not supported")
|
|
91
|
-
return
|
|
80
|
+
material = create_material(cf.media, cf.temp, num_points=cf.number_of_particles + 1,
|
|
81
|
+
isotope_c13_concentration=cf.isotope_c13_concentration)
|
|
92
82
|
|
|
93
83
|
# Local data structures — merged into the global ones after all workers finish:
|
|
94
84
|
scatter_stats = ScatteringData()
|
|
@@ -100,6 +90,9 @@ def _run_branch(branch_number, shared_list, shared_progress):
|
|
|
100
90
|
|
|
101
91
|
total_thermal_conductivity = 0.0
|
|
102
92
|
total_kappa_variance = 0.0
|
|
93
|
+
total_kappa1 = 0.0 # Callaway kappa1 accumulator (uses combined tau_C)
|
|
94
|
+
total_k2_num = 0.0 # Callaway kappa2 numerator sum: w * tau_C / tau_N
|
|
95
|
+
total_k2_den = 0.0 # Callaway kappa2 denominator sum: w * tau_C / (tau_N * tau_R)
|
|
103
96
|
last_pct = -1
|
|
104
97
|
|
|
105
98
|
for index in range(cf.number_of_particles):
|
|
@@ -122,11 +115,31 @@ def _run_branch(branch_number, shared_list, shared_progress):
|
|
|
122
115
|
part = scipy.constants.hbar * omega / (scipy.constants.k * cf.temp)
|
|
123
116
|
c_p = scipy.constants.k * part**2 * math.exp(part) / (math.exp(part) - 1)**2
|
|
124
117
|
|
|
125
|
-
# Thermal conductivity contribution from this k-point (Ref. Phys. Rev. 132 2461, 1963)
|
|
118
|
+
# Thermal conductivity contribution from this k-point (Ref. Phys. Rev. 132 2461, 1963).
|
|
119
|
+
# The measured mean free path is limited by resistive (Umklapp + isotope) and boundary
|
|
120
|
+
# scattering, so mean_relax_time is the Callaway resistive time tau_R (Normal scattering
|
|
121
|
+
# does not fire in the MFP-sampling mode). weight is the common per-mode integrand factor.
|
|
126
122
|
mean_relax_time = flight.mean_free_path / phonon.speed
|
|
127
|
-
|
|
123
|
+
weight = (1 / (6 * math.pi**2)) * c_p * phonon.speed**2 * k_vector**2 * d_k_vector
|
|
124
|
+
flight.thermal_conductivity = weight * mean_relax_time # single-mode RTA (SMRT)
|
|
128
125
|
total_thermal_conductivity += flight.thermal_conductivity
|
|
129
126
|
|
|
127
|
+
# Callaway dual-relaxation correction (Callaway, Phys. Rev. 113, 1046 (1959)):
|
|
128
|
+
# kappa = kappa1 + kappa2, with 1/tau_C = 1/tau_R + 1/tau_N. kappa1 uses the combined
|
|
129
|
+
# tau_C (Normal provisionally counted as resistive); kappa2 is the momentum-recovery term
|
|
130
|
+
# kappa2 = (sum w*tau_C/tau_N)^2 / (sum w*tau_C/(tau_N*tau_R)), accumulated linearly here
|
|
131
|
+
# and combined in main(). tau_C/tau_N = tau_C*normal_rate. Only computed when
|
|
132
|
+
# PHONON_HYDRODYNAMIC is on (the Normal / momentum-recovery treatment); otherwise
|
|
133
|
+
# normal_rate = 0 and kappa1 collapses to the standard SMRT conductivity.
|
|
134
|
+
normal_rate = material.phonon_normal_rate(omega) if cf.phonon_hydrodynamic else 0.0
|
|
135
|
+
if normal_rate > 0 and mean_relax_time > 0:
|
|
136
|
+
tau_C = 1.0 / (1.0 / mean_relax_time + normal_rate)
|
|
137
|
+
total_kappa1 += weight * tau_C
|
|
138
|
+
total_k2_num += weight * tau_C * normal_rate
|
|
139
|
+
total_k2_den += weight * tau_C * normal_rate / mean_relax_time
|
|
140
|
+
else:
|
|
141
|
+
total_kappa1 += weight * mean_relax_time
|
|
142
|
+
|
|
130
143
|
# This mode's kappa is linear in its mean free path, so its variance
|
|
131
144
|
# propagates directly through the same proportionality (kappa_i / mfp_i):
|
|
132
145
|
if flight.mean_free_path > 0:
|
|
@@ -143,6 +156,9 @@ def _run_branch(branch_number, shared_list, shared_progress):
|
|
|
143
156
|
shared_list.append({
|
|
144
157
|
'total_thermal_conductivity': total_thermal_conductivity,
|
|
145
158
|
'total_kappa_variance': total_kappa_variance,
|
|
159
|
+
'total_kappa1': total_kappa1,
|
|
160
|
+
'total_k2_num': total_k2_num,
|
|
161
|
+
'total_k2_den': total_k2_den,
|
|
146
162
|
'scatter_stats': scatter_stats.dump_data(),
|
|
147
163
|
'general_stats': general_stats.dump_data(),
|
|
148
164
|
'segment_stats': segment_stats.dump_data(),
|
|
@@ -213,10 +229,16 @@ def main(input_file, mode: SimulationMode):
|
|
|
213
229
|
scatter_maps = ScatteringMap()
|
|
214
230
|
total_thermal_conductivity = 0.0
|
|
215
231
|
total_kappa_variance = 0.0
|
|
232
|
+
total_kappa1 = 0.0
|
|
233
|
+
total_k2_num = 0.0
|
|
234
|
+
total_k2_den = 0.0
|
|
216
235
|
|
|
217
236
|
for result in shared_list:
|
|
218
237
|
total_thermal_conductivity += result['total_thermal_conductivity']
|
|
219
238
|
total_kappa_variance += result['total_kappa_variance']
|
|
239
|
+
total_kappa1 += result['total_kappa1']
|
|
240
|
+
total_k2_num += result['total_k2_num']
|
|
241
|
+
total_k2_den += result['total_k2_den']
|
|
220
242
|
scatter_stats.read_data(result['scatter_stats'])
|
|
221
243
|
general_stats.read_data(result['general_stats'])
|
|
222
244
|
segment_stats.read_data(result['segment_stats'])
|
|
@@ -267,6 +289,19 @@ def main(input_file, mode: SimulationMode):
|
|
|
267
289
|
fmt='%2.4e', delimiter=',',
|
|
268
290
|
header="K_material SEM [W/mK], K_effective SEM [W/mK]", encoding='utf-8')
|
|
269
291
|
|
|
292
|
+
# Callaway dual-relaxation conductivity kappa = kappa1 + kappa2 (momentum-recovery term).
|
|
293
|
+
# Only written in hydrodynamic mode (the Normal-process treatment); in the standard RTA the
|
|
294
|
+
# -s output is unchanged. Kept in a separate file so the positional indexers of
|
|
295
|
+
# "Thermal conductivity from MFP.csv" are not disturbed. See main_mfp_sampling._run_branch.
|
|
296
|
+
kappa2 = kappa_callaway = None
|
|
297
|
+
if cf.phonon_hydrodynamic:
|
|
298
|
+
kappa2 = (total_k2_num ** 2 / total_k2_den) if total_k2_den > 0 else 0.0
|
|
299
|
+
kappa_callaway = total_kappa1 + kappa2
|
|
300
|
+
np.savetxt("Data/Thermal conductivity Callaway.csv",
|
|
301
|
+
np.array([[total_kappa1, kappa2, kappa_callaway]]),
|
|
302
|
+
fmt='%2.4e', delimiter=',',
|
|
303
|
+
header="kappa1 [W/mK], kappa2 [W/mK], kappa_Callaway [W/mK]", encoding='utf-8')
|
|
304
|
+
|
|
270
305
|
sys.stdout.write("\rAnalyzing the data...")
|
|
271
306
|
plot_data(mode, cf)
|
|
272
307
|
|
|
@@ -274,7 +309,12 @@ def main(input_file, mode: SimulationMode):
|
|
|
274
309
|
output_scattering_information(scatter_stats)
|
|
275
310
|
output_parameter_warnings(mode, general_stats)
|
|
276
311
|
sys.stdout.write(f'\rSee the results in {Fore.GREEN}Results/{cf.output_folder_name}{Style.RESET_ALL}\n')
|
|
277
|
-
|
|
312
|
+
if cf.phonon_hydrodynamic:
|
|
313
|
+
sys.stdout.write(f"\rMaterial thermal conductivity (SMRT) = {Fore.GREEN}{total_thermal_conductivity:.5f} ± {kappa_sem:.5f}{Style.RESET_ALL} W/m·K\n")
|
|
314
|
+
sys.stdout.write(f"\rMaterial thermal conductivity (Callaway) = {Fore.GREEN}{kappa_callaway:.5f}{Style.RESET_ALL} W/m·K "
|
|
315
|
+
f"(kappa1 {total_kappa1:.2f} + kappa2 {kappa2:.2f})\n")
|
|
316
|
+
else:
|
|
317
|
+
sys.stdout.write(f"\rMaterial thermal conductivity = {Fore.GREEN}{total_thermal_conductivity:.5f} ± {kappa_sem:.5f}{Style.RESET_ALL} W/m·K\n")
|
|
278
318
|
if cf.holes:
|
|
279
319
|
sys.stdout.write(f"\rEffective thermal conductivity = {Fore.GREEN}{effective_thermal_conductivity:.5f} ± {effective_kappa_sem:.5f}{Style.RESET_ALL} W/m·K "
|
|
280
320
|
f"(porosity {porosity:.1%}, Eucken factor {eucken_factor:.3f})\n")
|
|
@@ -21,8 +21,8 @@ from freepaths.options import SimulationMode
|
|
|
21
21
|
from freepaths.data import ScatteringData, GeneralData, SegmentData, PathData, TriangleScatteringData
|
|
22
22
|
from freepaths.post_computations import ElectronPostComputation
|
|
23
23
|
from freepaths.progress import Progress
|
|
24
|
-
from freepaths.materials import
|
|
25
|
-
from freepaths.maps import ScatteringMap, ThermalMaps
|
|
24
|
+
from freepaths.materials import create_material
|
|
25
|
+
from freepaths.maps import ScatteringMap, ThermalMaps, DriftField
|
|
26
26
|
from freepaths.output_info import output_general_information, output_scattering_information, output_parameter_warnings, output_electron_information
|
|
27
27
|
from freepaths.animation import create_animation
|
|
28
28
|
from freepaths.output_plots import plot_data
|
|
@@ -35,20 +35,15 @@ class ParticleSimulator:
|
|
|
35
35
|
It is meant to be used as a worker for multiprocessing
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
def __init__(self, worker_id, mode: SimulationMode, total_particles, shared_list, output_trajectories_of):
|
|
38
|
+
def __init__(self, worker_id, mode: SimulationMode, total_particles, shared_list, output_trajectories_of, drift_field=None):
|
|
39
39
|
|
|
40
40
|
# Initialize the material:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
elif cf.media == "Graphite":
|
|
48
|
-
self.material = Graphite(cf.temp)
|
|
49
|
-
else:
|
|
50
|
-
logging.error(f"Material {cf.media} is not supported")
|
|
51
|
-
sys.exit()
|
|
41
|
+
self.material = create_material(cf.media, cf.temp,
|
|
42
|
+
isotope_c13_concentration=cf.isotope_c13_concentration)
|
|
43
|
+
|
|
44
|
+
# Frozen hydrodynamic drift field for this pass (None in the bootstrap pass and
|
|
45
|
+
# in non-hydrodynamic runs); read by momentum-conserving Normal scattering events:
|
|
46
|
+
self.material.drift_field = drift_field
|
|
52
47
|
|
|
53
48
|
# Carrier surface-depletion dead-layer width (0 unless SURFACE_POTENTIAL and doping are set):
|
|
54
49
|
cf.depletion_width = depletion_width(self.material)
|
|
@@ -130,11 +125,11 @@ class ParticleSimulator:
|
|
|
130
125
|
self.result_queue.append(collected_data)
|
|
131
126
|
|
|
132
127
|
|
|
133
|
-
def worker_process(worker_id, mode: SimulationMode, total_particles, shared_list, output_trajectories_of, finished_workers):
|
|
128
|
+
def worker_process(worker_id, mode: SimulationMode, total_particles, shared_list, output_trajectories_of, finished_workers, drift_field=None):
|
|
134
129
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
135
130
|
try:
|
|
136
131
|
# Create a particle simulator and run the simulation:
|
|
137
|
-
simulator = ParticleSimulator(worker_id, mode, total_particles, shared_list, output_trajectories_of)
|
|
132
|
+
simulator = ParticleSimulator(worker_id, mode, total_particles, shared_list, output_trajectories_of, drift_field)
|
|
138
133
|
simulator.simulate_particles(render_progress=1 if worker_id == 0 else 0)
|
|
139
134
|
|
|
140
135
|
# Declare that the calculation is finished:
|
|
@@ -165,25 +160,24 @@ def display_workers_finished(finished_workers):
|
|
|
165
160
|
pass
|
|
166
161
|
|
|
167
162
|
|
|
168
|
-
def
|
|
163
|
+
def run_pass(mode: SimulationMode, drift_field=None, total_particles=None):
|
|
169
164
|
"""
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
Launch all worker processes for one full sweep of the ensemble (one "pass"), with
|
|
166
|
+
the given frozen drift field, and return the collected result_list. In a
|
|
167
|
+
non-hydrodynamic run this is called exactly once; in hydrodynamic mode it is called
|
|
168
|
+
once per self-consistency pass, each time with the drift field frozen at the value
|
|
169
|
+
converged from the previous pass. total_particles overrides cf.number_of_particles
|
|
170
|
+
for this pass (used to run cheaper preruns than the reported run).
|
|
172
171
|
"""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
start_time = time.time()
|
|
176
|
-
|
|
177
|
-
# Create manager for managing variable acces for multiple workers:
|
|
172
|
+
if total_particles is None:
|
|
173
|
+
total_particles = cf.number_of_particles
|
|
178
174
|
manager = multiprocessing.Manager()
|
|
179
|
-
|
|
180
|
-
# These variables created with the manager can safely be accessed by multiple workers:
|
|
181
175
|
shared_list = manager.list()
|
|
182
176
|
finished_workers = manager.Value('i', 0)
|
|
183
177
|
|
|
184
178
|
# Divide all the particles among the workers:
|
|
185
|
-
workload_per_worker =
|
|
186
|
-
remaining_particles =
|
|
179
|
+
workload_per_worker = total_particles // cf.num_workers
|
|
180
|
+
remaining_particles = total_particles % cf.num_workers
|
|
187
181
|
|
|
188
182
|
# Divide number of output trajectories to save among workers:
|
|
189
183
|
output_trajectories_per_worker = cf.output_trajectories_of_first // cf.num_workers
|
|
@@ -196,7 +190,7 @@ def main(input_file, mode: SimulationMode):
|
|
|
196
190
|
for i in range(cf.num_workers):
|
|
197
191
|
worker_particles = workload_per_worker + (1 if i < remaining_particles else 0)
|
|
198
192
|
output_trajectory_of = output_trajectories_per_worker + (1 if i < remaining_output_trajectories else 0)
|
|
199
|
-
process = multiprocessing.Process(target=worker_process, args=(i, mode, worker_particles, shared_list, output_trajectory_of, finished_workers))
|
|
193
|
+
process = multiprocessing.Process(target=worker_process, args=(i, mode, worker_particles, shared_list, output_trajectory_of, finished_workers, drift_field))
|
|
200
194
|
processes.append(process)
|
|
201
195
|
process.start()
|
|
202
196
|
|
|
@@ -217,9 +211,11 @@ def main(input_file, mode: SimulationMode):
|
|
|
217
211
|
# Wait for the worker count to finish but continue after 3 seconds:
|
|
218
212
|
worker_count_process.join(timeout=3)
|
|
219
213
|
worker_count_process.terminate() # should not be necessary but sometimes process does not terminate
|
|
214
|
+
return list(shared_list)
|
|
220
215
|
|
|
221
|
-
|
|
222
|
-
|
|
216
|
+
|
|
217
|
+
def merge_results(result_list, mode: SimulationMode):
|
|
218
|
+
"""Merge the per-worker data dictionaries from one pass into unified data structures."""
|
|
223
219
|
scatter_stats = ScatteringData()
|
|
224
220
|
places_stats = TriangleScatteringData()
|
|
225
221
|
general_stats = GeneralData()
|
|
@@ -228,17 +224,9 @@ def main(input_file, mode: SimulationMode):
|
|
|
228
224
|
scatter_maps = ScatteringMap()
|
|
229
225
|
thermal_maps = ThermalMaps() if mode is SimulationMode.PHONON_TRACING else None
|
|
230
226
|
|
|
231
|
-
# Collect the results:
|
|
232
|
-
sys.stdout.write('\nCollecting data from workers...\r')
|
|
233
|
-
|
|
234
|
-
# Convert the shared list to a normal list so it's easier to use
|
|
235
|
-
result_list = list(shared_list)
|
|
236
|
-
|
|
237
|
-
# Check that all workers actually returned some data
|
|
238
227
|
if len(result_list) != cf.num_workers:
|
|
239
228
|
sys.stdout.write(f'WARNING: of {cf.num_workers} workers only the results of {len(result_list)} were collected\n')
|
|
240
229
|
|
|
241
|
-
# Put the data from every worker into it's respective place:
|
|
242
230
|
execution_time_list = []
|
|
243
231
|
for collected_data in result_list:
|
|
244
232
|
scatter_stats.read_data(collected_data['scatter_stats'])
|
|
@@ -251,12 +239,103 @@ def main(input_file, mode: SimulationMode):
|
|
|
251
239
|
thermal_maps.read_data(collected_data['thermal_maps'])
|
|
252
240
|
execution_time_list.append(collected_data['execution_time'])
|
|
253
241
|
|
|
242
|
+
return scatter_stats, places_stats, general_stats, segment_stats, path_stats, scatter_maps, thermal_maps, execution_time_list
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def main(input_file, mode: SimulationMode):
|
|
246
|
+
"""
|
|
247
|
+
This is the main function.
|
|
248
|
+
It should be used to simulate electron paths or phonon paths at low temperatures.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
sys.stdout.write(f'Simulation of {Fore.GREEN}{cf.output_folder_name}{Style.RESET_ALL}\n')
|
|
252
|
+
start_time = time.time()
|
|
253
|
+
|
|
254
|
+
# Hydrodynamic mode first traces the ensemble in NUMBER_OF_HYDRODYNAMIC_PRERUNS
|
|
255
|
+
# preruns that only build the self-consistent drift field (frozen within each prerun,
|
|
256
|
+
# under-relaxed between them), then a final reported run that reads the converged field
|
|
257
|
+
# and supplies the reported statistics and maps. The reported run does NOT update the
|
|
258
|
+
# field (there is no pass after it), so unlike a plain "report the last of N passes"
|
|
259
|
+
# scheme the reported run reads the fully converged field. Everything non-hydrodynamic
|
|
260
|
+
# runs a single pass.
|
|
261
|
+
hydrodynamic = cf.phonon_hydrodynamic and mode is SimulationMode.PHONON_TRACING
|
|
262
|
+
number_of_preruns = cf.number_of_hydrodynamic_preruns if hydrodynamic else 0
|
|
263
|
+
number_of_passes = number_of_preruns + 1
|
|
264
|
+
|
|
265
|
+
# Material instance for the momentum-susceptibility constant (drift-field derivation):
|
|
266
|
+
hydro_material = create_material(cf.media, cf.temp,
|
|
267
|
+
isotope_c13_concentration=cf.isotope_c13_concentration) if hydrodynamic else None
|
|
268
|
+
|
|
269
|
+
drift_field = None
|
|
270
|
+
drift_convergence = [] # per-prerun (n, mean|u_fresh|, mean|u_field|, rel_change) for the convergence test
|
|
271
|
+
flux_profiles = [] # per-pass mid-length cross-width |J_y|(x), to watch it become parabolic
|
|
272
|
+
# Preruns only need to converge the field, so they can use fewer particles than the
|
|
273
|
+
# reported run (NUMBER_OF_HYDRODYNAMIC_PRERUN_PARTICLES; None = same as the reported run):
|
|
274
|
+
prerun_particles = cf.number_of_hydrodynamic_prerun_particles
|
|
275
|
+
for pass_index in range(number_of_passes):
|
|
276
|
+
is_reported_run = (pass_index == number_of_passes - 1)
|
|
277
|
+
pass_particles = None
|
|
278
|
+
if hydrodynamic and not is_reported_run and prerun_particles:
|
|
279
|
+
pass_particles = prerun_particles
|
|
280
|
+
if hydrodynamic:
|
|
281
|
+
n = pass_particles if pass_particles else cf.number_of_particles
|
|
282
|
+
label = "reported run" if is_reported_run else f"prerun {pass_index + 1}/{number_of_preruns}"
|
|
283
|
+
sys.stdout.write(f'\nHydrodynamic {label} ({n} particles)\n')
|
|
284
|
+
|
|
285
|
+
result_list = run_pass(mode, drift_field, pass_particles)
|
|
286
|
+
|
|
287
|
+
# Collect the results of this pass:
|
|
288
|
+
sys.stdout.write('\nCollecting data from workers...\r')
|
|
289
|
+
(scatter_stats, places_stats, general_stats, segment_stats, path_stats,
|
|
290
|
+
scatter_maps, thermal_maps, execution_time_list) = merge_results(result_list, mode)
|
|
291
|
+
|
|
292
|
+
# During preruns, update the self-consistent drift field for the next pass. The
|
|
293
|
+
# freshly measured field is under-relaxed against the previous one (Robbins-Monro)
|
|
294
|
+
# for stability. The reported run still measures its drift (for the saved profile)
|
|
295
|
+
# but does not feed it back:
|
|
296
|
+
if hydrodynamic and thermal_maps is not None:
|
|
297
|
+
thermal_maps.calculate_drift_velocity(hydro_material)
|
|
298
|
+
|
|
299
|
+
# Cross-width heat-flux profile at mid-length, one per pass, so the profile can
|
|
300
|
+
# be watched evolving from blunt (bootstrap, no drift) toward parabolic (Poiseuille):
|
|
301
|
+
ny = thermal_maps.heat_flux_map_y.shape[0]
|
|
302
|
+
band = thermal_maps.heat_flux_map_y[int(0.35 * ny):int(0.65 * ny), :]
|
|
303
|
+
flux_profiles.append(np.abs(band).mean(axis=0))
|
|
304
|
+
|
|
305
|
+
if not is_reported_run:
|
|
306
|
+
fresh = DriftField(thermal_maps.drift_velocity_x, thermal_maps.drift_velocity_y, thermal_maps.drift_velocity_z)
|
|
307
|
+
previous = drift_field
|
|
308
|
+
if drift_field is None:
|
|
309
|
+
drift_field = fresh
|
|
310
|
+
else:
|
|
311
|
+
g = cf.hydrodynamic_preruns_weight
|
|
312
|
+
drift_field = DriftField(
|
|
313
|
+
(1 - g) * drift_field.u_x + g * fresh.u_x,
|
|
314
|
+
(1 - g) * drift_field.u_y + g * fresh.u_y,
|
|
315
|
+
(1 - g) * drift_field.u_z + g * fresh.u_z,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Convergence diagnostics: mean drift magnitude of the freshly measured
|
|
319
|
+
# field and of the (under-relaxed) field carried to the next prerun, plus
|
|
320
|
+
# the relative L1 change of that field vs the previous prerun. When the
|
|
321
|
+
# relative change flattens toward its noise floor, the preruns are adequate.
|
|
322
|
+
def _mean_mag(f):
|
|
323
|
+
mag = np.hypot(f.u_x, f.u_y)
|
|
324
|
+
return float(mag[mag > 0].mean()) if np.any(mag > 0) else 0.0
|
|
325
|
+
if previous is None:
|
|
326
|
+
rel_change = np.nan
|
|
327
|
+
else:
|
|
328
|
+
delta = np.abs(drift_field.u_x - previous.u_x) + np.abs(drift_field.u_y - previous.u_y)
|
|
329
|
+
scale = np.abs(drift_field.u_x) + np.abs(drift_field.u_y)
|
|
330
|
+
rel_change = float(delta.sum() / (scale.sum() + 1e-30))
|
|
331
|
+
drift_convergence.append((pass_index + 1, _mean_mag(fresh), _mean_mag(drift_field), rel_change))
|
|
332
|
+
|
|
254
333
|
# Give some info about the variability in the worker calculation time:
|
|
255
334
|
if cf.num_workers > 1:
|
|
256
335
|
sys.stdout.write(f'Shortest process execution time: {round(min(execution_time_list))}s\n')
|
|
257
336
|
sys.stdout.write(f'Longest process execution time: {round(max(execution_time_list))}s\n')
|
|
258
337
|
|
|
259
|
-
# Run thermal calculations:
|
|
338
|
+
# Run thermal calculations on the final pass:
|
|
260
339
|
if thermal_maps is not None:
|
|
261
340
|
thermal_maps.calculate_thermal_conductivity()
|
|
262
341
|
thermal_maps.calculate_heat_flux_modulus()
|
|
@@ -277,6 +356,18 @@ def main(input_file, mode: SimulationMode):
|
|
|
277
356
|
shutil.copy(input_file, "Results/" + cf.output_folder_name)
|
|
278
357
|
os.chdir("Results/" + cf.output_folder_name)
|
|
279
358
|
|
|
359
|
+
# Save the hydrodynamic-drift-field convergence trace (one row per prerun):
|
|
360
|
+
if drift_convergence:
|
|
361
|
+
np.savetxt("Data/Drift convergence.csv", np.array(drift_convergence), fmt="%1.4e", delimiter=",",
|
|
362
|
+
header="prerun, mean|u_fresh| (m/s), mean|u_field| (m/s), rel_change_of_field", encoding="utf-8")
|
|
363
|
+
if flux_profiles:
|
|
364
|
+
prof = np.array(flux_profiles).T # rows = x pixels, cols = passes
|
|
365
|
+
nx = prof.shape[0]
|
|
366
|
+
x = (np.arange(nx) + 0.5) * cf.width / nx - cf.width / 2
|
|
367
|
+
header = "x (m), " + ", ".join([f"pass{i + 1}" for i in range(prof.shape[1])])
|
|
368
|
+
np.savetxt("Data/Drift convergence profiles.csv", np.column_stack([x, prof]), fmt="%1.4e",
|
|
369
|
+
delimiter=",", header=header, encoding="utf-8")
|
|
370
|
+
|
|
280
371
|
# Try to load pre-computed phonon thermal conductivity to enable ZT calculation:
|
|
281
372
|
if mode is SimulationMode.ELECTRON:
|
|
282
373
|
kappa_ph = None
|