freepaths 2.5.1__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.1 → freepaths-2.6}/PKG-INFO +16 -6
- {freepaths-2.5.1 → freepaths-2.6}/README.md +10 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/__main__.py +1 -1
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/maps.py +12 -2
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/output_plots.py +4 -1
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/output_structure.py +6 -6
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/run_particle.py +5 -2
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/scatterers.py +44 -43
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/scattering.py +33 -5
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/scattering_interfaces.py +22 -18
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/scattering_primitives.py +51 -36
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/sources.py +3 -2
- {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/PKG-INFO +16 -6
- {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/SOURCES.txt +15 -1
- freepaths-2.6/license +626 -0
- {freepaths-2.5.1 → 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.1 → freepaths-2.6}/freepaths/animation.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/config.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/data.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/default_config.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/electron.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/flight.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/interface_smmm.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/main_mfp_sampling.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/main_tracing.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/materials.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/move.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/options.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/output_electron_plots.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/output_info.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/particle.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/phonon.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/post_computations.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/progress.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/scattering_semicircle.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths/scattering_types.py +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/dependency_links.txt +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/entry_points.txt +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/requires.txt +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/top_level.txt +0 -0
- {freepaths-2.5.1 → freepaths-2.6}/pyproject.toml +0 -0
- {freepaths-2.5.1 → 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)
|
|
@@ -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):
|
|
@@ -495,7 +495,8 @@ def plot_thermal_map():
|
|
|
495
495
|
"""Plot thermal map as color map"""
|
|
496
496
|
fig = plt.figure()
|
|
497
497
|
thermal_map = np.genfromtxt("Data/Thermal map.csv", unpack=False, delimiter=',', skip_header=0, encoding='utf-8')
|
|
498
|
-
|
|
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)))
|
|
499
500
|
minimum_of_colorbar = 1e-20 # Cannot be zero!
|
|
500
501
|
boundaries = [(-cf.width / 2) * 1e6, (cf.width / 2) * 1e6, 0, cf.length * 1e6]
|
|
501
502
|
plt.imshow(thermal_map, cmap='hot', interpolation='none', extent=boundaries,
|
|
@@ -513,6 +514,8 @@ def plot_heat_flux_map(file, label, units="a.u."):
|
|
|
513
514
|
"""Plot heat flux map as color map"""
|
|
514
515
|
fig = plt.figure()
|
|
515
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))
|
|
516
519
|
heat_flux_map = np.abs(np.flipud(heat_flux_map))
|
|
517
520
|
minimum_of_colorbar = 1e5
|
|
518
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"""
|
|
@@ -61,10 +61,10 @@ def draw_structure_side_view(cf, color_holes="white", color_back="gray"):
|
|
|
61
61
|
)
|
|
62
62
|
]
|
|
63
63
|
|
|
64
|
-
# Interfaces as white patches:
|
|
65
|
-
for interface in cf.interfaces:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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])
|
|
69
69
|
|
|
70
70
|
return patches
|
|
@@ -100,8 +100,11 @@ def run_particle(particle, flight, scatter_stats, places_stats, segment_stats, t
|
|
|
100
100
|
# If interface transmission has occurred, record it:
|
|
101
101
|
if scattering_types.interfaces_transmission:
|
|
102
102
|
places_stats.save_scattering_events(particle, scattering_types.interfaces_transmission)
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
|
|
104
|
+
# The particle moves on every timestep, so its free path and scattering clock advance on every
|
|
105
|
+
# timestep too (they used to skip timesteps with an interface transmission, losing one step of
|
|
106
|
+
# free path per transmission):
|
|
107
|
+
flight.add_step(cf.timestep)
|
|
105
108
|
|
|
106
109
|
|
|
107
110
|
# Record presence of the particle at this timestep and move on:
|
|
@@ -572,10 +572,8 @@ class ParabolaTop(Hole):
|
|
|
572
572
|
|
|
573
573
|
# Specular scattering:
|
|
574
574
|
if random() < p:
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
else:
|
|
578
|
-
pt.theta = 2 * normal_theta - pt.theta
|
|
575
|
+
# Mirror about the tangent, whose angle is normal_theta (the same form as ParabolaBottom):
|
|
576
|
+
pt.theta = 2 * normal_theta - pt.theta
|
|
579
577
|
scattering_types.walls = Scattering.SPECULAR
|
|
580
578
|
|
|
581
579
|
# Diffuse scattering:
|
|
@@ -583,8 +581,7 @@ class ParabolaTop(Hole):
|
|
|
583
581
|
scattering_types.walls = Scattering.DIFFUSE
|
|
584
582
|
for _ in range(10):
|
|
585
583
|
# Lambert distribution
|
|
586
|
-
pt.theta = normal_theta
|
|
587
|
-
pt.phi = asin((asin(2 * random() - 1)) / (pi / 2))
|
|
584
|
+
pt.theta, pt.phi = lambert_angles(normal_theta - pi / 2)
|
|
588
585
|
|
|
589
586
|
# Accept the angles only if they do not immediately cause new scattering:
|
|
590
587
|
if no_new_scattering(pt, cf):
|
|
@@ -632,8 +629,7 @@ class ParabolaBottom(Hole):
|
|
|
632
629
|
scattering_types.walls = Scattering.DIFFUSE
|
|
633
630
|
for _ in range(10):
|
|
634
631
|
# Lambertian distribution
|
|
635
|
-
pt.theta = normal_theta
|
|
636
|
-
pt.phi = asin((asin(2 * random() - 1)) / (pi / 2))
|
|
632
|
+
pt.theta, pt.phi = lambert_angles(normal_theta - pi / 2)
|
|
637
633
|
|
|
638
634
|
# Accept the angles only if they do not immediately cause new scattering:
|
|
639
635
|
if no_new_scattering(pt, cf):
|
|
@@ -754,7 +750,9 @@ class VerticalPlane(Interface):
|
|
|
754
750
|
|
|
755
751
|
self.assign_materials(cf)
|
|
756
752
|
|
|
757
|
-
|
|
753
|
+
# Angle of incidence from the interface normal (x) in 3D. The in-plane pi/2 - theta alone ignored phi,
|
|
754
|
+
# treating out-of-plane phonons as closer to normal incidence than they are:
|
|
755
|
+
theta_i = np.arccos(abs(np.cos(pt.phi) * np.sin(pt.theta)))
|
|
758
756
|
|
|
759
757
|
mat_0 = self.outer_material
|
|
760
758
|
mat_j = self.inner_material
|
|
@@ -789,7 +787,6 @@ class VerticalPlane(Interface):
|
|
|
789
787
|
"""
|
|
790
788
|
# Crossing the interface:
|
|
791
789
|
transmission = self.transmission_probability(cf, pt)
|
|
792
|
-
theta_i = np.pi / 2 - pt.theta # because in the paper is the projection angle of the x axis
|
|
793
790
|
|
|
794
791
|
# else:
|
|
795
792
|
if random() < transmission:
|
|
@@ -823,36 +820,40 @@ class VerticalPlane(Interface):
|
|
|
823
820
|
)
|
|
824
821
|
|
|
825
822
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
823
|
+
# HorizontalPlane is disabled until it is fixed. It cannot run as written: scattering.py calls
|
|
824
|
+
# interface.scatter(pt, cf, scattering_types), which does not match its signature below; it scatters a
|
|
825
|
+
# z-normal plane with the y-normal horizontal_surface_*_scattering functions; and it passes cf into
|
|
826
|
+
# their is_diffuse slot, which would make every event diffuse.
|
|
827
|
+
# class HorizontalPlane(Interface):
|
|
828
|
+
# """Horizontal plane that represents an interface"""
|
|
829
|
+
#
|
|
830
|
+
# def __init__(self, position_z=0, transmission=0):
|
|
831
|
+
# self.position_z = position_z
|
|
832
|
+
# self.transmission = transmission
|
|
833
|
+
#
|
|
834
|
+
# def is_crossed(self, pt, x, y, z):
|
|
835
|
+
# """Check if particle with traverses the vertical plane at given coordinate"""
|
|
836
|
+
# return (pt.z < self.position_z < z) or (pt.z > self.position_z > z)
|
|
837
|
+
#
|
|
838
|
+
# def is_transmitted(self):
|
|
839
|
+
# """Check if particle traverses the plane given the transmission probability"""
|
|
840
|
+
# return random() < self.transmission
|
|
841
|
+
#
|
|
842
|
+
# def scatter(self, pt, scattering_types, x, y, z, cf):
|
|
843
|
+
# """Calculate the new direction after scattering on the interface wall"""
|
|
844
|
+
#
|
|
845
|
+
# # Scattering on the left wall:
|
|
846
|
+
# if z < self.position_z:
|
|
847
|
+
# scattering_types.interfaces = horizontal_surface_up_scattering(pt, cf.interface_roughness, cf)
|
|
848
|
+
#
|
|
849
|
+
# # Scattering on the right wall:
|
|
850
|
+
# else:
|
|
851
|
+
# scattering_types.interfaces = horizontal_surface_down_scattering(pt, cf.interface_roughness, cf)
|
|
852
|
+
#
|
|
853
|
+
# def get_patch(self, color_holes, cf):
|
|
854
|
+
# """Create a patch in the shape of a thin line to use in the plots"""
|
|
855
|
+
# return Rectangle(
|
|
856
|
+
# (0, 1e6 * self.position_z),
|
|
857
|
+
# 1e6 * cf.length, 1e6 * 0.005*cf.thickness,
|
|
858
|
+
# facecolor=color_holes,
|
|
859
|
+
# )
|
|
@@ -11,6 +11,7 @@ from freepaths.config import cf
|
|
|
11
11
|
from freepaths.move import move
|
|
12
12
|
from freepaths.scattering_primitives import *
|
|
13
13
|
from freepaths.options import ParticleType
|
|
14
|
+
from freepaths.scattering_types import ScatteringTypes
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def internal_scattering(pt, flight, scattering_types):
|
|
@@ -95,8 +96,38 @@ def ceiling_scattering(pt, scattering_types, x, y, z, depletion=0.0):
|
|
|
95
96
|
scattering_types.top_bottom = in_plane_surface_scattering(pt, cf.top_roughness)
|
|
96
97
|
|
|
97
98
|
|
|
99
|
+
# A scattering event changes the direction, and the new direction can cross a boundary that was
|
|
100
|
+
# checked with the old one: e.g. a diffuse redraw on the ceiling right next to a side wall points
|
|
101
|
+
# straight out through the wall. So the boundaries are checked again with every new direction,
|
|
102
|
+
# up to this many passes, until the step is clear:
|
|
103
|
+
MAX_SCATTERING_PASSES = 4
|
|
104
|
+
|
|
105
|
+
|
|
98
106
|
def surface_scattering(pt, scattering_types, triangle_scattering_places):
|
|
99
|
-
"""Check for a surface scattering on this step"""
|
|
107
|
+
"""Check for a surface scattering on this step, until the step with the final direction is clear"""
|
|
108
|
+
direction = (pt.theta, pt.phi)
|
|
109
|
+
_surface_scattering_pass(pt, scattering_types, triangle_scattering_places)
|
|
110
|
+
|
|
111
|
+
# Only a step whose direction was changed by a scattering event needs another look (a few % of steps):
|
|
112
|
+
for _ in range(MAX_SCATTERING_PASSES - 1):
|
|
113
|
+
if (pt.theta, pt.phi) == direction:
|
|
114
|
+
break
|
|
115
|
+
direction = (pt.theta, pt.phi)
|
|
116
|
+
this_pass = ScatteringTypes()
|
|
117
|
+
# An interface already met on this step is not met again: a transmitted particle still has to
|
|
118
|
+
# cross the plane, and that crossing is the transmission itself, not a second encounter:
|
|
119
|
+
_surface_scattering_pass(pt, this_pass, triangle_scattering_places,
|
|
120
|
+
check_interfaces=scattering_types.interfaces is None)
|
|
121
|
+
for name, value in vars(this_pass).items():
|
|
122
|
+
if value is not None:
|
|
123
|
+
setattr(scattering_types, name, value)
|
|
124
|
+
|
|
125
|
+
# Correct angle if it became more than 180 degrees:
|
|
126
|
+
pt.correct_angle()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _surface_scattering_pass(pt, scattering_types, triangle_scattering_places, check_interfaces=True):
|
|
130
|
+
"""Check every boundary once against the step with the current direction"""
|
|
100
131
|
|
|
101
132
|
# Preliminary move to see if particle would cross something:
|
|
102
133
|
x, y, z = move(pt, cf.timestep)
|
|
@@ -140,7 +171,7 @@ def surface_scattering(pt, scattering_types, triangle_scattering_places):
|
|
|
140
171
|
break
|
|
141
172
|
|
|
142
173
|
# Scattering on interfaces (thin layers):
|
|
143
|
-
if cf.interfaces:
|
|
174
|
+
if cf.interfaces and check_interfaces:
|
|
144
175
|
for interface in cf.interfaces:
|
|
145
176
|
if interface.is_crossed(pt, x, y, z):
|
|
146
177
|
interface.scatter(pt, cf, scattering_types)
|
|
@@ -148,6 +179,3 @@ def surface_scattering(pt, scattering_types, triangle_scattering_places):
|
|
|
148
179
|
# If there was any scattering, then no need to check other interfaces:
|
|
149
180
|
if scattering_types.interfaces is not None:
|
|
150
181
|
break
|
|
151
|
-
|
|
152
|
-
# Correct angle if it became more than 180 degrees:
|
|
153
|
-
pt.correct_angle()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Module that provides scattering functions on through the interfaces made of other materials"""
|
|
2
2
|
|
|
3
|
-
from math import pi, cos, sin,
|
|
3
|
+
from math import pi, cos, sin, acos
|
|
4
4
|
from random import random
|
|
5
5
|
|
|
6
6
|
from freepaths.scattering_types import Scattering
|
|
@@ -8,7 +8,7 @@ from freepaths.scattering_types import Scattering
|
|
|
8
8
|
# Single definitions, shared with the free-surface primitives. They used to be duplicated
|
|
9
9
|
# here verbatim, and since scatterers.py star-imports both modules this copy shadowed the
|
|
10
10
|
# original, so any change to the Soffer equation had to be made twice to take effect.
|
|
11
|
-
from freepaths.scattering_primitives import specularity, no_new_scattering
|
|
11
|
+
from freepaths.scattering_primitives import specularity, no_new_scattering, lambert_angles
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def vertical_surface_left_scattering_2T(pt, roughness, cf, vg_i_to_vg_j, is_diffuse=False):
|
|
@@ -18,7 +18,9 @@ def vertical_surface_left_scattering_2T(pt, roughness, cf, vg_i_to_vg_j, is_diff
|
|
|
18
18
|
a = acos(cos(pt.phi)*sin(abs(pt.theta)))
|
|
19
19
|
p = specularity(a, roughness, pt)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# Angle of incidence from the interface normal (x) in 3D. The in-plane pi/2 - theta alone ignored phi,
|
|
22
|
+
# treating out-of-plane phonons as closer to normal incidence than they are:
|
|
23
|
+
theta_i = acos(abs(cos(pt.phi) * sin(pt.theta)))
|
|
22
24
|
|
|
23
25
|
# Specular scattering:
|
|
24
26
|
if random() < p and (not is_diffuse):
|
|
@@ -40,13 +42,13 @@ def vertical_surface_left_scattering_2T(pt, roughness, cf, vg_i_to_vg_j, is_diff
|
|
|
40
42
|
# Diffuse scattering:
|
|
41
43
|
else:
|
|
42
44
|
|
|
43
|
-
# Lambert cosine distribution
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
# Lambert cosine distribution, redrawn if the new direction would immediately leave the domain
|
|
46
|
+
# (near an edge with the ceiling or floor). The event is diffuse whatever the draw:
|
|
47
|
+
for attempt in range(10):
|
|
48
|
+
pt.theta, pt.phi = lambert_angles(-pi/2)
|
|
49
|
+
if no_new_scattering(pt, cf):
|
|
50
|
+
break
|
|
51
|
+
return Scattering.DIFFUSE
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
def vertical_surface_right_scattering_2T(pt, roughness, cf, vg_i_to_vg_j, is_diffuse=False):
|
|
@@ -56,7 +58,9 @@ def vertical_surface_right_scattering_2T(pt, roughness, cf, vg_i_to_vg_j, is_dif
|
|
|
56
58
|
a = acos(cos(pt.phi)*sin(abs(pt.theta)))
|
|
57
59
|
p = specularity(a, roughness, pt)
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
# Angle of incidence from the interface normal (x) in 3D. The in-plane pi/2 - theta alone ignored phi,
|
|
62
|
+
# treating out-of-plane phonons as closer to normal incidence than they are:
|
|
63
|
+
theta_i = acos(abs(cos(pt.phi) * sin(pt.theta)))
|
|
60
64
|
|
|
61
65
|
# Specular scattering:
|
|
62
66
|
if random() < p and (not is_diffuse):
|
|
@@ -75,10 +79,10 @@ def vertical_surface_right_scattering_2T(pt, roughness, cf, vg_i_to_vg_j, is_dif
|
|
|
75
79
|
# Diffuse scattering:
|
|
76
80
|
else:
|
|
77
81
|
|
|
78
|
-
# Lambert cosine distribution
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
# Lambert cosine distribution, redrawn if the new direction would immediately leave the domain
|
|
83
|
+
# (near an edge with the ceiling or floor). The event is diffuse whatever the draw:
|
|
84
|
+
for attempt in range(10):
|
|
85
|
+
pt.theta, pt.phi = lambert_angles(+pi/2)
|
|
86
|
+
if no_new_scattering(pt, cf):
|
|
87
|
+
break
|
|
88
|
+
return Scattering.DIFFUSE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Module that provides scattering functions on basic objects like various walls"""
|
|
2
2
|
|
|
3
|
-
from math import pi, cos, sin, tan, exp, atan, asin, acos
|
|
3
|
+
from math import pi, cos, sin, tan, exp, atan, atan2, asin, acos, sqrt
|
|
4
4
|
from random import random
|
|
5
5
|
from numpy import sign
|
|
6
6
|
|
|
@@ -26,6 +26,27 @@ def no_new_scattering(pt, cf):
|
|
|
26
26
|
return abs(z) < cf.thickness / 2 and abs(x) < cf.width / 2 and cf.length > y > 0
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def lambert_angles(normal_theta, normal_phi=0.0):
|
|
30
|
+
"""
|
|
31
|
+
Draw a direction from the Lambert cosine law about the surface normal
|
|
32
|
+
given by (normal_theta, normal_phi), in the direction convention of move.py:
|
|
33
|
+
d = (sin(theta)*cos(phi), cos(theta)*cos(phi), sin(phi)).
|
|
34
|
+
The cosine to the normal is mu = sqrt(random()) (density 2*mu), and the azimuth
|
|
35
|
+
around the normal is uniform. Returns the (theta, phi) of the new direction.
|
|
36
|
+
"""
|
|
37
|
+
mu = sqrt(random())
|
|
38
|
+
s = sqrt(1 - mu**2)
|
|
39
|
+
psi = 2*pi*random()
|
|
40
|
+
sin_t, cos_t = sin(normal_theta), cos(normal_theta)
|
|
41
|
+
sin_p, cos_p = sin(normal_phi), cos(normal_phi)
|
|
42
|
+
|
|
43
|
+
# Normal and two unit tangents (along d/dtheta and d/dphi of the normal):
|
|
44
|
+
d_x = mu*sin_t*cos_p + s*cos(psi)*cos_t - s*sin(psi)*sin_t*sin_p
|
|
45
|
+
d_y = mu*cos_t*cos_p - s*cos(psi)*sin_t - s*sin(psi)*cos_t*sin_p
|
|
46
|
+
d_z = mu*sin_p + s*sin(psi)*cos_p
|
|
47
|
+
return atan2(d_x, d_y), asin(max(-1.0, min(1.0, d_z)))
|
|
48
|
+
|
|
49
|
+
|
|
29
50
|
def random_scattering(pt):
|
|
30
51
|
"""Scattering in a random direction"""
|
|
31
52
|
pt.theta = -pi + random()*2*pi
|
|
@@ -48,13 +69,13 @@ def vertical_surface_left_scattering(pt, roughness, cf, is_diffuse=False):
|
|
|
48
69
|
# Diffuse scattering
|
|
49
70
|
else:
|
|
50
71
|
|
|
51
|
-
# Lambert cosine distribution
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
# Lambert cosine distribution, redrawn if the new direction would immediately leave the domain
|
|
73
|
+
# (near an edge with the ceiling or floor). The event is diffuse whatever the draw:
|
|
74
|
+
for attempt in range(10):
|
|
75
|
+
pt.theta, pt.phi = lambert_angles(-pi/2)
|
|
76
|
+
if no_new_scattering(pt, cf):
|
|
77
|
+
break
|
|
78
|
+
return Scattering.DIFFUSE
|
|
58
79
|
|
|
59
80
|
|
|
60
81
|
def vertical_surface_right_scattering(pt, roughness, cf, is_diffuse=False):
|
|
@@ -72,13 +93,13 @@ def vertical_surface_right_scattering(pt, roughness, cf, is_diffuse=False):
|
|
|
72
93
|
# Diffuse scattering:
|
|
73
94
|
else:
|
|
74
95
|
|
|
75
|
-
# Lambert cosine distribution
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
# Lambert cosine distribution, redrawn if the new direction would immediately leave the domain
|
|
97
|
+
# (near an edge with the ceiling or floor). The event is diffuse whatever the draw:
|
|
98
|
+
for attempt in range(10):
|
|
99
|
+
pt.theta, pt.phi = lambert_angles(+pi/2)
|
|
100
|
+
if no_new_scattering(pt, cf):
|
|
101
|
+
break
|
|
102
|
+
return Scattering.DIFFUSE
|
|
82
103
|
|
|
83
104
|
|
|
84
105
|
def horizontal_surface_down_scattering(pt, roughness, is_diffuse=False):
|
|
@@ -95,9 +116,7 @@ def horizontal_surface_down_scattering(pt, roughness, is_diffuse=False):
|
|
|
95
116
|
|
|
96
117
|
# Diffuse scattering:
|
|
97
118
|
# Lambert cosine distribution:
|
|
98
|
-
|
|
99
|
-
pt.theta = rand_sign*pi/2 + rand_sign*acos(random())
|
|
100
|
-
pt.phi = asin((asin(2*random() - 1))/(pi/2))
|
|
119
|
+
pt.theta, pt.phi = lambert_angles(pi)
|
|
101
120
|
return Scattering.DIFFUSE
|
|
102
121
|
|
|
103
122
|
|
|
@@ -115,16 +134,16 @@ def horizontal_surface_up_scattering(pt, roughness, is_diffuse=False):
|
|
|
115
134
|
|
|
116
135
|
# Diffuse scattering:
|
|
117
136
|
# Lambert cosine distribution:
|
|
118
|
-
pt.theta =
|
|
119
|
-
pt.phi = asin((asin(2*random() - 1))/(pi/2))
|
|
137
|
+
pt.theta, pt.phi = lambert_angles(0)
|
|
120
138
|
return Scattering.DIFFUSE
|
|
121
139
|
|
|
122
140
|
|
|
123
141
|
def inclined_surfaces_down_scattering(pt, beta, x, x0, roughness):
|
|
124
142
|
"""Scattering from a inclined surfaces pointing down like V"""
|
|
125
143
|
|
|
126
|
-
# Calculate angle to the surface and specular scattering probability:
|
|
127
|
-
|
|
144
|
+
# Calculate angle to the surface normal and specular scattering probability:
|
|
145
|
+
normal_theta = pi - sign(x - x0)*(pi/2 - beta)
|
|
146
|
+
a = acos(abs(cos(pt.phi)*cos(pt.theta - normal_theta)))
|
|
128
147
|
p = specularity(a, roughness, pt)
|
|
129
148
|
|
|
130
149
|
# Specular scattering:
|
|
@@ -133,17 +152,16 @@ def inclined_surfaces_down_scattering(pt, beta, x, x0, roughness):
|
|
|
133
152
|
return Scattering.SPECULAR
|
|
134
153
|
|
|
135
154
|
# Diffuse scattering:
|
|
136
|
-
|
|
137
|
-
pt.theta = rand_sign*pi - rand_sign*asin(random()) - sign(x - x0)*(pi/2 - beta)
|
|
138
|
-
pt.phi = asin((asin(2*random() - 1))/(pi/2))
|
|
155
|
+
pt.theta, pt.phi = lambert_angles(normal_theta)
|
|
139
156
|
return Scattering.DIFFUSE
|
|
140
157
|
|
|
141
158
|
|
|
142
159
|
def inclined_surfaces_up_scattering(pt, beta, x, x0, roughness):
|
|
143
160
|
"""Scattering from a inclined surfaces pointing up like ^"""
|
|
144
161
|
|
|
145
|
-
# Calculate angle to the surface and specular scattering probability:
|
|
146
|
-
|
|
162
|
+
# Calculate angle to the surface normal and specular scattering probability:
|
|
163
|
+
normal_theta = sign(x - x0)*(pi/2 - beta)
|
|
164
|
+
a = acos(abs(cos(pt.phi)*cos(pt.theta - normal_theta)))
|
|
147
165
|
p = specularity(a, roughness, pt)
|
|
148
166
|
|
|
149
167
|
# Specular scattering:
|
|
@@ -152,8 +170,7 @@ def inclined_surfaces_up_scattering(pt, beta, x, x0, roughness):
|
|
|
152
170
|
return Scattering.SPECULAR
|
|
153
171
|
|
|
154
172
|
# Diffuse scattering:
|
|
155
|
-
pt.theta =
|
|
156
|
-
pt.phi = asin((asin(2*random() - 1))/(pi/2))
|
|
173
|
+
pt.theta, pt.phi = lambert_angles(normal_theta)
|
|
157
174
|
return Scattering.DIFFUSE
|
|
158
175
|
|
|
159
176
|
|
|
@@ -175,8 +192,7 @@ def in_plane_surface_scattering(pt, roughness):
|
|
|
175
192
|
# phi = -asin(random())
|
|
176
193
|
|
|
177
194
|
# Lambert cosine distribution:
|
|
178
|
-
pt.theta = -
|
|
179
|
-
pt.phi = - sign(pt.phi) * random()*pi/2
|
|
195
|
+
pt.theta, pt.phi = lambert_angles(0, -sign(pt.phi)*pi/2)
|
|
180
196
|
return Scattering.DIFFUSE
|
|
181
197
|
|
|
182
198
|
|
|
@@ -200,8 +216,7 @@ def circle_outer_scattering(pt, tangent_theta, y, y0, roughness, cf):
|
|
|
200
216
|
# phi = asin(2*random() - 1)
|
|
201
217
|
|
|
202
218
|
# Lambert cosine distribution:
|
|
203
|
-
pt.theta = tangent_theta -
|
|
204
|
-
pt.phi = asin((asin(2*random() - 1))/(pi/2))
|
|
219
|
+
pt.theta, pt.phi = lambert_angles(tangent_theta - pi*(y < y0))
|
|
205
220
|
|
|
206
221
|
# Accept the angles only if they do not immediately cause new scattering:
|
|
207
222
|
if no_new_scattering(pt, cf):
|
|
@@ -211,7 +226,8 @@ def circle_outer_scattering(pt, tangent_theta, y, y0, roughness, cf):
|
|
|
211
226
|
def circle_inner_scattering(pt, tangent_theta, y, y0, roughness):
|
|
212
227
|
"""Scattering from the inner surface of the circle"""
|
|
213
228
|
|
|
214
|
-
|
|
229
|
+
# Calculate angle to the surface normal (radial, at angle tangent_theta up to pi) and specular scattering probability:
|
|
230
|
+
a = acos(abs(cos(pt.phi)*cos(pt.theta - tangent_theta)))
|
|
215
231
|
p = specularity(a, roughness, pt)
|
|
216
232
|
|
|
217
233
|
# Specular scattering:
|
|
@@ -220,8 +236,7 @@ def circle_inner_scattering(pt, tangent_theta, y, y0, roughness):
|
|
|
220
236
|
return Scattering.SPECULAR
|
|
221
237
|
|
|
222
238
|
# Diffuse scattering:
|
|
223
|
-
pt.theta = tangent_theta
|
|
224
|
-
pt.phi = asin((asin(2*random() - 1))/(pi/2))
|
|
239
|
+
pt.theta, pt.phi = lambert_angles(tangent_theta + pi*(y >= y0))
|
|
225
240
|
return Scattering.DIFFUSE
|
|
226
241
|
|
|
227
242
|
|
|
@@ -5,6 +5,8 @@ import enum
|
|
|
5
5
|
from random import random
|
|
6
6
|
from math import pi, asin
|
|
7
7
|
|
|
8
|
+
from freepaths.scattering_primitives import lambert_angles
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class Distributions(enum.Enum):
|
|
10
12
|
"""Possible distributions of angles"""
|
|
@@ -42,8 +44,7 @@ class Source:
|
|
|
42
44
|
theta = self.angle + 1e-10
|
|
43
45
|
phi = -pi/2 + pi*random()
|
|
44
46
|
elif self.angle_distribution == Distributions.LAMBERT:
|
|
45
|
-
theta =
|
|
46
|
-
phi = asin((asin(2*random() - 1))/(pi/2))
|
|
47
|
+
theta, phi = lambert_angles(self.angle)
|
|
47
48
|
elif self.angle_distribution == Distributions.UNIFORM:
|
|
48
49
|
theta = -pi + 2*pi*random()
|
|
49
50
|
phi = asin(2*random() - 1)
|