Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e170067

Browse files
committedAug 26, 2020
Merge branch 'release/2.1.3' into master
This release contains the high-level API LSC object. This should make performing basic LSC simualtions straightforward.
2 parents 5546acc + 7b1a362 commit e170067

17 files changed

+2280
-170
lines changed
 

‎README.md

+116-28
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,86 @@
11
![](https://raw.githubusercontent.com/danieljfarrell/pvtrace/master/docs/logo.png)
22

3-
> Optical ray tracing for luminescent materials and spectral converter photovoltaic devices
3+
> Optical ray tracing for luminescent materials and spectral converter photovoltaic devices
44
5-
## Install
5+
# Ray-tracing luminescent solar concentrators
66

7-
pip install pvtrace
7+
*pvtrace* is a statistical photon path tracer written in Python. Rays are followed through a 3D scene and their interactions with objects are recorded to build up statistical information about energy flow.
8+
9+
This is useful in photovoltaics and non-imaging optics where the goal is to design systems which efficiently transport light to target locations.
10+
11+
One of its key features is the ability to simulate re-absorption in luminescent materials. For example, like in devices like Luminescent Solar Concentrators (LSCs).
12+
13+
A basic LSC can be simulated and visualised in five lines of code,
14+
15+
```python
16+
from pvtrace import *
17+
lsc = LSC((5.0, 5.0, 1.0)) # size in cm
18+
lsc.show() # open visualiser
19+
lsc.simulate(100) # emit 100 rays
20+
lsc.report() # print report
21+
```
22+
23+
This script will render the ray-tracing in real time,
24+
25+
![](https://raw.githubusercontent.com/danieljfarrell/pvtrace/dev/lsc-device/docs/pvtrace-demo.gif)
26+
27+
pvtrace has been validate against three other luminescent concentrator codes. For full details see [Validation.ipynb](https://github.com/danieljfarrell/pvtrace/blob/dev/lsc-device/examples/Validation.ipynb) notebook
28+
29+
![](https://raw.githubusercontent.com/danieljfarrell/pvtrace/dev/lsc-device/examples/Validation.png)
830

9-
Tutorials are in Jupyter notebook form so to view those
31+
# Install
1032

11-
pip install jupyter
33+
## MacOS using pyenv
1234

13-
### pyenv (macOS)
35+
On MacOS *pvtrace* can be installed easily using [pyenv](https://github.com/pyenv/pyenv) and `pip`.
1436

15-
If using macOS you may want to use [pyenv](https://github.com/pyenv/pyenv) to create a clean virtual environment for pvtrace.
37+
Create a clean virtual environment for pvtrace
1638

1739
pyenv virtualenv 3.7.2 pvtrace-env
1840
pyenv activate pvtrace-env
1941
pip install pvtrace
2042
# download https://raw.githubusercontent.com/danieljfarrell/pvtrace/master/examples/hello_world.py
2143
python hello_world.py
2244

23-
### conda (Windows, Linux and macOS)
45+
## Linux and Windows using Conda
2446

25-
Conda can also be used but you must manually install the Rtree dependency *before* the `pip install pvtrace` command!
47+
On Linux and Windows you must use conda to create the python environment. Optionally you can also use this method on MacOS too if you prefer Conda over pyenv.
2648

2749
conda create --name pvtrace-env python=3.7
2850
conda activate pvtrace-env
2951
conda install Rtree
3052
pip install pvtrace
3153
# download https://raw.githubusercontent.com/danieljfarrell/pvtrace/master/examples/hello_world.py
3254
python hello_world.py
33-
34-
## Introduction
3555

36-
pvtrace is a statistical photon path tracer written in Python. It follows photons through a 3D scene and records their interactions with objects to build up statistical information about energy flow. This approach is particularly useful in photovoltaics and non-imaging optics where the goal is to design systems which efficiently transport light to target locations.
56+
# Features
3757

38-
## Documentation
58+
## Ray optics simulations
3959

40-
Interactive Jupyter notebooks are in [examples directory](https://github.com/danieljfarrell/pvtrace/tree/master/examples), download and take a look, although they can be viewed online.
60+
*pvtrace* supports 3D ray optics simulations shapes,
4161

42-
API documentation and some background at [https://pvtrace.readthedocs.io](https://pvtrace.readthedocs.io/)
62+
* box
63+
* sphere
64+
* cylinder
65+
* mesh
66+
67+
The optical properties of each shape can be customised,
4368

44-
## Capabilities
69+
* refractive index
70+
* absorption coefficient
71+
* scattering coefficient
72+
* emission lineshape
73+
* quantum yield
74+
* surface reflection
75+
* surface scattering
4576

46-
pvtrace was originally written to characterise the performance of Luminescent Solar Concentrators (LSC) and takes a Monte-Carlo approach to ray-tracing. Each ray is independent and can interact with objects in the scene via reflection and refraction. Objects can have different optical properties: refractive index, absorption coefficient, emission spectrum and quantum yield.
77+
![](https://raw.githubusercontent.com/danieljfarrell/pvtrace/master/docs/example.png)
4778

48-
One of the key features of pvtrace is the ability to simulate re-absorption of photons in luminescent materials. This requires following thousands of rays to build intensity profiles and spectra of incoming and outgoing photons because these process cannot be approximated in a continuous way.
79+
## High and low-level API
4980

50-
pvtrace may also be useful to researches or designers interested in ray-optics simulations but will be slower at running these simulations compared to other software packages because it follows each ray individually.
81+
*pvtrace* has a high-level API for handling common problems with LSCs and a low-level API where objects can be positioned in a 3D scene and optical properties customised.
5182

52-
![](https://raw.githubusercontent.com/danieljfarrell/pvtrace/master/docs/example.png)
53-
54-
A minimal working example that traces a glass sphere
83+
For example, a script using the low-level API to ray trace this glass sphere is below,
5584

5685
```python
5786
import time
@@ -60,6 +89,7 @@ import functools
6089
import numpy as np
6190
from pvtrace import *
6291

92+
# World node contains all objects
6393
world = Node(
6494
name="world (air)",
6595
geometry=Sphere(
@@ -68,6 +98,7 @@ world = Node(
6898
)
6999
)
70100

101+
# The glass sphere
71102
sphere = Node(
72103
name="sphere (glass)",
73104
geometry=Sphere(
@@ -78,12 +109,14 @@ sphere = Node(
78109
)
79110
sphere.location = (0, 0, 2)
80111

112+
# The source of rays
81113
light = Node(
82114
name="Light (555nm)",
83115
light=Light(direction=functools.partial(cone, np.pi/8)),
84116
parent=world
85117
)
86118

119+
# Render and ray-trace
87120
renderer = MeshcatRenderer(wireframe=True, open_browser=True)
88121
scene = Scene(world)
89122
renderer.render(scene)
@@ -102,12 +135,12 @@ while True:
102135
sys.exit()
103136
```
104137

105-
## Architecture
106-
107-
![](https://raw.githubusercontent.com/danieljfarrell/pvtrace/master/docs/pvtrace-design.png)
138+
## Scene Graph
108139

109140
*pvtrace* is designed in layers each with as limited scope as possible.
110141

142+
![](https://raw.githubusercontent.com/danieljfarrell/pvtrace/master/docs/pvtrace-design.png)
143+
111144
<dl>
112145
<dt>Scene</dt>
113146
<dd>Graph data structure of node and the thing that is ray-traced.</dd>
@@ -126,15 +159,70 @@ while True:
126159

127160
<dt>Components</dt>
128161
<dd>Specifies optical properties of the geometries volume, absorption coefficient, scattering coefficient, quantum yield, emission spectrum.</dd>
162+
163+
<dt>Ray-tracing engine</dt>
164+
<dd>The algorithm which spawns rays, computes intersections, samples probabilities and traverses the rays through the scene.</dd>
129165
</dl>
130166

131-
## Dependancies
167+
## Ray-tracing engine
168+
169+
Currently *pvtrace* supports only one ray-tracing engine: a photon path tracer. This is physically accurate, down to treating individual absorption and emission events, but is slow because the problem cannot be vectorised as each ray is followed individually.
170+
171+
# Documentation
172+
173+
Interactive Jupyter notebooks are in [examples directory](https://github.com/danieljfarrell/pvtrace/tree/master/examples), download and take a look, although they can be viewed online.
174+
175+
API documentation and some background at [https://pvtrace.readthedocs.io](https://pvtrace.readthedocs.io/)
176+
177+
# Contributing
178+
179+
Please use the github [issue](https://github.com/danieljfarrell/pvtrace/issues) tracker for bug fixes, suggestions, or support questions.
180+
181+
If you are considering contributing to pvtrace, first fork the project. This will make it easier to include your contributions using pull requests.
182+
183+
## Creating a development environment
184+
185+
1. First create a new development environment using [MacOS instructions](#macos-using-pyenv) or [Linux and Windows instructions](#linux-and-windows-using-conda), but do not install pvtrace using pip! You will need to clone your own copy of the source code in the following steps.
186+
2. Use the GitHub fork button to make your own fork of the project. This will make it easy to include your changes in pvtrace using a pull request.
187+
3. Follow the steps below to clone and install the development dependencies
188+
189+
```bash
190+
# Pull from your fork
191+
git clone https://github.com/<your username>/pvtrace.git
192+
193+
# Get development dependencies
194+
pip install -r pvtrace/requirements_dev.txt
195+
196+
# Add local `pvtrace` directory to known packages
197+
pip install -e pvtrace
198+
199+
# Run units tests
200+
pytest pvtrace/tests
201+
202+
# Run an example
203+
python pvtrace/examples/hello_world.py
204+
```
205+
206+
You should now be able to edit the source code and simply run scripts directly without the need to reinstall anything.
207+
208+
## Unit tests
209+
210+
Please add or modify an existing unit tests in the `pvtrace/tests` directory if you are adding new code. This will make it much easier to include your changes in the project.
211+
212+
## Pull requests
213+
214+
Pull requests will be considered. Please make contact before doing a lot of work, to make sure that the changes will definitely be included in the main project.
215+
216+
# Questions
217+
218+
You can get in contact with me directly at dan@excitonlabs.com or raise an issue on the issue tracker.
219+
220+
# Dependencies
132221

133222
Basic environment requires the following packages which will be installed with `pip` automatically
134223

135224
* python >= 3.7.2
136225
* numpy
137226
* trimesh[easy]
138227
* meshcat >= 0.0.16
139-
* anytree
140-
228+
* anytree

‎docs/pvtrace-demo.gif

2.83 MB
Loading

‎examples/001 Quick Start.ipynb

+15-88
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"cells": [
33
{
44
"cell_type": "code",
5-
"execution_count": 1,
5+
"execution_count": null,
66
"metadata": {},
77
"outputs": [],
88
"source": [
@@ -28,7 +28,7 @@
2828
},
2929
{
3030
"cell_type": "code",
31-
"execution_count": 2,
31+
"execution_count": null,
3232
"metadata": {},
3333
"outputs": [],
3434
"source": [
@@ -74,34 +74,9 @@
7474
},
7575
{
7676
"cell_type": "code",
77-
"execution_count": 3,
77+
"execution_count": null,
7878
"metadata": {},
79-
"outputs": [
80-
{
81-
"name": "stdout",
82-
"output_type": "stream",
83-
"text": [
84-
"You can open the visualizer by visiting the following URL:\n",
85-
"http://127.0.0.1:7000/static/\n"
86-
]
87-
},
88-
{
89-
"data": {
90-
"text/html": [
91-
"\n",
92-
"<div style=\"height: 400px; width: 600px; overflow-x: auto; overflow-y: hidden; resize: both\">\n",
93-
"<iframe src=\"http://127.0.0.1:7000/static/\" style=\"width: 100%; height: 100%; border: none\"></iframe>\n",
94-
"</div>\n"
95-
],
96-
"text/plain": [
97-
"<IPython.core.display.HTML object>"
98-
]
99-
},
100-
"execution_count": 3,
101-
"metadata": {},
102-
"output_type": "execute_result"
103-
}
104-
],
79+
"outputs": [],
10580
"source": [
10681
"renderer = MeshcatRenderer(wireframe=True)\n",
10782
"renderer.render(scene)\n",
@@ -110,24 +85,9 @@
11085
},
11186
{
11287
"cell_type": "code",
113-
"execution_count": 4,
88+
"execution_count": null,
11489
"metadata": {},
115-
"outputs": [
116-
{
117-
"data": {
118-
"application/vnd.jupyter.widget-view+json": {
119-
"model_id": "8de155093a2942e6956d4acc8b4644ec",
120-
"version_major": 2,
121-
"version_minor": 0
122-
},
123-
"text/plain": [
124-
"interactive(children=(FloatSlider(value=0.0, description='x', max=0.6, min=-0.6, step=0.01), FloatSlider(value…"
125-
]
126-
},
127-
"metadata": {},
128-
"output_type": "display_data"
129-
}
130-
],
90+
"outputs": [],
13191
"source": [
13292
"_ = interact_ray(scene, renderer)"
13393
]
@@ -141,7 +101,7 @@
141101
},
142102
{
143103
"cell_type": "code",
144-
"execution_count": 10,
104+
"execution_count": null,
145105
"metadata": {},
146106
"outputs": [],
147107
"source": [
@@ -172,7 +132,7 @@
172132
},
173133
{
174134
"cell_type": "code",
175-
"execution_count": 6,
135+
"execution_count": null,
176136
"metadata": {},
177137
"outputs": [],
178138
"source": [
@@ -182,20 +142,9 @@
182142
},
183143
{
184144
"cell_type": "code",
185-
"execution_count": 7,
145+
"execution_count": null,
186146
"metadata": {},
187-
"outputs": [
188-
{
189-
"data": {
190-
"text/plain": [
191-
"'GENERATE: Ray(pos=(0.00, 0.00, 0.00), dir=(0.00, 0.00, 1.00), nm=555.00, alive=True)'"
192-
]
193-
},
194-
"execution_count": 7,
195-
"metadata": {},
196-
"output_type": "execute_result"
197-
}
198-
],
147+
"outputs": [],
199148
"source": [
200149
"r, e = steps[0]\n",
201150
"f\"{e.name}: {r}\""
@@ -210,20 +159,9 @@
210159
},
211160
{
212161
"cell_type": "code",
213-
"execution_count": 8,
162+
"execution_count": null,
214163
"metadata": {},
215-
"outputs": [
216-
{
217-
"data": {
218-
"text/plain": [
219-
"'TRANSMIT: Ray(pos=(0.00, 0.00, 1.00), dir=(0.00, 0.00, 1.00), nm=555.00, alive=True)'"
220-
]
221-
},
222-
"execution_count": 8,
223-
"metadata": {},
224-
"output_type": "execute_result"
225-
}
226-
],
164+
"outputs": [],
227165
"source": [
228166
"r, e = steps[1]\n",
229167
"f\"{e.name}: {r}\""
@@ -238,20 +176,9 @@
238176
},
239177
{
240178
"cell_type": "code",
241-
"execution_count": 9,
179+
"execution_count": null,
242180
"metadata": {},
243-
"outputs": [
244-
{
245-
"data": {
246-
"text/plain": [
247-
"'EXIT: Ray(pos=(0.00, 0.00, 10.00), dir=(0.00, 0.00, 1.00), nm=555.00, alive=True)'"
248-
]
249-
},
250-
"execution_count": 9,
251-
"metadata": {},
252-
"output_type": "execute_result"
253-
}
254-
],
181+
"outputs": [],
255182
"source": [
256183
"r, e = steps[2]\n",
257184
"f\"{e.name}: {r}\""
@@ -307,7 +234,7 @@
307234
"name": "python",
308235
"nbconvert_exporter": "python",
309236
"pygments_lexer": "ipython3",
310-
"version": "3.7.2"
237+
"version": "3.7.6"
311238
}
312239
},
313240
"nbformat": 4,

‎examples/Luminescent solar concentrators.ipynb

+505
Large diffs are not rendered by default.

‎examples/Validation.ipynb

+739
Large diffs are not rendered by default.

‎examples/Validation.png

56.5 KB
Loading

‎examples/hello_box.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import time
2+
import sys
3+
import functools
4+
import numpy as np
5+
from pvtrace import *
6+
import logging
7+
logging.getLogger('trimesh').setLevel(logging.CRITICAL)
8+
9+
world = Node(
10+
name="world (air)",
11+
geometry=Sphere(
12+
radius=50.0,
13+
material=Material(refractive_index=1.0),
14+
)
15+
)
16+
17+
box = Node(
18+
name="sphere (glass)",
19+
geometry=Box(
20+
(10.0, 10.0, 1.0),
21+
material=Material(refractive_index=1.5),
22+
),
23+
parent=world
24+
)
25+
26+
light = Node(
27+
name="Light (555nm)",
28+
light=Light(),
29+
parent=world
30+
)
31+
light.rotate(np.radians(60), (1.0, 0.0, 0.0))
32+
33+
start_t = time.time()
34+
scene = Scene(world)
35+
for ray in scene.emit(100):
36+
steps = photon_tracer.follow(scene, ray)
37+
38+
print(f"Took {time.time() - start_t}s to trace 100 rays.")
39+
40+

‎examples/nested_cylinders.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,18 @@
3636
parent=world
3737
)
3838
cil1.translate((0, 0, 2))
39+
cil1.rotate(np.pi*0.2, (0, 1, 0))
3940

4041
cil2 = Node(
4142
name="B",
4243
geometry=Cylinder(
43-
length=0.5,
44+
length=2.0,
4445
radius=0.4,
45-
material=Material(refractive_index=6.0),
46+
material=Material(refractive_index=1.5),
4647
),
4748
parent=cil1
4849
)
50+
cil2.rotate(np.pi/2, (1, 0, 0))
4951

5052
# Add source of photons
5153
light = Node(
@@ -65,7 +67,7 @@
6567
viewer = MeshcatRenderer(open_browser=True, transparency=False, opacity=0.5, wireframe=True)
6668
scene = Scene(world)
6769
viewer.render(scene)
68-
for ray in scene.emit(100):
70+
for ray in scene.emit(20):
6971
history = photon_tracer.follow(scene, ray)
7072
path, events = zip(*history)
7173
viewer.add_ray_path(path)

‎pvtrace/__init__.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
__version__ = "2.1.3.dev0"
1+
__version__ = "2.1.3"
22
"""
33
Optical ray tracing for luminescent materials and spectral converter photovoltaic devices
44
"""
55
import logging
66

77
logging.basicConfig(level=logging.DEBUG)
88
logger = logging.getLogger("pvtrace")
9+
logging.getLogger("trimesh").setLevel(logging.CRITICAL)
10+
logging.getLogger("shapely.geos").setLevel(logging.CRITICAL)
11+
912

1013
# Import commonly used classes to pvtrace namespace so users don't
1114
# have to understand module layout.
@@ -14,7 +17,9 @@
1417
from .algorithm import photon_tracer
1518

1619
# data
17-
from .data import lumogen_f_red_305
20+
from .data import lumogen_f_red_305, fluro_red
21+
22+
from .device.lsc import LSC
1823

1924
# geometry
2025
from .geometry.box import Box
@@ -30,11 +35,7 @@
3035

3136

3237
# material
33-
from .material.component import (
34-
Scatterer,
35-
Absorber,
36-
Luminophore,
37-
)
38+
from .material.component import Scatterer, Absorber, Luminophore
3839
from .material.distribution import Distribution
3940
from .material.material import Material
4041
from .material.surface import (

‎pvtrace/algorithm/photon_tracer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def follow(scene, ray, maxsteps=1000, maxpathlength=np.inf, emit_method="kT"):
174174
component = material.component(ray.wavelength)
175175
if component.is_radiative(ray):
176176
ray = component.emit(
177-
ray.representation(scene.root, container), emit_method=emit_method
177+
ray.representation(scene.root, container), method=emit_method
178178
)
179179
ray = ray.representation(container, scene.root)
180180
if isinstance(component, Luminophore):

‎pvtrace/data/fluro_red.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import numpy as np
2+
from scipy.special import erf
3+
4+
def absorption(x):
5+
""" Fit to Coumarin Fluro Red absorption coefficient spectrum using four Gaussians.
6+
7+
Parameters
8+
----------
9+
x : numpy.array
10+
Wavelength array in nanometers. This should take values in the optical
11+
range between 200 and 900.
12+
13+
Returns
14+
-------
15+
numpy.array
16+
The spectrum normalised to peak value of 1.0.
17+
18+
Notes
19+
-----
20+
This fit is "good enough" for getting sensible answers but for research purposes
21+
you should be using your own data as this might not be exactly the same
22+
spectrum as your materials.
23+
24+
Example
25+
-------
26+
To make a absorption coefficient spectrum in the range 300 to 800 nanometers
27+
containing 200 points::
28+
29+
spectrum = absorption(np.linspace(300, 800, 200))
30+
"""
31+
p1 = 549.06438843562137
32+
a1 = 439.06754804626956
33+
w1 = 24.298601639828647
34+
35+
p2 = 379.48645797468572
36+
a2 = 85.177292848284353
37+
w2 = 13.513987279089216
38+
39+
p3 = 519.58858977131513
40+
a3 = 660.1731296017241
41+
w3 = 38.263352007649125
42+
43+
p4 = 490.05625608592726
44+
a4 = 511.11501615291041
45+
w4 = 52.213294432464529
46+
spec = (
47+
a1 * np.exp(-(((p1 - x) / w1) ** 2))
48+
+ a2 * np.exp(-(((p2 - x) / w2) ** 2))
49+
+ a3 * np.exp(-(((p3 - x) / w3) ** 2))
50+
+ a4 * np.exp(-(((p4 - x) / w4) ** 2))
51+
)
52+
spec = spec / np.max(spec)
53+
return spec
54+
55+
56+
def emission(x):
57+
""" Fit to Coumarin Fluro Red emission spectrum using an exponentially modified
58+
Gaussian.
59+
60+
Parameters
61+
----------
62+
x : numpy.array
63+
Wavelength array in nanometers. This should take values in the optical
64+
range between 200 and 900.
65+
66+
Returns
67+
-------
68+
numpy.array
69+
The spectrum normalised to peak value of 1.0
70+
71+
Notes
72+
-----
73+
This fit is "good enough" for getting sensible answers but for research purposes
74+
you should be using your own data as this might not be exactly the same
75+
spectrum as your materials.
76+
77+
Example
78+
-------
79+
To make a emission spectrum in the range 300 to 800 nanometers containing 200
80+
points::
81+
82+
spectrum = emission(np.linspace(300, 800, 200))
83+
"""
84+
def emg(x, a, b, c, d):
85+
r2 = np.sqrt(2)
86+
return a * c * np.sqrt(2 * np.pi) / (2 * d) * \
87+
np.exp((c**2/(2*d**2))-((x-b)/d)) * \
88+
(d/np.abs(d) + erf((x - b)/(r2*c) - c/(r2*d)))
89+
90+
a = 1.1477763237584664
91+
b = 592.06478874548839
92+
c = 19.981040318195117
93+
d = 12.723704058786568
94+
spec = emg(x, a, b, c, d)
95+
return spec
96+
97+
if __name__ == '__main__':
98+
import matplotlib.pyplot as plt
99+
x = np.arange(200, 900)
100+
plt.plot(x, absorption(x), label="absorption")
101+
plt.plot(x, emission(x), label="emission")
102+
plt.grid(linestyle='dashed')
103+
plt.show()

‎pvtrace/device/lsc.py

+636-40
Large diffs are not rendered by default.

‎pvtrace/geometry/utils.py

+2
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,8 @@ def allinrange(x, x_range):
376376
x_range : tuple of float
377377
A tuple defining a range like (xmin, xmax)
378378
"""
379+
if isinstance(x, (int, float, np.float, np.int)):
380+
x = np.array([x])
379381
return np.where(np.logical_or(x < x_range[0], x > x_range[1]))[0].size == 0
380382

381383

‎pvtrace/material/component.py

+1
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ def emit(self, ray: "Ray", method="kT", T=300.0, **kwargs) -> "Ray":
302302
nm = ray.wavelength
303303
# Different ways of sampling the emission distribution.
304304
if method == "kT":
305+
# Known issue: this can blue shift outside simulation range!
305306
# Emission energy can be within 3kT above current value. Simple bolzmann.
306307
eV = 1240.0 / nm
307308
eV = eV + 3 / 2 * kB * T # Assumes 3 dimensional degrees of freedom

‎pvtrace/material/distribution.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def __call__(self, x):
7777
return self._y
7878

7979
if not allinrange(x, self._x_range):
80-
raise ValueError("x is outside data range.")
80+
raise ValueError("x is outside data range.", {"x": x, "x_range": self._x_range})
8181

8282
if self.hist:
8383
idx = np.searchsorted(self._edges[:-1], x)
@@ -122,7 +122,7 @@ def lookup(self, x):
122122
123123
"""
124124
if not allinrange(x, self._x_range):
125-
raise ValueError("x is outside data range.")
125+
raise ValueError("x is outside data range.", {"x": x, "x_range": self._x_range})
126126

127127
if self.hist:
128128
idx = np.searchsorted(self._edges[:-1], x)

‎setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
author='Daniel Farrell',
1818
author_email='dan@excitonlabs.com',
1919
url='https://github.com/danieljfarrell/pvtrace',
20-
download_url = 'https://github.com/danieljfarrell/pvtrace/archive/v2.1.2.tar.gz',
20+
download_url = 'https://github.com/danieljfarrell/pvtrace/archive/v{}.tar.gz'.format(__version__),
2121
python_requires='>=3.7.2',
2222
packages=find_packages(),
2323
keywords=[

‎tests/test_3D_flux_comparison.py

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import pytest
2+
import sys
3+
import os
4+
import numpy as np
5+
import pandas as pd
6+
from pvtrace import LSC, Distribution, rectangular_mask
7+
from pvtrace.data import fluro_red
8+
9+
10+
@pytest.fixture(scope='module')
11+
def lsc():
12+
""" Compares an LSC simulation to 3D thermodynamic
13+
radiative transfer model
14+
15+
A.J, Chatten et al. https://doi.org/10.1134/1.1787111
16+
17+
Expected fractions:
18+
- edge = 0.25
19+
- escape = 0.66
20+
- loss = 0.09
21+
"""
22+
x = np.arange(400, 801, dtype=np.float)
23+
size = (l, w, d) = (4.8, 1.8, 0.250) # cm-1
24+
lsc = LSC(size, wavelength_range=x)
25+
26+
lsc.add_luminophore(
27+
'Fluro Red',
28+
np.column_stack((x, fluro_red.absorption(x) * 11.387815)), # cm-1
29+
np.column_stack((x, fluro_red.emission(x))),
30+
quantum_yield=0.95
31+
)
32+
33+
lsc.add_absorber(
34+
'PMMA',
35+
0.02 # cm-1
36+
)
37+
38+
def lamp_spectrum(x):
39+
""" Fit to an experimentally measured lamp spectrum with long wavelength filter.
40+
"""
41+
def g(x, a, p, w):
42+
return a * np.exp(-(((p - x) / w)**2 ))
43+
a1 = 0.53025700136646192
44+
p1 = 512.91400020614333
45+
w1 = 93.491838802960473
46+
a2 = 0.63578999789955015
47+
p2 = 577.63100003089369
48+
w2 = 66.031706473985736
49+
return g(x, a1, p1, w1) + g(x, a2, p2, w2)
50+
51+
lamp_dist = Distribution(x, lamp_spectrum(x))
52+
wavelength_callable = lambda : lamp_dist.sample(np.random.uniform())
53+
position_callable = lambda : rectangular_mask(l/2, w/2)
54+
lsc.add_light(
55+
"Oriel Lamp + Filter",
56+
(0.0, 0.0, 0.5 * d + 0.01), # put close to top surface
57+
rotation=(np.radians(180), (1, 0, 0)), # normal and into the top surface
58+
wavelength=wavelength_callable, # wavelength delegate callable
59+
position=position_callable # uniform surface illumination
60+
)
61+
62+
throw = 300
63+
lsc.simulate(throw, emit_method='redshift')
64+
return lsc
65+
66+
@pytest.mark.skip(reason="Takes to long to include in unit general tests.")
67+
def test_edge(lsc):
68+
incident = float(
69+
len(
70+
lsc.spectrum(
71+
source={"Oriel Lamp + Filter"},
72+
kind='first',
73+
facets={'top'}
74+
)
75+
)
76+
)
77+
edge = len(lsc.spectrum(facets={'left', 'right', 'near', 'far'}, source='all'))
78+
assert np.isclose(edge/incident, 0.25, atol=0.04)
79+
80+
@pytest.mark.skip(reason="Takes to long to include in unit general tests.")
81+
def test_escape(lsc):
82+
incident = float(
83+
len(
84+
lsc.spectrum(
85+
source={"Oriel Lamp + Filter"},
86+
kind='first',
87+
facets={'top'}
88+
)
89+
)
90+
)
91+
escape = len(lsc.spectrum(facets={'top', 'bottom'}, source='all'))
92+
assert np.isclose(escape/incident, 0.64, atol=0.04)
93+
94+
@pytest.mark.skip(reason="Takes to long to include in unit general tests.")
95+
def test_lost(lsc):
96+
incident = float(
97+
len(
98+
lsc.spectrum(
99+
source={"Oriel Lamp + Filter"},
100+
kind='first',
101+
facets={'top'}
102+
)
103+
)
104+
)
105+
lost = len(lsc.spectrum(source='all', events={'absorb'}))
106+
assert np.isclose(lost/incident, 0.11, atol=0.04)

0 commit comments

Comments
 (0)
Please sign in to comment.