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.
Files changed (55) hide show
  1. {freepaths-2.5.1 → freepaths-2.6}/PKG-INFO +16 -6
  2. {freepaths-2.5.1 → freepaths-2.6}/README.md +10 -0
  3. {freepaths-2.5.1 → freepaths-2.6}/freepaths/__main__.py +1 -1
  4. {freepaths-2.5.1 → freepaths-2.6}/freepaths/maps.py +12 -2
  5. {freepaths-2.5.1 → freepaths-2.6}/freepaths/output_plots.py +4 -1
  6. {freepaths-2.5.1 → freepaths-2.6}/freepaths/output_structure.py +6 -6
  7. {freepaths-2.5.1 → freepaths-2.6}/freepaths/run_particle.py +5 -2
  8. {freepaths-2.5.1 → freepaths-2.6}/freepaths/scatterers.py +44 -43
  9. {freepaths-2.5.1 → freepaths-2.6}/freepaths/scattering.py +33 -5
  10. {freepaths-2.5.1 → freepaths-2.6}/freepaths/scattering_interfaces.py +22 -18
  11. {freepaths-2.5.1 → freepaths-2.6}/freepaths/scattering_primitives.py +51 -36
  12. {freepaths-2.5.1 → freepaths-2.6}/freepaths/sources.py +3 -2
  13. {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/PKG-INFO +16 -6
  14. {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/SOURCES.txt +15 -1
  15. freepaths-2.6/license +626 -0
  16. {freepaths-2.5.1 → freepaths-2.6}/setup.py +3 -4
  17. freepaths-2.6/tests/test_cauchy_mean_chord.py +115 -0
  18. freepaths-2.6/tests/test_containment.py +84 -0
  19. freepaths-2.6/tests/test_depletion_geometry.py +51 -0
  20. freepaths-2.6/tests/test_dispersion.py +76 -0
  21. freepaths-2.6/tests/test_electrons.py +221 -0
  22. freepaths-2.6/tests/test_end_to_end.py +222 -0
  23. freepaths-2.6/tests/test_free_path.py +88 -0
  24. freepaths-2.6/tests/test_interface_transmission.py +74 -0
  25. freepaths-2.6/tests/test_lambert_scattering.py +114 -0
  26. freepaths-2.6/tests/test_sampling_tables.py +173 -0
  27. freepaths-2.6/tests/test_smmm.py +126 -0
  28. freepaths-2.6/tests/test_soffer.py +99 -0
  29. freepaths-2.6/tests/test_specular_scattering.py +60 -0
  30. {freepaths-2.5.1 → freepaths-2.6}/freepaths/animation.py +0 -0
  31. {freepaths-2.5.1 → freepaths-2.6}/freepaths/config.py +0 -0
  32. {freepaths-2.5.1 → freepaths-2.6}/freepaths/data.py +0 -0
  33. {freepaths-2.5.1 → freepaths-2.6}/freepaths/default_config.py +0 -0
  34. {freepaths-2.5.1 → freepaths-2.6}/freepaths/electron.py +0 -0
  35. {freepaths-2.5.1 → freepaths-2.6}/freepaths/flight.py +0 -0
  36. {freepaths-2.5.1 → freepaths-2.6}/freepaths/interface_smmm.py +0 -0
  37. {freepaths-2.5.1 → freepaths-2.6}/freepaths/main_mfp_sampling.py +0 -0
  38. {freepaths-2.5.1 → freepaths-2.6}/freepaths/main_tracing.py +0 -0
  39. {freepaths-2.5.1 → freepaths-2.6}/freepaths/materials.py +0 -0
  40. {freepaths-2.5.1 → freepaths-2.6}/freepaths/move.py +0 -0
  41. {freepaths-2.5.1 → freepaths-2.6}/freepaths/options.py +0 -0
  42. {freepaths-2.5.1 → freepaths-2.6}/freepaths/output_electron_plots.py +0 -0
  43. {freepaths-2.5.1 → freepaths-2.6}/freepaths/output_info.py +0 -0
  44. {freepaths-2.5.1 → freepaths-2.6}/freepaths/particle.py +0 -0
  45. {freepaths-2.5.1 → freepaths-2.6}/freepaths/phonon.py +0 -0
  46. {freepaths-2.5.1 → freepaths-2.6}/freepaths/post_computations.py +0 -0
  47. {freepaths-2.5.1 → freepaths-2.6}/freepaths/progress.py +0 -0
  48. {freepaths-2.5.1 → freepaths-2.6}/freepaths/scattering_semicircle.py +0 -0
  49. {freepaths-2.5.1 → freepaths-2.6}/freepaths/scattering_types.py +0 -0
  50. {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/dependency_links.txt +0 -0
  51. {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/entry_points.txt +0 -0
  52. {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/requires.txt +0 -0
  53. {freepaths-2.5.1 → freepaths-2.6}/freepaths.egg-info/top_level.txt +0 -0
  54. {freepaths-2.5.1 → freepaths-2.6}/pyproject.toml +0 -0
  55. {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.5.1
3
+ Version: 2.6
4
4
  Summary: Phonon Monte Carlo simulator
5
5
  Home-page: https://github.com/anufrievroman/freepaths
6
6
  Author: Roman Anufriev
7
7
  Author-email: anufriev.roman@protonmail.com
8
- License: GPL
8
+ License-Expression: GPL-3.0-only
9
9
  Classifier: Development Status :: 4 - Beta
10
- Classifier: License :: OSI Approved :: GNU General Public License (GPL)
11
10
  Classifier: Environment :: Console
12
11
  Classifier: Natural Language :: English
13
12
  Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3.9
15
- Classifier: Programming Language :: Python :: 3.10
16
13
  Classifier: Programming Language :: Python :: 3.11
17
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
18
16
  Classifier: Topic :: Utilities
19
17
  Requires-Python: ~=3.11
20
18
  Description-Content-Type: text/markdown
19
+ License-File: license
21
20
  Requires-Dist: numpy
22
21
  Requires-Dist: matplotlib
23
22
  Requires-Dist: scipy
@@ -28,7 +27,8 @@ Dynamic: classifier
28
27
  Dynamic: description
29
28
  Dynamic: description-content-type
30
29
  Dynamic: home-page
31
- Dynamic: license
30
+ Dynamic: license-file
31
+ Dynamic: license-expression
32
32
  Dynamic: requires-dist
33
33
  Dynamic: requires-python
34
34
  Dynamic: summary
@@ -94,6 +94,16 @@ Alternatively, you can run FreePATHS in the mean free path sampling mode, which
94
94
  The calculated thermal conductivity will be output in the terminal. However, other statistical quantities and plots will still be calculated and output in the `Results` folder.
95
95
 
96
96
 
97
+ ## Testing
98
+
99
+ The `tests` folder holds the validation tests that must pass before a new version is published. They run against the source tree, not an installed copy. From the repository root:
100
+
101
+ ```
102
+ pip install pytest
103
+ pytest tests
104
+ ```
105
+
106
+
97
107
  ## Troubleshooting
98
108
 
99
109
  - [Troubles with installation](https://anufrievroman.gitbook.io/freepaths/installation)
@@ -59,6 +59,16 @@ Alternatively, you can run FreePATHS in the mean free path sampling mode, which
59
59
  The calculated thermal conductivity will be output in the terminal. However, other statistical quantities and plots will still be calculated and output in the `Results` folder.
60
60
 
61
61
 
62
+ ## Testing
63
+
64
+ The `tests` folder holds the validation tests that must pass before a new version is published. They run against the source tree, not an installed copy. From the repository root:
65
+
66
+ ```
67
+ pip install pytest
68
+ pytest tests
69
+ ```
70
+
71
+
62
72
  ## Troubleshooting
63
73
 
64
74
  - [Troubles with installation](https://anufrievroman.gitbook.io/freepaths/installation)
@@ -7,7 +7,7 @@ from colorama import Fore, Style
7
7
 
8
8
  from freepaths.options import SimulationMode
9
9
 
10
- __version__ = "2.5.1"
10
+ __version__ = "2.6"
11
11
 
12
12
  colorama.init()
13
13
 
@@ -5,6 +5,7 @@ from scipy.constants import k as k_B
5
5
  import numpy as np
6
6
  from freepaths.config import cf
7
7
  from freepaths.options import SimulationMode
8
+ import freepaths.move
8
9
 
9
10
  class Maps:
10
11
  """Parent maps class with functions common to all classes below"""
@@ -182,9 +183,18 @@ class ThermalMaps(Maps):
182
183
  This first time step is unique for each phonon. This is done to simulate more relistic continious heat flow.
183
184
  """
184
185
 
186
+ # The phonon is recorded at the middle of the step it is about to make, not at its start.
187
+ # A sample stands for the whole step, and the start of the step is a first-order (left
188
+ # rectangle) rule: in a temperature gradient it undercounts the heat flux by a fraction
189
+ # step / (2 * MFP), which made the Fourier kappa low by -10% at MFP/step = 5. The midpoint
190
+ # is second-order accurate, so the bias disappears (tests/test_end_to_end.py, gray medium):
191
+ d_x, d_y, _ = freepaths.move.step(pt.theta, pt.phi, pt.speed, cf.timestep)
192
+ x = pt.x + d_x / 2
193
+ y = pt.y + d_y / 2
194
+
185
195
  # Calculate the index of the pixel in which we record this phonon:
186
- index_x = int(((pt.x + cf.width / 2) * cf.number_of_pixels_x) // cf.width)
187
- index_y = int(pt.y // (cf.length / cf.number_of_pixels_y))
196
+ index_x = int(((x + cf.width / 2) * cf.number_of_pixels_x) // cf.width)
197
+ index_y = int(y // (cf.length / cf.number_of_pixels_y))
188
198
 
189
199
  # Prevent error if the phonon is outside the structure:
190
200
  if (0 <= index_x < cf.number_of_pixels_x) and (0 <= index_y < cf.number_of_pixels_y):
@@ -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
- thermal_map = np.flipud(thermal_map)
498
+ # A map one pixel wide or long is read back as a 1D array, which imshow rejects:
499
+ thermal_map = np.flipud(np.reshape(thermal_map, (cf.number_of_pixels_y, cf.number_of_pixels_x)))
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 HorizontalPlane, VerticalPlane
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
- if isinstance(interface, HorizontalPlane):
67
- patch = interface.get_patch(color_holes, cf)
68
- patches.extend(patch if isinstance(patch, list) else [patch])
64
+ # Interfaces as white patches (only HorizontalPlane is visible in this view; it is disabled for now):
65
+ # for interface in cf.interfaces:
66
+ # if isinstance(interface, HorizontalPlane):
67
+ # patch = interface.get_patch(color_holes, cf)
68
+ # patches.extend(patch if isinstance(patch, list) else [patch])
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
- else:
104
- flight.add_step(cf.timestep)
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
- if abs(pt.theta) > pi / 2:
576
- pt.theta = pt.theta - 2 * normal_theta
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 + asin(2 * random() - 1) - pi / 2
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 + asin(2 * random() - 1) - pi / 2
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
- theta_i = np.pi / 2 - pt.theta # because in the paper is the projection angle of the x axis
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
- class HorizontalPlane(Interface):
827
- """Horizontal plane that represents an interface"""
828
-
829
- def __init__(self, position_z=0, transmission=0):
830
- self.position_z = position_z
831
- self.transmission = transmission
832
-
833
- def is_crossed(self, pt, x, y, z):
834
- """Check if particle with traverses the vertical plane at given coordinate"""
835
- return (pt.z < self.position_z < z) or (pt.z > self.position_z > z)
836
-
837
- def is_transmitted(self):
838
- """Check if particle traverses the plane given the transmission probability"""
839
- return random() < self.transmission
840
-
841
- def scatter(self, pt, scattering_types, x, y, z, cf):
842
- """Calculate the new direction after scattering on the interface wall"""
843
-
844
- # Scattering on the left wall:
845
- if z < self.position_z:
846
- scattering_types.interfaces = horizontal_surface_up_scattering(pt, cf.interface_roughness, cf)
847
-
848
- # Scattering on the right wall:
849
- else:
850
- scattering_types.interfaces = horizontal_surface_down_scattering(pt, cf.interface_roughness, cf)
851
-
852
- def get_patch(self, color_holes, cf):
853
- """Create a patch in the shape of a thin line to use in the plots"""
854
- return Rectangle(
855
- (0, 1e6 * self.position_z),
856
- 1e6 * cf.length, 1e6 * 0.005*cf.thickness,
857
- facecolor=color_holes,
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, asin, acos
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
- theta_i = pi / 2 - pt.theta # because in the paper is the projection angle of the x axis
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
- pt.theta = -pi/2 + asin(2*random() - 1)
45
- pt.phi = asin((asin(2*random() - 1))/(pi/2))
46
-
47
- # Accept the angles if they do not cause new scattering:
48
- if no_new_scattering(pt, cf):
49
- return Scattering.DIFFUSE
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
- theta_i = pi / 2 - pt.theta # because in the paper is the projection angle of the x axis
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
- pt.theta = +pi/2 + asin(2*random() - 1)
80
- pt.phi = asin((asin(2*random() - 1))/(pi/2))
81
-
82
- # Accept the angles if they do not cause new scattering:
83
- if no_new_scattering(pt, cf):
84
- return Scattering.DIFFUSE
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
- pt.theta = -pi/2 + asin(2*random() - 1)
53
- pt.phi = asin((asin(2*random() - 1))/(pi/2))
54
-
55
- # Accept the angles if they do not cause new scattering:
56
- if no_new_scattering(pt, cf):
57
- return Scattering.DIFFUSE
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
- pt.theta = +pi/2 + asin(2*random() - 1)
77
- pt.phi = asin((asin(2*random() - 1))/(pi/2))
78
-
79
- # Accept the angles if they do not cause new scattering:
80
- if no_new_scattering(pt, cf):
81
- return Scattering.DIFFUSE
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
- rand_sign = sign((2*random() - 1))
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 = asin(2*random() - 1)
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
- a = acos(cos(pt.phi)*cos(pi/2 - abs(pt.theta) - beta))
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
- rand_sign = sign((2*random() - 1))
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
- a = acos(cos(pt.phi)*cos(pi/2 - abs(pt.theta) + beta))
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 = asin(2*random() - 1) + sign(x - x0)*(pi/2 - beta)
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 = - pi + random()*2*pi
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 - (asin(2*random() - 1)) - pi*(y < y0)
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
- a = atan(tan((pi/2 - pt.theta) + tangent_theta) * cos(pt.phi))
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 - asin(2*random()-1) + pi*(y >= y0)
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 = asin(2*random() - 1) + self.angle
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)