├── .github
└── workflows
│ └── lint_and_test.yml
├── .gitignore
├── COPYING
├── INSTALL.txt
├── README.md
├── environment.yml
├── pyproject.toml
├── structcol
├── __init__.py
├── detector.py
├── detector_polarization_phase.py
├── event_distribution.py
├── model.py
├── montecarlo.py
├── phase_func_sphere.py
├── refractive_index.py
├── structure.py
└── tests
│ ├── __init__.py
│ ├── test_detector.py
│ ├── test_detector_sphere.py
│ ├── test_event_distribution.py
│ ├── test_fields.py
│ ├── test_mie.py
│ ├── test_model.py
│ ├── test_montecarlo.py
│ ├── test_montecarlo_bulk.py
│ ├── test_montecarlo_sphere.py
│ ├── test_refractive_index.py
│ ├── test_structcol.py
│ └── test_structure.py
└── tutorials
├── detector_tutorial.ipynb
├── event_distribution_tutorial.ipynb
├── fields_montecarlo_tutorial.ipynb
├── montecarlo_tutorial.ipynb
├── multiscale_color_mixing_tutorial.ipynb
├── multiscale_montecarlo_tutorial.ipynb
├── multiscale_polydispersity_tutorial.ipynb
├── structure_factor_data_tutorial.ipynb
└── tutorial.ipynb
/.github/workflows/lint_and_test.yml:
--------------------------------------------------------------------------------
1 | name: Lint, then test on all platforms
2 |
3 | on:
4 | # empty "push:" will trigger CI on push to any branch
5 | push:
6 | pull_request:
7 | branches: [ "develop", "master" ]
8 |
9 | jobs:
10 | lint:
11 | # only need to run on one platform since we're just looking at the code
12 | runs-on: ubuntu-latest
13 | defaults:
14 | run:
15 | shell: bash -el {0}
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: astral-sh/ruff-action@v3
19 | with:
20 | # fail if there are Python syntax errors or undefined names
21 | args: "check --select=E9,F63,F7,F82"
22 | # do another run to produce a linting report. exit-zero treats all errors
23 | # as warnings. This will flag codestyle problems in the PR but will not
24 | # cause the action to fail
25 | - run: ruff check --exit-zero --output-format=github
26 |
27 | test:
28 | # linting must succeed for testing to run; this helps us rapidly flag code
29 | # errors before going to testing
30 | needs: lint
31 | runs-on: ${{ matrix.os }}
32 | # conda enviroment activation requires bash
33 | defaults:
34 | run:
35 | shell: bash -el {0}
36 | strategy:
37 | fail-fast: false
38 | matrix:
39 | os: [ubuntu-latest, macos-latest, windows-latest]
40 | python-version: ["3.13"] # , "3.10", "3.11", "3.12"]
41 | max-parallel: 5
42 |
43 | steps:
44 | - uses: actions/checkout@v4
45 | - name: Set up miniforge with structcol environment
46 | uses: conda-incubator/setup-miniconda@v3
47 | with:
48 | environment-file: environment.yml
49 | miniforge-version: latest
50 | - name: Check out python-mie and install
51 | shell: bash -el {0}
52 | run: |
53 | git clone https://github.com/manoharan-lab/python-mie.git
54 | pip install ./python-mie
55 | rm -r ./python-mie
56 | - name: Install plugin to annotate test results
57 | run: pip install pytest-github-actions-annotate-failures
58 | - name: Test with pytest
59 | run: |
60 | pytest
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
91 | # others
92 | *.DS_Store
93 | *~
94 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 | {one line to give the program's name and a brief idea of what it does.}
635 | Copyright (C) {year} {name of author}
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | {project} Copyright (C) {year} {fullname}
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/INSTALL.txt:
--------------------------------------------------------------------------------
1 | The following Python packages are needed to run the code:
2 | - numpy
3 | - scipy
4 | - pint (needed to assign units to quantities)
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # structural-color
2 | Python package for modeling structural color in colloidal systems. See the
3 | tutorial Jupyter notebook (tutorial.ipynb) for instructions on using the
4 | package.
5 |
6 | Requires the [python-mie (pymie)](https://github.com/manoharan-lab/python-mie)
7 | package for Mie scattering calculations. To install:
8 |
9 | ```shell
10 | git clone https://github.com/manoharan-lab/python-mie.git
11 | pip install ./python-mie
12 | ```
13 |
14 | To remove:
15 |
16 | ```shell
17 | pip remove pymie
18 | ```
19 |
20 | You might want to first set up a virtual environment and install the pymie
21 | package there.
22 |
23 | The original code was developed by Sofia Magkiriadou (with contributions from
24 | Jerome Fung and others) during her research [1,2] in the
25 | [Manoharan Lab at Harvard University](http://manoharan.seas.harvard.edu). This
26 | research was supported by the National Science Foundation under grant number
27 | DMR-1420570 and by an International Collaboration Grant (Grant No.
28 | Sunjin-2010-002) from the Korean Ministry of Trade, Industry & Energy of Korea.
29 | The code has since been updated. It now works in Python 3, and it can handle
30 | quantities with dimensions (using [pint](https://github.com/hgrecco/pint)).
31 |
32 | [1] Magkiriadou, S.; Park, J.-G.; Kim, Y.-S.; and Manoharan, V. N. “Absence of
33 | Red Structural Color in Photonic Glasses, Bird Feathers, and Certain Beetles”
34 | *Physical Review E* 90, no. 6 (2014): 62302. [doi:10.1103/PhysRevE.90.062302](https://journals.aps.org/pre/abstract/10.1103/PhysRevE.90.062302)
35 |
36 | [2] Magkiriadou, S. “Structural Color from Colloidal Glasses” (PhD Thesis,
37 | Harvard University, 2014): [Download](http://dash.harvard.edu/bitstream/handle/1/14226099/MAGKIRIADOU-DISSERTATION-2015.pdf?sequence=1).
38 |
39 | Additional publications based on this code:
40 |
41 | 1. Stephenson, A. B.; Xiao, M.; Hwang, V.; Qu, L.; Odorisio, P. A.; Burke, M.; Task, K.; Deisenroth, T.; Barkley, S.; Darji, R. H.; Manoharan, V. N. “Predicting the Structural Colors of Films of Disordered Photonic Balls.” *ACS Photonics* 10, no. 1 (2023): 58-70. [doi:10.1021/acsphotonics.2c00892](https://pubs.acs.org/doi/abs/10.1021/acsphotonics.2c00892).
42 |
43 | 2. Xiao, M.; Stephenson, A. B.; Neophytou, A.; Hwang, V.; Chakrabarti, D.; Manoharan, V. N. “Investigating the Trade-off between Color Saturation and Angle-Independence in Photonic Glasses.” *Optics Express* 29, no. 14 (2021): 21212–21224. [doi:10.1364/OE.425399](https://opg.optica.org/abstract.cfm?uri=oe-29-14-21212).
44 |
45 | 3. Hwang, V.; Stephenson, A. B.; Barkley, S.; Brandt, S.; Xiao, M.; Aizenberg, J.; Manoharan, V. N. “Designing Angle-Independent Structural Colors Using Monte Carlo Simulations of Multiple Scattering.” *Proceedings of National Academy Sciences* 118, no. 4 (2021): e2015551118. [doi:10.1073/pnas.2015551118](https://www.pnas.org/doi/abs/10.1073/pnas.2015551118).
46 |
47 | 4. Hwang, V.\*; Stephenson, A. B.\*; Magkiriadou, S.; Park, J.-G.; Manoharan, V. N. “Effects of Multiple Scattering on Angle-Independent Structural Color in Disordered Colloidal Materials.” *Physical Review E* 101, no. 1 (2020): 012614. \*equal contribution [doi:10.1103/PhysRevE.101.012614](https://journals.aps.org/pre/abstract/10.1103/PhysRevE.101.012614).
48 |
49 |
--------------------------------------------------------------------------------
/environment.yml:
--------------------------------------------------------------------------------
1 | # dependencies for structural color package
2 | #
3 | # To use:
4 | # conda env create -f .\environment.yml
5 | # and then
6 | # conda activate structcol
7 | #
8 | # To update dependencies after changing this environment file:
9 | # conda env update --name structcol --file environment.yml --prune
10 | #
11 | # can also use mamba instead of conda in the above
12 | name: structcol
13 | channels:
14 | - conda-forge
15 | - defaults
16 | dependencies:
17 | - python>=3.11
18 | - numpy
19 | - scipy
20 | - pandas
21 | - pint
22 | - ipython
23 | - matplotlib
24 | - seaborn
25 |
26 | # include jupyterlab for convenience
27 | - jupyterlab
28 |
29 | # for running tests
30 | - pytest
31 |
32 | # for linting
33 | - ruff
34 |
35 | # for installing other dependencies
36 | - pip
37 |
38 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "structcol"
7 | version = "0.3"
8 | description = "Python package for modeling structural color"
9 | readme = "README.md"
10 | authors = [
11 | { name = "Manoharan Lab, Harvard University", email = "vnm@seas.harvard.edu" },
12 | ]
13 | dependencies = [
14 | "numpy",
15 | "pint",
16 | "scipy",
17 | ]
18 |
19 | [project.urls]
20 | Homepage = "https://github.com/manoharan-lab/structural-color"
21 |
22 | # pytest: convert all warnings to errors
23 | [tool.pytest.ini_options]
24 | filterwarnings = [
25 | "error",
26 | ]
27 |
28 | [tool.ruff]
29 | src = ['structcol']
30 | # don't check the notebooks
31 | exclude = ["tutorials"]
32 | line-length = 79
33 |
34 | [tool.ruff.lint]
35 | # Rulesets for ruff to check
36 | select = [
37 | # pyflakes rules
38 | "F",
39 | # pycodestyle (PEP8)
40 | "E", "W",
41 | ]
42 |
43 | [tool.ruff.lint.per-file-ignores]
44 | # Ignore long line warnings and unused variable warnings in test files. We
45 | # sometimes have long lines for nicely formatting gold results, and we sometimes
46 | # have unused variables just to check if function returns without an error
47 | "**/tests/*" = ["E501", "F841"]
48 | # Ignore "ambiguous variable name" when we use "l" as a variable in Mie
49 | # calculations
50 | "pymie/mie.py" = ["E741"]
51 | # for now, ignore "imported but unused" in __init__.py
52 | "__init__.py" = ["F401"]
53 |
--------------------------------------------------------------------------------
/structcol/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan, Sofia Makgiriadou
2 | #
3 | # This file is part of the structural-color python package.
4 | #
5 | # This package is free software: you can redistribute it and/or modify it under
6 | # the terms of the GNU General Public License as published by the Free Software
7 | # Foundation, either version 3 of the License, or (at your option) any later
8 | # version.
9 | #
10 | # This package is distributed in the hope that it will be useful, but WITHOUT
11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 | # details.
14 | #
15 | # You should have received a copy of the GNU General Public License along with
16 | # this package. If not, see .
17 |
18 | """
19 | The structural-color (structcol) python package includes theoretical models for
20 | predicting the structural color from disordered colloidal samples (also known
21 | as "photonic glasses").
22 |
23 |
24 | Notes
25 | -----
26 | Based on work by Sofia Magkiriadou in the Manoharan Lab at Harvard University
27 | [1]_
28 |
29 | Requires pint:
30 | PyPI: https://pypi.python.org/pypi/Pint/
31 | Github: https://github.com/hgrecco/pint
32 | Docs: https://pint.readthedocs.io/en/latest/
33 |
34 | References
35 | ----------
36 | [1] Magkiriadou, S., Park, J.-G., Kim, Y.-S., and Manoharan, V. N. “Absence of
37 | Red Structural Color in Photonic Glasses, Bird Feathers, and Certain Beetles”
38 | Physical Review E 90, no. 6 (2014): 62302. doi:10.1103/PhysRevE.90.062302
39 |
40 | .. moduleauthor :: Vinothan N. Manoharan
41 | .. moduleauthor :: Sofia Magkiriadou .
42 | """
43 |
44 | # Load the default unit registry from pint and use it everywhere.
45 | # Using the unit registry (and wrapping all functions) ensures that we don't
46 | # make unit mistakes.
47 | # Also load commonly used functions from pymie package
48 | from pymie import Quantity, ureg, q, index_ratio, size_parameter, np, mie
49 |
50 | # Global variable speed of light
51 | # get this from Pint in a somewhat indirect way:
52 | LIGHT_SPEED_VACUUM = Quantity(1.0, 'speed_of_light').to('m/s')
53 |
54 | def refraction(angles, n_before, n_after):
55 | '''
56 | Returns angles after refracting through an interface
57 |
58 | Parameters
59 | ----------
60 | angles: float or array of floats
61 | angles relative to normal before the interface
62 | n_before: float
63 | Refractive index of the medium light is coming from
64 | n_after: float
65 | Refractive index of the medium light is going to
66 |
67 | '''
68 | snell = n_before / n_after * np.sin(angles)
69 | snell[abs(snell) > 1] = np.nan # this avoids a warning
70 | return np.arcsin(snell)
71 |
72 |
73 | def normalize(x, y, z, return_nan=True):
74 | '''
75 | normalize a vector
76 |
77 | Parameters
78 | ----------
79 | x: float or array
80 | 1st component of vector
81 | y: float or array
82 | 2nd component of vector
83 | z: float or array
84 | 3rd component of vector
85 |
86 | Returns
87 | -------
88 | array of normalized vector(s) components
89 | '''
90 | magnitude = np.sqrt(np.abs(x) ** 2 + np.abs(y) ** 2 + np.abs(z) ** 2)
91 |
92 | # we ignore divide by zero error here because we do not want an error
93 | # in the case where we try to normalize a null vector <0,0,0>
94 | with np.errstate(divide='ignore', invalid='ignore'):
95 | if (not return_nan) and magnitude.all() == 0:
96 | magnitude[magnitude == 0] = 1
97 | return np.array([x / magnitude, y / magnitude, z / magnitude])
98 |
99 |
100 | def select_events(inarray, events):
101 | '''
102 | Selects the items of inarray according to event coordinates
103 |
104 | Parameters
105 | ----------
106 | inarray: 2D or 3D array
107 | Should have axes corresponding to events, trajectories
108 | or coordinates, events, trajectories
109 | events: 1D array
110 | Should have length corresponding to ntrajectories.
111 | Non-zero entries correspond to the event of interest
112 |
113 | Returns
114 | -------
115 | 1D array: contains only the elements of inarray corresponding to non-zero
116 | events values.
117 |
118 | '''
119 | # make inarray a numpy array if not already
120 | if isinstance(inarray, Quantity):
121 | inarray = inarray.magnitude
122 | inarray = np.array(inarray)
123 |
124 | # there is no 0th event, so disregard a 0 (or less) in the events array
125 | valid_events = (events > 0)
126 |
127 | # The 0th element in arrays such as direction refer to the 1st event
128 | # so subtract 1 from all the valid events to correct for array indexing
129 | ev = events[valid_events].astype(int) - 1
130 |
131 | # find the trajectories where there are valid events
132 | tr = np.where(valid_events)[0]
133 |
134 | # want output of the same form as events, so create variable
135 | # for object type
136 | dtype = type(np.ndarray.flatten(inarray)[0])
137 |
138 | # get an output array with elements corresponding to the input events
139 | if len(inarray.shape) == 2:
140 | outarray = np.zeros(len(events), dtype=dtype)
141 | outarray[valid_events] = inarray[ev, tr]
142 |
143 | if len(inarray.shape) == 3:
144 | outarray = np.zeros((inarray.shape[0], len(events)), dtype=dtype)
145 | outarray[:, valid_events] = inarray[:, ev, tr]
146 |
147 | if isinstance(inarray, Quantity):
148 | outarray = Quantity(outarray, inarray.units)
149 | return outarray
150 |
151 | # Create a module-wide random number generator object that will be used by
152 | # default in any functions that do random sampling. Users can override the
153 | # default by passing their own rng to such functions. A user-specified rng is
154 | # needed for testing and may be useful for parallel computation.
155 | rng = np.random.default_rng()
156 |
--------------------------------------------------------------------------------
/structcol/detector_polarization_phase.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of the structural-color python package.
4 | #
5 | # This package is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This package is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this package. If not, see .
17 |
18 | """
19 | This module provides functions for detecting properties of
20 | trajectories simulated by the Monte Carlo model that are
21 | related to it's field properties: polarization and phase.
22 |
23 |
24 | .. moduleauthor:: Annie Stephenson
25 |
26 | """
27 | from pymie import mie
28 | from . import select_events
29 | from . import LIGHT_SPEED_VACUUM
30 | import numpy as np
31 | import structcol as sc
32 | import warnings
33 |
34 |
35 | def calc_refl_phase_fields(trajectories, refl_indices, refl_per_traj,
36 | components=False):
37 | '''
38 | Calculates the reflectance including phase, by considering trajectories
39 | that exit at the same time to be coherent. To do this, we must bin
40 | trajectories with similar exit times and add their fields. Then
41 | we convolve the reflectance as a function of time with a step function
42 | in order to give a steady state value for the reflectance.
43 |
44 | Parameters
45 | ----------
46 | trajectories: Trajectory object
47 | Trajectory object used in Monte Carlo simulation
48 | refl_indices: 1d array (length: ntraj)
49 | array of event indices for reflected trajectories
50 | refl_per_traj: 1d array (length: ntraj)
51 | reflectance distributed to each trajectory, including fresnel
52 | contributions
53 | components: boolean
54 |
55 | Returns
56 | -------
57 | if components == True:
58 | return tot_field_x, tot_field_y, tot_field_z, refl_fields,
59 | refl_non_phase / intensity_incident
60 | else:
61 | return refl_fields, refl_non_phase / intensity_incident
62 | '''
63 |
64 | ntraj = len(trajectories.direction[0, 0, :])
65 |
66 | if np.all(refl_indices == 0):
67 | no_refl_warn = '''No trajectories were reflected.
68 | Check sample parameters or increase number
69 | of trajectories.'''
70 | warnings.warn(no_refl_warn)
71 | if isinstance(trajectories.weight, sc.Quantity):
72 | weights = trajectories.weight.magnitude
73 | else:
74 | weights = trajectories.weight
75 |
76 | # Get the amplitude of the field
77 | # The expression below gives 0 for not reflected traj, but that's fine
78 | # since we only care about reflected trajectories.
79 | w = np.sqrt(refl_per_traj * ntraj)
80 |
81 | # Write expression for field.
82 | # 0th event is before entering sample, so we start from 1,
83 | # for later use with select_events.
84 | traj_field_x = w * trajectories.fields[0, 1:, :]
85 | traj_field_y = w * trajectories.fields[1, 1:, :]
86 | traj_field_z = w * trajectories.fields[2, 1:, :]
87 |
88 | # Select traj_field values only for the reflected indices.
89 | refl_field_x = select_events(traj_field_x, refl_indices)
90 | refl_field_y = select_events(traj_field_y, refl_indices)
91 | refl_field_z = select_events(traj_field_z, refl_indices)
92 |
93 | # Add reflected fields from all trajectories.
94 | tot_field_x = np.sum(refl_field_x)
95 | tot_field_y = np.sum(refl_field_y)
96 | tot_field_z = np.sum(refl_field_z)
97 |
98 | # Calculate the incoherent reflectance for comparison.
99 | non_phase_int_x = np.conj(refl_field_x) * refl_field_x
100 | non_phase_int_y = np.conj(refl_field_y) * refl_field_y
101 | non_phase_int_z = np.conj(refl_field_z) * refl_field_z
102 | refl_non_phase = np.sum(non_phase_int_x + non_phase_int_y
103 | + non_phase_int_z)
104 |
105 | # Calculate intensity as E^*E.
106 | intensity_x = np.conj(tot_field_x) * tot_field_x
107 | intensity_y = np.conj(tot_field_y) * tot_field_y
108 | intensity_z = np.conj(tot_field_z) * tot_field_z
109 |
110 | # Add the x,y, and z intensity.
111 | refl_intensity = np.sum(intensity_x + intensity_y + intensity_z)
112 |
113 | # Normalize, assuming incident light is incoherent.
114 | intensity_incident = ntraj # np.sum(weights[0,:])
115 | refl_fields = np.real(refl_intensity / intensity_incident)
116 |
117 | refl_x = np.sum(intensity_x) / intensity_incident
118 | refl_y = np.sum(intensity_y) / intensity_incident
119 | refl_z = np.sum(intensity_z) / intensity_incident
120 | refl_intensity_tot = np.real(refl_non_phase / intensity_incident)
121 |
122 | if components:
123 | return (tot_field_x, tot_field_y, tot_field_z, refl_fields,
124 | refl_intensity_tot)
125 | else:
126 | return refl_fields, refl_intensity_tot
127 |
128 |
129 | def calc_refl_co_cross_fields(trajectories, refl_indices, refl_per_traj,
130 | det_theta):
131 | '''
132 | Goniometer detector size should already be taken account
133 | in calc_refl_trans() so the refl_indices will only include trajectories
134 | that exit within the detector area.
135 |
136 | Muliplying by the sines and cosines of the detector theta is an
137 | approximation, since the goniometer detector area is usually small
138 | enough such that the detector size is not that big. Should check that
139 | this approximation is reasonable. The alternative would be to keep track
140 | of the actual exit theta of each trajectory, using the direction property.
141 |
142 | '''
143 |
144 | (tot_field_x,
145 | tot_field_y,
146 | tot_field_z,
147 | refl_field,
148 | refl_intensity) = calc_refl_phase_fields(trajectories, refl_indices,
149 | refl_per_traj,
150 | components=True)
151 | if isinstance(tot_field_x, sc.Quantity):
152 | tot_field_x = tot_field_x.magnitude
153 | tot_field_y = tot_field_y.magnitude
154 | tot_field_z = tot_field_z.magnitude
155 | if isinstance(det_theta, sc.Quantity):
156 | det_theta = det_theta.to('radians').magnitude
157 |
158 | # Incorporate geometry of the goniometer setup.
159 | # Rotate the total x, y, z fields to the par/perp detector basis,
160 | # by performing a clockwise rotation about the y-axis by angle det_theta.
161 | # Co-polarized field is mostly x-polarized.
162 | # Cross-polarized field is mostly y-polarized.
163 | # Field perpendicular to scattering plane is mostly z-polarized.
164 | tot_field_co = (tot_field_x * np.cos(det_theta) + tot_field_z
165 | * np.sin(det_theta))
166 | tot_field_cr = tot_field_y
167 | tot_field_perp = (-tot_field_x * np.sin(det_theta) + tot_field_z
168 | * np.cos(det_theta))
169 |
170 | # Take the modulus to get intensity.
171 | refl_co = np.real(np.conj(tot_field_co) * tot_field_co)
172 | refl_cr = np.real(np.conj(tot_field_cr) * tot_field_cr)
173 | refl_perp = np.real(np.conj(tot_field_perp) * tot_field_perp)
174 |
175 | return (refl_co, refl_cr, refl_perp, refl_field, refl_intensity)
176 |
177 |
178 | def calc_traj_time(step, exit_indices, radius,
179 | n_particle, n_sample, wavelength,
180 | min_angle=0.01,
181 | num_angles=200):
182 | '''
183 | Calculates the amount of time each trajectory spends scattering in the
184 | sample before exit
185 |
186 | TODO: make this work for polydisperse, core-shell, and bispecies
187 |
188 | parameters:
189 | ----------
190 | step: 2d array (structcol.Quantity [length])
191 | Step sizes between scattering events in each of the trajectories.
192 | exit_indices: 1d array (length: ntrajectories)
193 | event number at exit for each trajectory. Input refl_indices if you
194 | want to only consider reflectance and trans_indices if you want to only
195 | consider transmittance. Input refl_indices + trans_indices if you
196 | want to consider both
197 | radius: float (structcol.Quantity [length])
198 | Radius of particle.
199 | n_particle: float
200 | Index of refraction of particle.
201 | n_sample: float
202 | Index of refraction of sample.
203 | wavelength: float (structcol.Quantity [length])
204 | Wavelength.
205 | min_angle: float (in radians)
206 | minimum angle to integrate over for total cross section
207 | num_angles: float
208 | number of angles to integrate over for total cross section
209 |
210 | returns:
211 | -------
212 | traj_time: 1d array (structcol.Quantity [time], length ntraj)
213 | time each trajectory spends scattering inside the sample before exit
214 | travel_time: 1d array (structcol.Quantity [time], length ntraj)
215 | time each trajectory spends travelling inside the sample before exit
216 | dwell_time: float (structcol.Quantity [time])
217 | time duration of scattering inside a particle
218 | '''
219 |
220 | # calculate the path length
221 | ntraj = len(exit_indices)
222 | path_length_traj = sc.Quantity(np.zeros(ntraj), 'um')
223 |
224 | for i in range(0, ntraj):
225 | path_length_traj[i] = np.sum(step[:exit_indices[i], i])
226 | stuck_traj_ind = np.where(path_length_traj.magnitude == 0)[0]
227 |
228 | # calculate the time passed based on distance travelled
229 | velocity = LIGHT_SPEED_VACUUM / np.real(n_sample.magnitude)
230 | travel_time = path_length_traj / velocity
231 |
232 | # calculate the dwell time in a scatterer
233 | dwell_time = mie.calc_dwell_time(radius, n_sample, n_particle, wavelength,
234 | min_angle=min_angle,
235 | num_angles=num_angles)
236 |
237 | # add the dwell times and travel times
238 | traj_time = travel_time + dwell_time
239 |
240 | # set traj_time = 0 for stuck trajectories
241 | traj_time[stuck_traj_ind] = sc.Quantity(0.0, 'fs')
242 |
243 | # change units to femtoseconds and discard imaginary part
244 | traj_time = traj_time.to('fs')
245 | traj_time = np.real(traj_time.magnitude)
246 | traj_time = sc.Quantity(traj_time, 'fs')
247 |
248 | return traj_time, travel_time, dwell_time
249 |
--------------------------------------------------------------------------------
/structcol/refractive_index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Copyright 2016, Vinothan N. Manoharan, Sofia Makgiriadou, Victoria Hwang
3 | #
4 | # This file is part of the structural-color python package.
5 | #
6 | # This package is free software: you can redistribute it and/or modify it under
7 | # the terms of the GNU General Public License as published by the Free Software
8 | # Foundation, either version 3 of the License, or (at your option) any later
9 | # version.
10 | #
11 | # This package is distributed in the hope that it will be useful, but WITHOUT
12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14 | # details.
15 | #
16 | # You should have received a copy of the GNU General Public License along with
17 | # this package. If not, see .
18 |
19 | """
20 | Functions for calculating refractive index as a function of wavelength for
21 | various materials.
22 |
23 | Notes
24 | -----
25 | Most of this data is from refractiveindex.info [1]_. According to
26 | http://refractiveindex.info/download.php,
27 | "refractiveindex.info database is in public domain. Copyright and related
28 | rights were waived by Mikhail Polyanskiy through the CC0 1.0 Universal Public
29 | Domain Dedication. You can copy, modify and distribute refractiveindex.info
30 | database, even for commercial purposes, all without asking permission."
31 |
32 | References
33 | ----------
34 | [1] Dispersion formulas from M. N. Polyanskiy. "Refractive index database,"
35 | http://refractiveindex.info (accessed August 14, 2016).
36 |
37 | .. moduleauthor :: Vinothan N. Manoharan
38 | .. moduleauthor :: Sofia Magkiriadou
39 | .. moduleauthor :: Victoria Hwang .
40 | """
41 |
42 | import numpy as np
43 | # unit registry and Quantity constructor from pint
44 | from . import ureg, Quantity
45 | from scipy.optimize import fsolve
46 | from scipy.interpolate import interp1d
47 | import warnings
48 |
49 | # dictionary of refractive index dispersion formulas. This is used by the 'n'
50 | # function below; it's outside the function definition so that it doesn't have
51 | # to be initialized on every function call (see stackoverflow 60208).
52 | #
53 | # NOTE: If you add a material to the dictionary, you need to add a test
54 | # function to structcol/tests/test_refractive_index.py that will test to make
55 | # sure the dispersion relation returns the proper values of the refractive
56 | # index at two or more points.
57 | #
58 | # np.power doesn't seem to be supported by pint -- hence the w*w... or
59 | # /w/w/w/w... syntax
60 | n_dict = {
61 | # water data from M. Daimon and A. Masumura. Measurement of the refractive
62 | # index of distilled water from the near-infrared region to the ultraviolet
63 | # region, Appl. Opt. 46, 3811-3820 (2007).
64 | # Fit of the experimental data with the Sellmeier dispersion formula:
65 | # refractiveindex.info
66 | # data for high performance liquid chromatography (HPLC) distilled water at
67 | # 20.0 °C
68 | 'water': lambda w: np.sqrt(5.684027565e-1*w*w/
69 | (w*w - Quantity('5.101829712e-3 um^2')) +
70 | 1.726177391e-1*w*w/
71 | (w*w - Quantity('1.821153936e-2 um^2')) +
72 | 2.086189578e-2*w*w/
73 | (w*w - Quantity('2.620722293e-2 um^2')) +
74 | 1.130748688e-1*w*w/
75 | (w*w - Quantity('1.069792721e1 um^2'))
76 | + 1),
77 |
78 |
79 | # polystyrene data from N. Sultanova, S. Kasarova and I. Nikolov.
80 | # Dispersion properties of optical polymers, Acta Physica Polonica A 116,
81 | # 585-587 (2009).
82 | # Fit of the experimental data with the Sellmeier dispersion formula:
83 | # refractiveindex.info
84 | # data for 20 degrees C, 0.4368-1.052 micrometers
85 | 'polystyrene': lambda w: np.sqrt(1.4435*w*w/
86 | (w*w-Quantity("0.020216 um^2"))+1),
87 |
88 | # pmma data from G. Beadie, M. Brindza, R. A. Flynn, A. Rosenberg, and J.
89 | # S. Shirk. Refractive index measurements of poly(methyl methacrylate)
90 | # (PMMA) from 0.4-1.6 micrometers, Appl. Opt. 54, F139-F143 (2015)
91 | # refractiveindex.info
92 | # data for 20.1 degrees C, 0.42-1.62 micrometers
93 | 'pmma': lambda w: np.sqrt(2.1778 + Quantity('6.1209e-3 um^-2')*w*w -
94 | Quantity('1.5004e-3 um^-4')*w*w*w*w +
95 | Quantity('2.3678e-2 um^2')/w/w -
96 | Quantity('4.2137e-3 um^4')/w/w/w/w +
97 | Quantity('7.3417e-4 um^6')/w/w/w/w/w/w -
98 | Quantity('4.5042e-5 um^8')/w/w/w/w/w/w/w/w),
99 |
100 | # rutile TiO2 from J. R. Devore. Refractive Indices of Rutile and
101 | # Sphalerite, J. Opt. Soc. Am. 41, 416-419 (1951)
102 | # refractiveindex.info
103 | # data for rutile TiO2, ordinary ray, 0.43-1.53 micrometers
104 | 'rutile': lambda w: np.sqrt(5.913 +
105 | Quantity('0.2441 um^2')/
106 | (w*w - Quantity('0.0803 um^2'))),
107 |
108 | # fused silica (amorphous quartz) data from I. H. Malitson. Interspecimen
109 | # Comparison of the Refractive Index of Fused Silica, J. Opt. Soc. Am. 55,
110 | # 1205-1208 (1965)
111 | # refractiveindex.info
112 | # data for "room temperature", 0.21-3.71 micrometers
113 | 'fused silica': lambda w: np.sqrt(1 + 0.6961663*w*w/
114 | (w*w - Quantity('0.0684043**2 um^2')) +
115 | 0.4079426*w*w/
116 | (w*w - Quantity('0.1162414**2 um^2')) +
117 | 0.8974794*w*w/
118 | (w*w - Quantity('9.896161**2 um^2'))),
119 | # soda lime glass data from M. Rubin. Optical properties of soda lime
120 | # silica glasses, Solar Energy Materials 12, 275-288 (1985)
121 | # refractiveindex.info
122 | # data for "room temperature", 0.31-4.6 micrometers
123 | 'soda lime glass': lambda w: (1.5130 - Quantity('0.003169 um^-2')*w*w
124 | + Quantity('0.003962 um^2')/(w*w)),
125 |
126 |
127 | # zirconia (ZrO2) data from I. Bodurov, I. Vlaeva, A. Viraneva,
128 | # T. Yovcheva, S. Sainov. Modified design of a laser refractometer,
129 | # Nanoscience & Nanotechnology 16, 31-33 (2016).
130 | # data for 24 degrees C, 0.405 - 0.635 micrometers
131 | 'zirconia': lambda w: np.sqrt(1 + 3.3037*w*w/
132 | (w*w - Quantity('0.1987971**2 um**2'))),
133 |
134 | # ethanol data from J. Rheims, J Köser and T Wriedt. Refractive-index
135 | # measurements in the near-IR using an Abbe refractometer,
136 | # Meas. Sci. Technol. 8, 601-605 (1997)
137 | # refractiveindex.info
138 | 'ethanol': lambda w: (1.35265 + Quantity('0.00306 um^2')/(w**2)
139 | + Quantity('0.00002 um^4')/(w**4)),
140 |
141 | # the w/w is a crude hack to make the function output an array when the
142 | # input is an array
143 | 'vacuum': lambda w: Quantity('1.0')*w/w,
144 |
145 | # brookite TiO2 from Radhakrishnan. "The Optical Properties of titanium
146 | # dioxide". Proceedings of the Indian Academy of Sciences-Mathematical
147 | # Sciences March 1982, 35:117. Note that this is for n_alpha. However,
148 | # n_alpha is almost identical to n_beta, which in turn is very similar to
149 | # rutile. However n_gamma is a bit different, but is not considered
150 | # data for rutile TiO2, ordinary ray, 0.43-0.71 micrometers
151 | 'brookite': lambda w: np.sqrt(2.9858 + 2.1036*w*w
152 | /(w*w - Quantity('0.287**2 um^2'))
153 | -Quantity('0.18 um^-2')*w*w+1.),
154 |
155 | # anatase TiO2 from Wang et al. Think Solid Films. 405, 2002, 50-54
156 | # measured from 500-1700 nm
157 | 'anatase': lambda w: 2.1526 + Quantity('4.1155e-2 um^2')/(w*w)+
158 | Quantity('2.1798e-3 um^4')/(w*w*w*w)
159 | }
160 |
161 | # ensures wavelen has units of length
162 | @ureg.check(None, '[length]', None, None, None)
163 | def n(material, wavelen, index_data=None, wavelength_data=None, kind='linear'):
164 | """
165 | Refractive index of various materials.
166 |
167 | Parameters
168 | ----------
169 | material: string
170 | Material type; if not found in dictionary, assumes vacuum
171 | w: structcol.Quantity [length]
172 | Wavelength in vacuum.
173 | index_data: array (optional)
174 | Refractive index data from literature or experiment that the user can
175 | input if desired. The data is interpolated, so that the user can call
176 | specific values of the index. The index data can be real or complex.
177 | wavelength_data: Quantity array (optional)
178 | Wavelength data corresponding to index_data. Must be specified as pint
179 | Quantity.
180 | kind: string (optional)
181 | Type of interpolation. The options are: ‘linear’, ‘nearest’, ‘zero’,
182 | ‘slinear’, ‘quadratic’, ‘cubic’, ‘previous’, ‘next',
183 | where ‘zero’, ‘slinear’, ‘quadratic’ and ‘cubic’ refer to a spline
184 | interpolation of zeroth, first, second or third order; ‘previous’ and
185 | ‘next’ simply return the previous or next value of the point.
186 | The default is 'linear'.
187 |
188 | Returns
189 | -------
190 | structcol.Quantity (dimensionless)
191 | refractive index
192 |
193 | Dispersion formulas from M. N. Polyanskiy. "Refractive index database,"
194 | http://refractiveindex.info (accessed August 14, 2016).
195 | """
196 | if material == 'data':
197 | if index_data is None or wavelength_data is None:
198 | raise KeyError("'data' material requires input of index "
199 | "and corresponding wavelength data.")
200 |
201 | if isinstance(index_data, Quantity):
202 | index_data = index_data.magnitude
203 | fit = interp1d(wavelength_data.magnitude, index_data, kind=kind)
204 | wavelen = wavelen.to(wavelength_data.units).round(2)
205 | return Quantity(fit(wavelen.magnitude), '')
206 |
207 | else:
208 | if index_data is not None or wavelength_data is not None:
209 | warnings.warn("No need to specify the index or wavelength data. "
210 | "No material except for 'data' requires input data.")
211 | try:
212 | return n_dict[material](wavelen)
213 | except KeyError:
214 | print("Material \""+material+"\" not implemented. Perhaps a typo?")
215 | raise
216 |
217 | #------------------------------------------------------------------------------
218 | # OTHER MATERIALS
219 | # for the rest of these materials, need to find dispersion relations and
220 | # implement the functions in the dictionary.
221 | def n_silica_colloidal(w):
222 | return 1.40
223 |
224 | def n_keratin(w):
225 | return 1.532
226 |
227 | def n_ptbma(w):
228 | # from http://www.sigmaaldrich.com/catalog/product/aldrich/181587?lang=en®ion=US
229 | return 1.46
230 |
231 | #------------------------------------------------------------------------------
232 | # CARGILLE OILS
233 |
234 | def n_cargille(i,series,w):
235 | """
236 | Refractive index of cargille index-matching oils
237 | available at:
238 | http://www.cargille.com/refractivestandards.shtml
239 |
240 | Parameters
241 | ----------
242 | i: int
243 | The cardinal number of the liquid (starting with 0
244 | valid cardinal numbers:
245 | AAA: 0-19
246 | AA: 0-29
247 | A: 0-90
248 | B: 0-29
249 | E: 0-28
250 | acrylic: 0
251 | series: string
252 | the series of the cargille index matching liquid. Can be A, AA, AAA, B,
253 | E, or acrylic
254 | w : structcol.Quantity [length]
255 | Wavelength in vacuum.
256 |
257 | Returns
258 | -------
259 | structcol.Quantity (dimensionless)
260 | refractive index
261 | """
262 | cs = {}
263 | ds = {}
264 | es = {}
265 |
266 | # convert wavelength to micrometers to make units compatible for given oil
267 | # coefficients
268 |
269 | w = w.to('um')
270 |
271 | ## Series AAA ##
272 |
273 | cs['AAA'] = np.array([1.295542, 1.30031, 1.305078, 1.309845, 1.314614,
274 | 1.319379, 1.324146, 1.328914, 1.333685, 1.338451, 1.343219,
275 | 1.347986, 1.352753, 1.357522, 1.362290, 1.367058, 1.371824,
276 | 1.376592, 1.38136, 1.386127])
277 |
278 | ds['AAA'] = np.array([148828.2, 157595.9, 166363.5, 175352.1, 184119.7,
279 | 193034.7, 201949.6, 210643.5, 219558.5, 228252.4, 237020,
280 | 245861.3, 254850, 263470.2, 272237.8, 281079.1, 290067.7,
281 | 298835.3, 307603, 316444.2]) * 10**(-8)
282 |
283 | es['AAA'] = np.array([2.05E+11, 1.85E+11, 1.56E+11, 1.29E+11, 1.01E+11,
284 | 7.42E+10, 4.94E+10, 2.22E+10, -2.47E+09, -3.21E+10,
285 | -5.44E+10, -8.16E+10, -1.06E+11, -1.33E+11, -1.58E+11,
286 | -1.83E+11, -2.15E+11, -2.40E+11, -2.65E+11,
287 | -2.89E+11]) * 10**(-16)
288 |
289 | ## Series AA ##
290 |
291 | cs['AA'] = np.array([1.387868, 1.389882, 1.391889, 1.393901, 1.395912,
292 | 1.397926, 1.399937, 1.401949, 1.403958, 1.40597, 1.407981,
293 | 1.409992, 1.412004, 1.414014, 1.416028, 1.418037, 1.420049,
294 | 1.422058, 1.42407, 1.426082, 1.428093, 1.430105, 1.432116,
295 | 1.434128, 1.43614, 1.438149, 1.440161, 1.442173, 1.444184,
296 | 1.446195])
297 |
298 | ds['AA'] = np.array([434180.6, 432928.1, 431970.3, 430644.1, 429465.3,
299 | 428360.1, 427033.9, 425928.8, 424971, 423571.1, 422465.9,
300 | 421287.1, 419960.9, 418929.4, 417455.9, 416571.7, 415392.9,
301 | 414214, 412961.5, 412003.7, 410530.2, 409572.4, 408393.5,
302 | 406993.7, 405814.8, 404783.4, 403604.5, 402425.7, 401173.2,
303 | 400141.7]) * 10**(-8)
304 |
305 | es['AA'] = np.array([-4.47E+11, -4.18E+11, -3.93E+11, -3.66E+11, -3.39E+11,
306 | -3.14E+11, -2.77E+11, -2.57E+11, -2.27E+11, -2.03E+11,
307 | -1.78E+11, -1.51E+11, -1.16E+11, -8.65E+10, -6.67E+10,
308 | -3.71E+10, -1.24E+10, 1.48E+10, 4.45E+10, 7.17E+10,
309 | 9.64E+10, 1.24E+11, 1.51E+11, 1.80E+11, 2.08E+11, 2.35E+11,
310 | 2.60E+11, 2.87E+11, 3.19E+11, 3.44E+11]) * 10**(-16)
311 |
312 | ## Series A ##
313 |
314 | cs['A'] = np.array([1.447924, 1.449697, 1.451466, 1.453239, 1.45501,
315 | 1.456781, 1.458555, 1.460322, 1.462094, 1.463866, 1.465634,
316 | 1.467407, 1.469178, 1.470951, 1.472723, 1.474492, 1.476265,
317 | 1.478035, 1.479808, 1.481575, 1.483347, 1.485118, 1.486889,
318 | 1.488659, 1.490433, 1.492205, 1.493976, 1.495745, 1.497516,
319 | 1.499288, 1.501059, 1.502832, 1.504600, 1.506372, 1.508142,
320 | 1.509916, 1.511686, 1.513456, 1.515231, 1.517000, 1.518769,
321 | 1.520540, 1.522312, 1.524080, 1.525856, 1.527626, 1.529397,
322 | 1.531168, 1.532941, 1.534712, 1.536480, 1.538251, 1.540023,
323 | 1.541794, 1.543566, 1.545338, 1.546669, 1.548351, 1.550034,
324 | 1.551717, 1.553395, 1.555081, 1.556763, 1.558443, 1.560123,
325 | 1.561807, 1.563488, 1.565175, 1.566852, 1.568536, 1.570218,
326 | 1.571900, 1.573582, 1.575262, 1.576944, 1.578628, 1.580308,
327 | 1.581992, 1.583672, 1.585352, 1.587038, 1.588717, 1.590402,
328 | 1.592081, 1.593764, 1.595445, 1.597127, 1.598809, 1.600493,
329 | 1.602174, 1.603857])
330 |
331 | ds['A'] = np.array([407435.7, 413993, 420624, 426960.2, 433591.2, 440148.5,
332 | 446558.4, 453336.7, 459967.7, 466451.3, 473303.3, 479860.6,
333 | 486196.8, 492975.2, 499237.7, 505942.4, 512647.0, 519130.6,
334 | 525835.3, 532539.9, 538728.8, 545433.4, 551917.0, 558695.3,
335 | 565179.0, 571736.2, 578219.8, 584998.1, 591481.8, 598039.0,
336 | 604670.0, 611080.0, 618005.6, 624489.2, 630972.8, 637603.8,
337 | 644234.8, 650718.3, 657128.3, 663759.2, 670316.5, 677094.8,
338 | 683725.8, 690283.1, 696840.4, 703250.3, 709955.0, 716438.5,
339 | 722995.8, 729626.8, 736331.4, 742962.4, 749372.3, 756003.3,
340 | 762560.6, 769191.5, 785400.5, 792252.5, 799546.6, 806251.2,
341 | 813471.6, 820544.6, 827470.3, 834322.3, 841542.6, 848321.0,
342 | 855394.0, 862246.0, 869466.4, 876465.7, 883391.4, 890243.4,
343 | 897390.1, 904242.1, 911241.4, 918314.4, 925240.1, 932239.5,
344 | 939312.5, 946238.2, 953090.2, 960384.2, 967162.6, 974088.2,
345 | 981308.6, 988234.2, 995159.9, 1002380.0, 1009085.0,
346 | 1016084.0, 1023305.0]) * 10**(-8)
347 |
348 | es['A'] = np.array([4.15E+11, 4.62E+11, 5.12E+11, 5.61E+11, 6.08E+11,
349 | 6.50E+11, 7.05E+11, 7.49E+11, 7.96E+11, 8.45E+11, 8.90E+11,
350 | 9.42E+11, 9.89E+11, 1.04E+12, 1.08E+12, 1.13E+12, 1.18E+12,
351 | 1.23E+12, 1.27E+12, 1.32E+12, 1.37E+12, 1.41E+12, 1.46E+12,
352 | 1.51E+12, 1.56E+12, 1.60E+12, 1.65E+12, 1.70E+12, 1.75E+12,
353 | 1.80E+12, 1.84E+12, 1.89E+12, 1.94E+12, 1.99E+12, 2.04E+12,
354 | 2.08E+12, 2.13E+12, 2.18E+12, 2.23E+12, 2.27E+12, 2.32E+12,
355 | 2.37E+12, 2.42E+12, 2.45E+12, 2.51E+12, 2.56E+12, 2.61E+12,
356 | 2.66E+12, 2.70E+12, 2.75E+12, 2.80E+12, 2.84E+12, 2.89E+12,
357 | 2.94E+12, 2.99E+12, 3.03E+12, 3.27E+12, 3.41E+12, 3.55E+12,
358 | 3.70E+12, 3.83E+12, 3.97E+12, 4.11E+12, 4.26E+12, 4.40E+12,
359 | 4.54E+12, 4.68E+12, 4.82E+12, 4.96E+12, 5.10E+12, 5.24E+12,
360 | 5.38E+12, 5.53E+12, 5.66E+12, 5.80E+12, 5.95E+12, 6.08E+12,
361 | 6.23E+12, 6.36E+12, 6.51E+12, 6.65E+12, 6.79E+12, 6.93E+12,
362 | 7.07E+12, 7.21E+12, 7.35E+12, 7.49E+12, 7.64E+12, 7.78E+12,
363 | 7.92E+12, 8.05E+12]) * 10**(-16)
364 |
365 | ## Series B ##
366 |
367 | cs['B'] = np.array([1.605535, 1.607222, 1.608903, 1.610586, 1.612267,
368 | 1.613949, 1.61563, 1.617315, 1.617298, 1.619131, 1.62096,
369 | 1.622794, 1.624623, 1.626456, 1.628283, 1.630115, 1.631944,
370 | 1.633776, 1.635606, 1.637437, 1.639267, 1.641097, 1.642928,
371 | 1.644759, 1.646585, 1.64842, 1.650249, 1.652081, 1.653913,
372 | 1.655743])
373 |
374 | ds['B'] = np.array([1030304, 1037009, 1043861, 1050934, 1058154, 1065080,
375 | 1072005, 1079078, 1192615, 1193941, 1195415, 1196225,
376 | 1197404, 1198657, 1199983, 1201162, 1202341, 1203298,
377 | 1204551, 1205951, 1207130, 1208382, 1209561, 1210519,
378 | 1212066, 1212950, 1214129, 1215529, 1216708,
379 | 1217813]) * 10**(-8)
380 |
381 | es['B'] = np.array([8.19E+12, 8.34E+12, 8.48E+12, 8.62E+12, 8.76E+12,
382 | 8.90E+12, 9.04E+12, 9.18E+12, 7.66E+12, 7.82E+12, 7.99E+12,
383 | 8.15E+12, 8.32E+12, 8.48E+12, 8.64E+12, 8.80E+12, 8.97E+12,
384 | 9.13E+12, 9.29E+12, 9.45E+12, 9.61E+12, 9.78E+12, 9.95E+12,
385 | 1.01E+13, 1.03E+13, 1.04E+13, 1.06E+13, 1.08E+13, 1.09E+13,
386 | 1.11E+13]) * 10**(-16)
387 |
388 | ## Series E ##
389 |
390 | cs['E'] = np.array([1.47825, 1.482607, 1.486951, 1.491295, 1.495639,
391 | 1.499986, 1.504328, 1.508673, 1.51302, 1.517363, 1.521711,
392 | 1.526055, 1.531458, 1.538248, 1.545039, 1.549192, 1.553395,
393 | 1.5576302, 1.561807, 1.566011, 1.570218, 1.574422,
394 | 1.578628, 1.582834, 1.587038, 1.591241, 1.595445, 1.599651,
395 | 1.603857])
396 |
397 | ds['E'] = np.array([575420.1, 583524.6, 592071.2, 600470.4, 608648.6,
398 | 617121.5, 625299.6, 633698.9, 641950.8, 650350, 658601.8,
399 | 666780, 690062, 732500.2, 775085.7, 796083.8, 813471.6,
400 | 830859.5, 848321, 865782.5, 883391.4, 900852.9, 918314.4,
401 | 935849.7, 953090.2, 970551.7, 988234.2, 1005622,
402 | 1023157]) * 10**(-8)
403 |
404 | es['E'] = np.array([6.23E+12, 6.74E+12, 7.24E+12, 7.74E+12, 8.24E+12,
405 | 8.74E+12, 9.24E+12, 9.75E+12, 1.02E+13, 1.07E+13, 1.12E+13,
406 | 1.18E+13, 1.05E+13, 6.82E+12, 3.18E+12, 3.49E+12, 3.83E+12,
407 | 4.18E+12, 4.54E+12, 4.89E+12, 5.24E+12, 5.59E+12, 5.95E+12,
408 | 6.30E+12, 6.65E+12, 7.00E+12, 7.35E+12, 7.71E+12,
409 | 8.06E+12]) * 10**(-16)
410 |
411 | ## Acrylic-matching liquid, Code 5032
412 |
413 | cs['acrylic'] = np.array([1.478419])
414 |
415 | ds['acrylic'] = np.array([463182.1]) * 10**(-8)
416 |
417 | es['acrylic'] = np.array([-8.637338E+10]) * 10**(-16)
418 |
419 | try:
420 | n = cs[str(series)][i]+(ds[str(series)][i]/w.magnitude**2)
421 | + (es[str(series)][i]/w.magnitude**4)
422 | except IndexError:
423 | raise ValueError("""An oil with this cardinal number was not found.
424 | Check your cardinal number and make sure it is valid for the
425 | selected series """)
426 | except KeyError:
427 | raise ValueError("""An oil of this series was not found.
428 | Check your series and make sure it is valid. """)
429 |
430 | return Quantity(n)
431 |
432 | #------------------------------------------------------------------------------
433 | # EFFECTIVE INDEX CALCULATION
434 |
435 | def n_eff(n_particle, n_matrix, volume_fraction, maxwell_garnett=False):
436 | """
437 | Calculates Bruggeman effective refractive index for a composite of n
438 | dielectric media. If maxwell_garnett is set to true and there are two
439 | media, calculates the effective index using the Maxwell-Garnett
440 | formulation. Both Maxwell-Garnett and Bruggeman formulas can handle complex
441 | refractive indices.
442 |
443 | Parameters
444 | ----------
445 | n_particle: float or structcol.Quantity (dimensionless)
446 | refractive indices of the inclusion. If it's a core-shell particle,
447 | must be an array of indices from innermost to outermost layer.
448 | n_matrix: float or structcol.Quantity(dimensionless)
449 | refractive index of the matrix.
450 | volume_fraction: float or structcol.Quantity (dimensionless)
451 | volume fraction of inclusion. If it's a core-shell particle,
452 | must be an array of volume fractions from innermost to outermost layer.
453 | maxwell_garnett: boolean
454 | If true, the model uses Maxwell-Garnett's effective index for the
455 | sample. In that case, the user must specify one refractive index for
456 | the particle and one for the matrix. If false, the model uses
457 | Bruggeman's formula, which can be used for multilayer particles.
458 |
459 | Returns
460 | -------
461 | structcol.Quantity (dimensionless)
462 | refractive index
463 |
464 | References
465 | ----------
466 | [1] Markel, V. A. "Introduction to the Maxwell Garnett approximation:
467 | tutorial". Vol. 33, No. 7, Journal of the Optical Society of America A
468 | (2016).
469 | Bruggeman's equation in Eq. 29.
470 | Maxwell-Garnett relation in Eq. 18.
471 |
472 | """
473 | if isinstance(volume_fraction, Quantity):
474 | volume_fraction = volume_fraction.magnitude
475 |
476 | if maxwell_garnett:
477 | # check that the particle and matrix indices have the same length
478 | if (len(np.array([n_particle.magnitude]).flatten())
479 | != len(np.array([n_matrix.magnitude]).flatten())):
480 | raise ValueError('Maxwell-Garnett requires particle and '
481 | 'matrix index arrays to have the same length')
482 | ni = n_particle
483 | nm = n_matrix
484 | phi = volume_fraction
485 | neff = nm * np.sqrt((2*nm**2 + ni**2 + 2*phi*((ni**2)-(nm**2))) /
486 | (2*nm**2 + ni**2 - phi*((ni**2)-(nm**2))))
487 |
488 | if neff.imag == 0:
489 | return Quantity(neff.real)
490 | else:
491 | return Quantity(neff)
492 |
493 | else:
494 | # convert the particle index and volume fractions into 1D arrays
495 | n_particle = np.array([n_particle.magnitude]).flatten()
496 | volume_fraction = np.array([volume_fraction]).flatten()
497 |
498 | # check that the number of volume fractions and of indices is the same
499 | if len(n_particle) != len(volume_fraction):
500 | raise ValueError('Arrays of indices and volume fractions '
501 | 'must be the same length')
502 |
503 | volume_fraction_matrix = Quantity(1 - np.sum(volume_fraction), '')
504 |
505 | # create arrays combining the particle and the matrix' indices and vf
506 | n_particle_list = n_particle.tolist()
507 | n_particle_list.append(n_matrix.magnitude)
508 | n_array = np.array(n_particle_list)
509 |
510 | vf_list = volume_fraction.tolist()
511 | vf_list.append(volume_fraction_matrix.magnitude)
512 | vf_array = np.array(vf_list)
513 |
514 | # define a function for Bruggeman's equation
515 | def sum_bg(n_bg, vf, n_array):
516 | N = len(n_array.flatten())
517 | a, b = n_bg
518 | S = sum((vf[n]*(n_array[n]**2 - (a+b*1j)**2)
519 | /(n_array[n]**2 + 2*(a+b*1j)**2))
520 | for n in np.arange(0, N))
521 | return (S.real, S.imag)
522 |
523 | # set an initial guess and solve for Bruggeman's refractive index of
524 | # the composite
525 | # most refractive indices range between 1 and 3
526 | initial_guess = [1.5, 0]
527 | n_bg_real, n_bg_imag = fsolve(sum_bg, initial_guess, args=(vf_array,
528 | n_array))
529 |
530 | if n_bg_imag == 0:
531 | return Quantity(n_bg_real)
532 | elif n_bg_imag < 0:
533 | raise ValueError('Cannot find positive imaginary root for the '
534 | 'effective index')
535 | else:
536 | return Quantity(n_bg_real + n_bg_imag*1j)
537 |
--------------------------------------------------------------------------------
/structcol/structure.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Sofia Makgiriadou, Vinothan N. Manoharan, Victoria Hwang,
2 | # Annie Stephenson
3 | #
4 | # This file is part of the structural-color python package.
5 | #
6 | # This package is free software: you can redistribute it and/or modify it under
7 | # the terms of the GNU General Public License as published by the Free Software
8 | # Foundation, either version 3 of the License, or (at your option) any later
9 | # version.
10 | #
11 | # This package is distributed in the hope that it will be useful, but WITHOUT
12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14 | # details.
15 | #
16 | # You should have received a copy of the GNU General Public License along with
17 | # this package. If not, see .
18 | """
19 | Routines for simulating and calculating structures, structural parameters, and
20 | structure factors
21 |
22 | .. moduleauthor :: Sofia Magkiriadou
23 | .. moduleauthor :: Vinothan N. Manoharan
24 | .. moduleauthor :: Victoria Hwang
25 | .. moduleauthor :: Annie Stephenson
26 | """
27 |
28 | import numpy as np
29 | # unit registry and Quantity constructor from pint
30 | from . import ureg, Quantity
31 | import scipy as sp
32 | import scipy
33 | import os
34 | import structcol as sc
35 |
36 | @ureg.check('[]', '[]') # inputs should be dimensionless
37 | def factor_py(qd, phi):
38 | """
39 | Calculate structure factor of hard spheres using the Ornstein-Zernike
40 | equation and Percus-Yevick approximation [1]_ [2]_.
41 |
42 | Parameters:
43 | ----------
44 | qd: 1D numpy array
45 | dimensionless quantity that represents the frequency space value that
46 | the structure factor depends on
47 | phi: structcol.Quantity [dimensionless]
48 | volume fraction of particles or voids in matrix
49 |
50 | Returns:
51 | -------
52 | 1D numpy array:
53 | The structure factor as a function of qd.
54 |
55 | Notes
56 | -----
57 | Might not be accurate for volume fractions above 0.5 (need to check against
58 | some simulated structure factors).
59 |
60 | This code is fully vectorized, so you can feed it orthogonal vectors for
61 | both qd and phi and it will produce a 2D output:
62 | qd = np.arange(0.1, 20, 0.01)
63 | phi = np.array([0.15, 0.3, 0.45])
64 | s = structure.factor_py(qd.reshape(-1,1), phi.reshape(1,-1))
65 |
66 | References
67 | ----------
68 | [1] Boualem Hammouda, "Probing Nanoscale Structures -- The SANS Toolbox"
69 | http://www.ncnr.nist.gov/staff/hammouda/the_SANS_toolbox.pdf Chapter 32
70 | (http://www.ncnr.nist.gov/staff/hammouda/distance_learning/chapter_32.pdf)
71 |
72 | [2] Boualem Hammouda, "A Tutorial on Small-Angle Neutron Scattering from
73 | Polymers", http://www.ncnr.nist.gov/programs/sans/pdf/polymer_tut.pdf, pp.
74 | 51--53.
75 | """
76 |
77 | # constants in the direct correlation function
78 | lambda1 = (1 + 2*phi)**2 / (1 - phi)**4
79 | lambda2 = -(1 + phi/2.)**2 / (1 - phi)**4
80 | # Fourier transform of the direct correlation function (eq X.33 of [2]_)
81 | c = -24*phi*(lambda1 * (np.sin(qd) - qd*np.cos(qd)) / qd**3
82 | - 6*phi*lambda2 * (qd**2 * np.cos(qd) - 2*qd*np.sin(qd)
83 | - 2*np.cos(qd)+2.0) / qd**4
84 | - (phi*lambda1/2.) * (qd**4 * np.cos(qd)
85 | - 4*qd**3 * np.sin(qd)
86 | - 12 * qd**2 * np.cos(qd)
87 | + 24*qd * np.sin(qd)
88 | + 24 * np.cos(qd) - 24.0) / qd**6)
89 | # Structure factor at qd (eq X.34 of [2]_)
90 | return 1.0/(1-c)
91 |
92 |
93 | def factor_para(qd, phi, sigma=.15):
94 | """
95 | Calculate structure factor of a structure characterized by disorder of the
96 | second kind as defined in Guinier [1]. This type of structure is referred
97 | to as paracrystalline by Hoseman [2]. See also [3] for concise description.
98 |
99 | Parameters:
100 | ----------
101 | qd: 1D numpy array
102 | dimensionless quantity that represents the frequency space value that
103 | the structure factor depends on
104 | phi: structcol.Quantity [dimensionless]
105 | volume fraction of particles or voids in matrix
106 | sigma: float
107 | The standard deviation of a Gaussian representing the distribution of
108 | particle/void spacings in the structure. Sigma has implied units of
109 | particle diamter squared. A larger sigma will give more broad peaks,
110 | and a smaller sigma more sharp peaks.
111 |
112 | Returns:
113 | -------
114 | 1D numpy array:
115 | The structure factor as a function of qd.
116 |
117 | Notes
118 | -----
119 | This code is fully vectorized, so you can feed it orthogonal vectors for
120 | both qd and phi and it will produce a 2D output:
121 | qd = np.arange(0.1, 20, 0.01)
122 | phi = np.array([0.15, 0.3, 0.45])
123 | s = structure.factor_py(qd.reshape(-1,1), phi.reshape(1,-1))
124 |
125 | References
126 | ----------
127 | [1] Guinier, A (1963). X-Ray Diffraction. San Francisco and London: WH
128 | Freeman.
129 |
130 | [2] Lindenmeyer, PH; Hosemann, R (1963). "Application of the Theory of
131 | Paracrystals to the Crystal Structure Analysis of Polyacrylonitrile".
132 | J. Applied Physics. 34: 42
133 |
134 | [3] https://en.wikipedia.org/wiki/Structure_factor#Disorder_of_the_second_kind
135 | """
136 | r = np.exp(-(qd*phi**(-1/3)*sigma)**2/2)
137 | return (1-r**2)/(1+r**2-2*r*np.cos(qd*phi**(-1/3)))
138 |
139 |
140 | def factor_poly(q, phi, diameters, c, pdi):
141 | """
142 | Calculate polydisperse structure factor for a monospecies (one mean
143 | particle size) or a bispecies (two different mean particle sizes) system,
144 | each with its own polydispersity. The size distribution is assumed to be
145 | the Schulz distribution, which tends to Gaussian when the polydispersity
146 | goes to zero, and skews to larger sizes when the polydispersity becomes
147 | large.
148 |
149 | Parameters
150 | ----------
151 | qd: 1D numpy array
152 | dimensionless quantity that represents the frequency space value that
153 | the structure factor depends on
154 | phi: structcol.Quantity [dimensionless]
155 | volume fraction of all the particles or voids in matrix
156 | diameters: array of structcol.Quantity [length]
157 | mean diameters of each species of particles (can be one for a
158 | monospecies or two for bispecies).
159 | c: array of structcol.Quantity [dimensionless]
160 | 'number' concentration of each species. For ex, a system composed of 90
161 | A particles and 10 B particles would have c = [0.9, 0.1].
162 | pdi: array of float
163 | polydispersity index of each species.
164 |
165 | Returns
166 | -------
167 | 1D numpy array: The structure factor as a function of qd.
168 |
169 | References
170 | ----------
171 | M. Ginoza and M. Yasutomi, "Measurable Structure Factor of a Multi-Species
172 | Polydisperse Percus-Yevick Fluid with Schulz Distributed Diameters",
173 | J. Phys. Soc. Japan, 68, 7, 2292-2297 (1999).
174 | """
175 |
176 | def fm(x, t, tm, m):
177 | if isinstance(x, Quantity):
178 | x = x.to('').magnitude
179 | if isinstance(t, Quantity):
180 | t = t.to('').magnitude
181 | if isinstance(tm, Quantity):
182 | tm = tm.to('').magnitude
183 | t = np.reshape(t, (len(np.atleast_1d(t)),1))
184 | tm = np.reshape(tm, (len(tm),1))
185 | return (tm * (1 + x/(t+1))**(-(t+1+m)))
186 |
187 | def tm(m, t):
188 | t = np.reshape(t, (len(np.atleast_1d(t)),1))
189 | num_array = np.arange(m, 0, -1) + t
190 | prod = np.prod(num_array, axis=1).reshape((len(t), 1))
191 | return (prod / (t + 1)**m)
192 |
193 | # if the pdi is zero, assume it's very small (we get the same results)
194 | # because otherwise we get a divide by zero error
195 | # pdi = Quantity(np.atleast_1d(pdi).astype(float), pdi.units)
196 | pdi = np.atleast_1d(pdi).astype(float).magnitude
197 | pdi[pdi < 1e-5] = 1e-5
198 |
199 | Dsigma = pdi**2
200 | Delta = 1 - phi
201 | t = 1/Dsigma - 1
202 |
203 | t0 = tm(0, t)
204 | t1 = tm(1, t)
205 | # from eq. 24 of reference and simplifying
206 | t2 = Dsigma + 1
207 | # from eq. 24 and also on page 2295
208 | t3 = (Dsigma + 1) * (2*Dsigma + 1)
209 |
210 | # If monospecies, no need to calculate individual species parameters.
211 | # concentration c should always be a 2-element array because polydisperse
212 | # calculations assume the format of a bispecies particle mixture,
213 | # so if either element in c is 0, we assume the form factor is monospecies
214 | # We include the second monospecies test in case the user enters a 1d
215 | # concentration, even though the docstring advises that concentration
216 | # should have two elements.
217 | if np.any(c == 0) or (len(np.atleast_1d(c)) == 1):
218 | if len(np.atleast_1d(c)) == 1:
219 | t3_1d = t3
220 | diam_1d = diameters
221 | else:
222 | ind0 = np.where(c != 0)[0]
223 | t3_1d = t3[ind0]
224 | diam_1d = diameters[ind0]
225 | rho = 6*phi/(t3_1d*np.pi*diam_1d**3)
226 | else:
227 | phi_ratio = 1 / (c[0]/c[1] * (diameters[0] / diameters[1]) ** 3 *
228 | t3[0] / t3[1] + 1)
229 | phi_tau1 = phi_ratio * phi
230 | phi_tau0 = phi - phi_tau1
231 |
232 | rho_tau0 = 6*phi_tau0/(t3[0]*np.pi*diameters[0]**3)
233 | rho_tau1 = 6*phi_tau1/(t3[1]*np.pi*diameters[1]**3)
234 | rho = rho_tau0 + rho_tau1
235 |
236 | # this is the "effective" mean interparticle spacing
237 | sigma0 = (6*phi / (np.pi*rho))**(1/3)
238 |
239 | #q = qd / sigma0
240 |
241 | t2 = np.reshape(t2, (len(np.atleast_1d(t2)), 1))
242 | c = np.reshape(c, (len(np.atleast_1d(c)), 1))
243 | diameters = np.reshape(diameters, (len(np.atleast_1d(diameters)), 1))
244 |
245 | if hasattr(q, 'shape'):
246 | q_shape = q.shape
247 | else:
248 | q_shape = np.array([])
249 | if len(q_shape) == 2:
250 | q = Quantity(np.ndarray.flatten(q.magnitude), q.units) # added
251 | s = 1j*q
252 | x = s*diameters
253 | F0 = rho
254 | zeta2 = rho * sigma0**2
255 |
256 | f0 = fm(x,t,t0,0)
257 | f1 = fm(x,t,t1,1)
258 | f2 = fm(x,t,t2,2)
259 | f0_inv = fm(-x,t,t0,0)
260 | f1_inv = fm(-x,t,t1,1)
261 | f2_inv = fm(-x,t,t2,2)
262 |
263 | # from eqs 29a-29d
264 | fa = 1/x**3 * (1 - x/2 - f0 - x/2 * f1)
265 | fb = 1/x**3 * (1 - x/2 * t2 - f1 - x/2 * f2)
266 | fc = 1/x**2 * (1 - x - f0)
267 | fd = 1/x**2 * (1 - x*t2 - f1)
268 |
269 | Ialpha1 = 24/s**3 * np.sum(c * F0 * (-1/2*(1-f0) + x/4 * (1 + f1)), axis=0)
270 | Ialpha2 = 24/s**3 * np.sum(c * F0 * (-diameters/2 * (1-f1) +
271 | s*diameters**2/4 * (t2 + f2)), axis=0)
272 |
273 | Iw1 = 2*np.pi*rho/(Delta*s**3) * (Ialpha1 + s/2*Ialpha2)
274 | Iw2 = (np.pi*rho/(Delta*s**2) * (1 + np.pi*zeta2/(Delta*s))*Ialpha1 +
275 | np.pi**2*zeta2*rho/(2*Delta**2*s**2) * Ialpha2)
276 |
277 | F11 = np.sum(c*2*np.pi*rho*diameters**3/Delta * fa, axis=0)
278 | F12 = np.sum(c/diameters * ((np.pi/Delta)**2 * rho * zeta2
279 | * diameters**4*fa
280 | + np.pi*rho*diameters**3/Delta * fc), axis=0)
281 | F21 = np.sum(c * diameters * 2*np.pi*rho*diameters**3/Delta * fb, axis=0)
282 | F22 = np.sum(c * ((np.pi/Delta)**2 *rho*zeta2*diameters**4*fb +
283 | np.pi*rho*diameters**3/Delta * fd), axis=0)
284 |
285 | FF11 = 1 - F11
286 | FF12 = -F12
287 | FF21 = -F21
288 | FF22 = 1 - F22
289 |
290 | G11 = FF22 / (FF11 * FF22 - FF12 * FF21)
291 | G12 = -FF12 / (FF11 * FF22 - FF12 * FF21)
292 | G21 = -FF21 / (FF11 * FF22 - FF12 * FF21)
293 | G22 = FF11 / (FF11 * FF22 - FF12 * FF21)
294 |
295 | I0 = -9/2*(2/s)**6 * np.sum(c * F0**2 * (1-1/2*(f0_inv + f0) +
296 | x/2 *(f1_inv - f1) -
297 | (s**2*diameters**2)/8 * (f2_inv + f2 + 2*t2)),
298 | axis=0)
299 |
300 | term1 = Iw1 * G11 * Ialpha1 / I0
301 | term2 = Iw1 * G12 * Ialpha2 / I0
302 | term3 = Iw2 * G21 * Ialpha1 / I0
303 | term4 = Iw2 * G22 * Ialpha2 / I0
304 |
305 | h2 = (term1 + term2 + term3 + term4).real
306 |
307 | SM = 1 - 2*h2
308 | SM[SM<0] = 0
309 | if len(q_shape)==2:
310 | SM = np.reshape(SM,q_shape)
311 | return(SM)
312 |
313 | def factor_data(qd, s_data, qd_data):
314 | """
315 | Calculate an interpolated structure factor using data
316 |
317 | Parameters:
318 | ----------
319 | qd: 1D numpy array
320 | dimensionless quantity that represents the frequency space value that
321 | the structure factor depends on
322 | s_data: 1D numpy array
323 | structure factor values from data
324 | qd_data: 1D numpy array
325 | qd values from data
326 |
327 | Returns:
328 | -------
329 | 1D numpy array:
330 | The structure factor as a function of qd.
331 | """
332 | s_func = sp.interpolate.interp1d(qd_data, s_data, kind = 'linear',
333 | bounds_error=False, fill_value=s_data[0])
334 |
335 | return s_func(qd)
336 |
337 | def field_phase_data(qd, filename='spf.dat'):
338 | s_file = os.path.join(os.getcwd(),filename)
339 | s_phase_data=np.loadtxt(s_file)
340 | qd_data = s_phase_data[:,0]
341 | s_phase = s_phase_data[:,1]
342 | s_phase_func = sp.interpolate.interp1d(qd_data, s_phase, kind = 'linear',
343 | bounds_error=False,
344 | fill_value=s_phase_data[0,1])
345 | return s_phase_func(qd)
346 |
347 | def phase_factor(qd, phi, n=1000):
348 | # define r/d
349 | r_d = np.linspace(0,10, n)
350 |
351 | # calculate g
352 | g = radial_dist_py(phi, x = r_d)
353 | integral = np.zeros(qd.shape)
354 | rho = 3.0 * phi / (4.0 * np.pi) # dimensionless rho*sigma**3
355 |
356 | # calculate the integral for each qd
357 | for i in range(qd.shape[0]):
358 | for j in range(qd.shape[1]):
359 | bessel = rho*4*np.pi*r_d**2*np.pi*scipy.special.jv(0, qd[i,j]*r_d)
360 | integral[i,j] = np.trapz(bessel*g, x=r_d)
361 |
362 | return integral
363 |
364 |
365 | def field_phase_py(qd, phi, n=10000, r_d=np.arange(1,5,0.005), rng=None):
366 | '''
367 | Calculate the phase shift contribution based on the radial distribution
368 | function calculated using the Percus-Yevick approximation
369 |
370 | Parameters:
371 | ----------
372 | qd: 1D numpy array
373 | dimensionless quantity q times diameter
374 | phi: structcol.Quantity [dimensionless]
375 | volume fraction of particles or voids in matrix
376 | n: float
377 | number of samples of g(r)
378 | r_d: 1D numpy array
379 | range of radial positions normalized by particle diameter.
380 | rng: numpy.random.Generator object (default None) random number generator.
381 | If not specified, use the default generator initialized on loading the
382 | package
383 |
384 | Returns:
385 | --------
386 | field_s: 1D numpy array
387 | phase shift contributions based on the structure
388 | '''
389 | if rng is None:
390 | rng = sc.rng
391 |
392 | # calculate radial distribution function up to r/R= 5
393 | #g_file = os.path.join(os.getcwd(),'g_4.csv')
394 | #df=pd.read_csv(g_file, sep=',',header=None)
395 | #r_d = np.array(df[0])
396 | #g = np.array(df[1])
397 | g = radial_dist_py(phi, x = r_d)
398 |
399 | # sample the g of r probability distribution
400 | r_samp = rng.choice(r_d, n, p = g/np.sum(g))
401 |
402 | # calculate the field term
403 | field_s = np.zeros(qd.shape, dtype='complex')
404 | for i in range(qd.shape[0]):
405 | for j in range(qd.shape[1]):
406 | field_s[i,j] = 1/n*np.sum(np.exp(1j*qd[i,j]*r_samp))
407 |
408 | return field_s
409 |
410 | def radial_dist_py(phi, x=np.arange(1,5,0.005)):
411 | '''
412 | Calculate the radial distribution function for hard spheres using the
413 | Percus-Yevick approximation.
414 |
415 | This function and its helper functions is based on the code found here:
416 | https://github.com/FTurci/hard-spheres-utilities/blob/master/Percus-Yevick.py
417 | This method for calculating g(r) is described in the SI of:
418 | J. W. E. Drewitt, F. Turci, B. J. Heinen, S. G. Macleod, F. Qin, A. K.
419 | Kleppe, and O. T. Lord. Phys. Rev. Lett. 124
420 |
421 | Parameters:
422 | -----------
423 | phi: structcol.Quantity [dimensionless]
424 | volume fraction of particles or voids in matrix
425 | x: 1D numpy array
426 | dimensionless value defined as position over particle diameter (r/d)
427 |
428 | Returns:
429 | --------
430 | g_fcn(x): 1D numpy array
431 | The radial distribution function calculated at the specified x
432 | values.
433 | '''
434 | # number density
435 | if isinstance(phi,Quantity):
436 | phi = phi.magnitude
437 | rho=6./np.pi*phi
438 |
439 | # get the direct correlation function c(r) from the analytic Percus-Yevick
440 | # solution, vectorizing the function
441 | c=np.vectorize(cc)
442 |
443 | # space discretization
444 | dr=0.005
445 | r=np.arange(1,1024*2+1,1 )*dr
446 |
447 | # reciprocal space discretization (highest available frequency)
448 | dk=1/r[-1]
449 | k=np.arange(1,1024*2+1,1 )*dk
450 |
451 | # direct correlation function c(r)
452 | c_direct=c(r,phi)
453 |
454 | # calculate the Fourier transform
455 | ft_c_direct=spherical_FT(c_direct, k,r,dr)
456 |
457 | # using the Ornstein-Zernike equation, calculate the structure factor
458 | ft_h=ft_c_direct/(1.-rho*ft_c_direct)
459 |
460 | # inverse Fourier transform
461 | h=inverse_spherical_FT(ft_h, k,r,dk)
462 |
463 | # radial distribution function
464 | gg=h+1
465 |
466 | # clean the r<1 region
467 | g=np.zeros(len(gg))
468 | g[r>=1]=gg[r>=1]
469 |
470 | # make g function from interpolation
471 | g_fcn=sp.interpolate.InterpolatedUnivariateSpline(r, g)
472 |
473 | return g_fcn(x)
474 |
475 | def spherical_FT(f,k,r,dr):
476 | '''
477 | Spherical Fourier Transform (using the liquid isotropicity)
478 | '''
479 | ft=np.zeros(len(k))
480 | for i in range(len(k)):
481 | ft[i]=4.*np.pi*np.sum(r*np.sin(k[i]*r)*f*dr)/k[i]
482 | return ft
483 |
484 | def inverse_spherical_FT(ff,k,r,dk):
485 | '''
486 | Inverse spherical Fourier Transform (using the liquid isotropicity)
487 | '''
488 | ift=np.zeros(len(r))
489 | for i in range(len(r)):
490 | ift[i]=np.sum(k*np.sin(k*r[i])*ff*dk)/r[i]/(2*np.pi**2)
491 | return ift
492 |
493 | # functions to calcualte direct correlation function
494 | # from Percus-Yevick. See D. Henderson "Condensed Matter Physics" 2009, Vol.
495 | # 12, No. 2, pp. 127-135
496 | # or M. S Wertheim "Exact Solutions of the Percus-Yevick Integral for Hard
497 | # Spheres" PRL. Vol. 10, No. 8, 1963
498 | def c0(eta):
499 | return -(1.+2.*eta)**2/(1.-eta)**4
500 | def c1(eta):
501 | return 6.*eta*(1.+eta*0.5)**2/(1.-eta)**4
502 | def c3(eta):
503 | return eta*0.5*c0(eta)
504 | def cc(r,eta):
505 | if r>1:
506 | return 0
507 | else:
508 | return c0(eta)+c1(eta)*r +c3(eta)*r**3
509 |
--------------------------------------------------------------------------------
/structcol/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan
2 | #
3 | # This file is part of the structural-color python package.
4 | #
5 | # This package is free software: you can redistribute it and/or modify it under
6 | # the terms of the GNU General Public License as published by the Free Software
7 | # Foundation, either version 3 of the License, or (at your option) any later
8 | # version.
9 | #
10 | # This package is distributed in the hope that it will be useful, but WITHOUT
11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 | # details.
14 | #
15 | # You should have received a copy of the GNU General Public License along with
16 | # this package. If not, see .
17 |
18 |
19 |
--------------------------------------------------------------------------------
/structcol/tests/test_detector.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan, Victoria Hwang, Solomon Barkley,
2 | # Annie Stephenson
3 | #
4 | # This file is part of the structural-color python package.
5 | #
6 | # This package is free software: you can redistribute it and/or modify it under
7 | # the terms of the GNU General Public License as published by the Free Software
8 | # Foundation, either version 3 of the License, or (at your option) any later
9 | # version.
10 | #
11 | # This package is distributed in the hope that it will be useful, but WITHOUT
12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14 | # details.
15 | #
16 | # You should have received a copy of the GNU General Public License along with
17 | # this package. If not, see .
18 | """
19 | Tests for the montecarlo model (in structcol/montecarlo.py)
20 |
21 | .. moduleauthor:: Victoria Hwang
22 | .. moduleauthor:: Vinothan N. Manoharan
23 | .. moduleauthor:: Solomon Barkley
24 | .. moduleauthor:: Annie Stephenson
25 |
26 | """
27 |
28 | import structcol as sc
29 | from .. import montecarlo as mc
30 | from .. import detector as det
31 | from .. import refractive_index as ri
32 | import numpy as np
33 | import warnings
34 | from numpy.testing import assert_equal, assert_almost_equal
35 | import pytest
36 |
37 | # Define a system to be used for the tests
38 | nevents = 3
39 | ntrajectories = 4
40 | radius = sc.Quantity('150.0 nm')
41 | volume_fraction = 0.5
42 | n_particle = sc.Quantity(1.5, '')
43 | n_matrix = sc.Quantity(1.0, '')
44 | n_medium = sc.Quantity(1.0, '')
45 | n_sample = ri.n_eff(n_particle, n_matrix, volume_fraction)
46 | angles = sc.Quantity(np.linspace(0.01,np.pi, 200), 'rad')
47 | wavelen = sc.Quantity('400.0 nm')
48 |
49 | # Index of the scattering event and trajectory corresponding to the reflected
50 | # photons
51 | refl_index = np.array([2,0,2])
52 |
53 | def test_calc_refl_trans():
54 | high_thresh = 10
55 | small_n = 1
56 | large_n = 2
57 |
58 | # test absoprtion and stuck without fresnel
59 | z_pos = np.array([[0,0,0,0],[1,1,1,1],[-1,11,2,11],[-2,12,4,12]])
60 | ntrajectories = z_pos.shape[1]
61 | kz = np.array([[1,1,1,1],[-1,1,1,1],[-1,1,1,1]])
62 | weights = np.array([[.8, .8, .9, .8],[.7, .3, .7, 0],[.1, .1, .5, 0]])
63 | trajectories = mc.Trajectory([np.nan, np.nan, z_pos],[np.nan, np.nan, kz], weights)
64 | # Should raise warning that n_matrix and n_particle are not set, so
65 | # tir correction is based only on sample index
66 | with pytest.warns(UserWarning):
67 | refl, trans= det.calc_refl_trans(trajectories, high_thresh, small_n,
68 | small_n, 'film')
69 | expected_trans_array = np.array([0, .3, .25, 0])/ntrajectories #calculated manually
70 | expected_refl_array = np.array([.7, 0, .25, 0])/ntrajectories #calculated manually
71 | assert_almost_equal(refl, np.sum(expected_refl_array))
72 | assert_almost_equal(trans, np.sum(expected_trans_array))
73 |
74 | # test above but with covers on front and back
75 | # (should raise warning that n_matrix and n_particle are not set, so
76 | # tir correction is based only on sample index)
77 | with pytest.warns(UserWarning):
78 | refl, trans = det.calc_refl_trans(trajectories, high_thresh, small_n,
79 | small_n, 'film',n_front=large_n,
80 | n_back=large_n)
81 | expected_trans_array = np.array([0.00814545, 0.20014545, 0.2, 0.])/ntrajectories #calculated manually
82 | expected_refl_array = np.array([0.66700606, 0.20349091, 0.4, 0.2])/ntrajectories #calculated manually
83 | assert_almost_equal(refl, np.sum(expected_refl_array))
84 | assert_almost_equal(trans, np.sum(expected_trans_array))
85 |
86 | # test fresnel as well
87 | z_pos = np.array([[0,0,0,0],[5,5,5,5],[-5,-5,15,15],[5,-15,5,25],[-5,-25,6,35]])
88 | ntrajectories = z_pos.shape[1]
89 | kz = np.array([[1,1,1,0.86746757864487367],[-.1,-.1,.1,.1],[0.1,-.1,-.1,0.1],[-1,-.9,1,1]])
90 | weights = np.array([[.8, .8, .9, .8],[.7, .3, .7, .5],[.6, .2, .6, .4], [.4, .1, .5, .3]])
91 | trajectories = mc.Trajectory([np.nan, np.nan, z_pos],[np.nan, np.nan, kz], weights)
92 |
93 | # Should raise warning that n_matrix and n_particle are not set, so
94 | # tir correction is based only on sample index
95 | with pytest.warns(UserWarning):
96 | refl, trans= det.calc_refl_trans(trajectories, high_thresh, small_n,
97 | large_n, 'film')
98 | expected_trans_array = np.array([ .00167588, .00062052, .22222222, .11075425])/ntrajectories #calculated manually
99 | expected_refl_array = np.array([ .43317894, .18760061, .33333333, .59300905])/ntrajectories #calculated manually
100 | assert_almost_equal(refl, np.sum(expected_refl_array))
101 | assert_almost_equal(trans, np.sum(expected_trans_array))
102 |
103 | # test refraction and detection_angle
104 | # (should raise warning that n_matrix and n_particle are not set, so
105 | # tir correction is based only on sample index)
106 | with pytest.warns(UserWarning):
107 | refl, trans= det.calc_refl_trans(trajectories, high_thresh, small_n,
108 | large_n, 'film', detection_angle=0.1)
109 | expected_trans_array = np.array([ .00167588, .00062052, .22222222, .11075425])/ntrajectories #calculated manually
110 | expected_refl_array = np.array([ .43203386, .11291556, .29105299, .00046666])/ntrajectories #calculated manually
111 | assert_almost_equal(refl, np.sum(expected_refl_array))
112 | assert_almost_equal(trans, np.sum(expected_trans_array))
113 |
114 | # test steps in z longer than sample thickness
115 | z_pos = np.array([[0,0,0,0,0,0,0],[1.1,2.1,3.1,0.6,0.6,0.6,0.1],[1.2,2.2,3.2,1.6,0.7,0.7,-0.6],[1.3,2.3,3.3,3.3,-2.1,-1.1,-2.1]])
116 | ntrajectories = z_pos.shape[1]
117 | kz = np.array([[1,1,1,1,1,1,1],[1,1,1,0.1,1,1,-0.1],[1,1,1,1,-1,-1,-1]])
118 | weights = np.array([[1,1,1,1,1,1,1],[1,1,1,1,1,1,1],[1,1,1,1,1,1,1]])
119 | thin_sample_thickness = 1
120 | trajectories = mc.Trajectory([np.nan, np.nan, z_pos],[np.nan, np.nan, kz], weights)
121 | # Should raise warning that n_matrix and n_particle are not set, so
122 | # tir correction is based only on sample index
123 | with pytest.warns(UserWarning):
124 | refl, trans= det.calc_refl_trans(trajectories, thin_sample_thickness,
125 | small_n, large_n, 'film')
126 | expected_trans_array = np.array([.8324515, .8324515, .8324515, .05643739, .05643739, .05643739, .8324515])/ntrajectories #calculated manually
127 | expected_refl_array = np.array([.1675485, .1675485, .1675485, .94356261, .94356261, .94356261, .1675485])/ntrajectories #calculated manually
128 | assert_almost_equal(refl, np.sum(expected_refl_array))
129 | assert_almost_equal(trans, np.sum(expected_trans_array))
130 |
131 | def test_reflection_core_shell():
132 | # test that the reflection of a non-core-shell system is the same as that
133 | # of a core-shell with a shell index matched with the core
134 | seed = 1
135 | nevents = 60
136 | ntrajectories = 30
137 |
138 | # Reflection using a non-core-shell system
139 | warnings.filterwarnings("ignore", category=UserWarning) # ignore the "not enough events" warning
140 | R, T = calc_montecarlo(nevents, ntrajectories, radius, n_particle,
141 | n_sample, n_medium, volume_fraction, wavelen, seed)
142 |
143 | # Reflection using core-shells with the shell index-matched to the core
144 | radius_cs = sc.Quantity(np.array([100.0, 150.0]), 'nm') # specify the radii from innermost to outermost layer
145 | n_particle_cs = sc.Quantity(np.array([1.5,1.5]), '') # specify the index from innermost to outermost layer
146 |
147 | # calculate the volume fractions of each layer
148 | vf_array = np.empty(len(radius_cs))
149 | r_array = np.array([0] + radius_cs.magnitude.tolist())
150 | for r in np.arange(len(r_array)-1):
151 | vf_array[r] = (r_array[r+1]**3-r_array[r]**3) / (r_array[-1]**3) * volume_fraction
152 |
153 | n_sample_cs = ri.n_eff(n_particle_cs, n_matrix, vf_array)
154 | R_cs, T_cs = calc_montecarlo(nevents, ntrajectories, radius_cs,
155 | n_particle_cs, n_sample_cs, n_medium,
156 | volume_fraction, wavelen, seed)
157 |
158 | assert_almost_equal(R, R_cs)
159 | assert_almost_equal(T, T_cs)
160 |
161 | # Expected outputs, consistent with results expected from before refactoring
162 | R_before = 0.7862152377246211 #before correcting nevents in sample_angles:: 0.81382378303119451
163 | R_cs_before = 0.7862152377246211 #before correcting nevents in sample_angles: 0.81382378303119451
164 | T_before = 0.21378476227537888 #before correcting nevents in sample_angles: 0.1861762169688054
165 | T_cs_before = 0.21378476227537888 #before correcting nevents in sample_angles: 0.1861762169688054
166 |
167 | assert_almost_equal(R_before, R)
168 | assert_almost_equal(R_cs_before, R_cs)
169 | assert_almost_equal(T_before, T)
170 | assert_almost_equal(T_cs_before, T_cs)
171 |
172 | # Test that the reflectance is the same for a core-shell that absorbs (with
173 | # the same refractive indices for all layers) and a non-core-shell that
174 | # absorbs with the same index
175 | # Reflection using a non-core-shell absorbing system
176 | n_particle_abs = sc.Quantity(1.5+0.001j, '')
177 | n_sample_abs = ri.n_eff(n_particle_abs, n_matrix, volume_fraction)
178 |
179 | R_abs, T_abs = calc_montecarlo(nevents, ntrajectories, radius,
180 | n_particle_abs, n_sample_abs, n_medium,
181 | volume_fraction, wavelen, seed)
182 |
183 | # Reflection using core-shells with the shell index-matched to the core
184 | n_particle_cs_abs = sc.Quantity(np.array([1.5+0.001j,1.5+0.001j]), '')
185 | n_sample_cs_abs = ri.n_eff(n_particle_cs_abs, n_matrix, vf_array)
186 |
187 | R_cs_abs, T_cs_abs = calc_montecarlo(nevents, ntrajectories, radius_cs,
188 | n_particle_cs_abs, n_sample_cs_abs,
189 | n_medium, volume_fraction, wavelen,
190 | seed)
191 |
192 | assert_almost_equal(R_abs, R_cs_abs, decimal=6)
193 | assert_almost_equal(T_abs, T_cs_abs, decimal=6)
194 |
195 | # Expected outputs, consistent with results expected from before refactoring
196 | #
197 | # (note: values below may be off at the 10th decimal place as of scipy
198 | # 1.15.0; since the goal is to ensure that we get the same results for
199 | # homogeneous spheres as for index-matched core-shell spheres, this
200 | # difference should not concern us too much)
201 | R_abs_before = 0.3079106226852705 #before correcting nevents in sample_angles: 0.3956821177047554
202 | R_cs_abs_before = 0.3079106226846794 #before correcting nevents in sample_angles: 0.39568211770416667
203 | T_abs_before = 0.02335228504958959 #before correcting nevents in sample_angles: 0.009944245822685388
204 | T_cs_abs_before = 0.023352285049450985 #before correcting nevents in sample_angles: 0.009944245822595715
205 |
206 | assert_almost_equal(R_abs_before, R_abs, decimal=10)
207 | assert_almost_equal(R_cs_abs_before, R_cs_abs, decimal=10)
208 | assert_almost_equal(T_abs_before, T_abs, decimal=10)
209 | assert_almost_equal(T_cs_abs_before, T_cs_abs, decimal=10)
210 |
211 | # Same as previous test but with absorbing matrix as well
212 | # Reflection using a non-core-shell absorbing system
213 | n_particle_abs = sc.Quantity(1.5+0.001j, '')
214 | n_matrix_abs = sc.Quantity(1.+0.001j, '')
215 | n_sample_abs = ri.n_eff(n_particle_abs, n_matrix_abs, volume_fraction)
216 |
217 | R_abs, T_abs = calc_montecarlo(nevents, ntrajectories, radius,
218 | n_particle_abs, n_sample_abs, n_medium,
219 | volume_fraction, wavelen, seed)
220 |
221 | # Reflection using core-shells with the shell index-matched to the core
222 | n_particle_cs_abs = sc.Quantity(np.array([1.5+0.001j,1.5+0.001j]), '')
223 | n_sample_cs_abs = ri.n_eff(n_particle_cs_abs, n_matrix_abs, vf_array)
224 |
225 | R_cs_abs, T_cs_abs = calc_montecarlo(nevents, ntrajectories, radius_cs,
226 | n_particle_cs_abs, n_sample_cs_abs, n_medium,
227 | volume_fraction, wavelen, seed)
228 |
229 | assert_almost_equal(R_abs, R_cs_abs, decimal=6)
230 | assert_almost_equal(T_abs, T_cs_abs, decimal=6)
231 |
232 | # Expected outputs, consistent with results expected from before refactoring
233 | R_abs_before = 0.19121902522926137 #before correcting nevents in sample_angles: 0.27087005070007175
234 | R_cs_abs_before = 0.19121902522926137 #before correcting nevents in sample_angles: 0.27087005070007175
235 | T_abs_before = 0.0038425936376528256 #before correcting nevents in sample_angles: 0.0006391960305096798
236 | T_cs_abs_before = 0.0038425936376528256 #before correcting nevents in sample_angles: 0.0006391960305096798
237 |
238 | assert_almost_equal(R_abs_before, R_abs)
239 | assert_almost_equal(R_cs_abs_before, R_cs_abs)
240 | assert_almost_equal(T_abs_before, T_abs)
241 | assert_almost_equal(T_cs_abs_before, T_cs_abs)
242 |
243 |
244 | def test_reflection_absorbing_particle_or_matrix():
245 | # test that the reflections with a real n_particle and with a complex
246 | # n_particle with a 0 imaginary component are the same
247 | seed = 1
248 | nevents = 60
249 | ntrajectories = 30
250 |
251 | # Reflection using non-absorbing particle
252 | warnings.filterwarnings("ignore", category=UserWarning) # ignore the "not enough events" warning
253 | R, T = calc_montecarlo(nevents, ntrajectories, radius, n_particle,
254 | n_sample, n_medium, volume_fraction, wavelen, seed)
255 |
256 | # Reflection using particle with an imaginary component of 0
257 | n_particle_abs = sc.Quantity(1.5 + 0j, '')
258 | R_abs, T_abs = calc_montecarlo(nevents, ntrajectories, radius,
259 | n_particle_abs, n_sample, n_medium,
260 | volume_fraction, wavelen, seed)
261 |
262 | assert_equal(R, R_abs)
263 | assert_equal(T, T_abs)
264 |
265 | # Expected outputs, consistent with results expected from before refactoring
266 | R_before = 0.7862152377246211#before correcting nevents in sample_angles: 0.81382378303119451
267 | R_abs_before = 0.7862152377246211#before correcting nevents in sample_angles: 0.81382378303119451
268 | T_before = 0.21378476227537888#before correcting nevents in sample_angles: 0.1861762169688054
269 | T_abs_before = 0.21378476227537888#before correcting nevents in sample_angles: 0.1861762169688054
270 |
271 | assert_almost_equal(R_before, R)
272 | assert_almost_equal(R_abs_before, R_abs)
273 | assert_almost_equal(T_before, T)
274 | assert_almost_equal(T_abs_before, T_abs)
275 |
276 | # Same as previous test but with absorbing matrix
277 | # Reflection using matrix with an imaginary component of 0
278 | n_matrix_abs = sc.Quantity(1. + 0j, '')
279 | n_sample_abs = ri.n_eff(n_particle, n_matrix_abs, volume_fraction)
280 | R_abs, T_abs = calc_montecarlo(nevents, ntrajectories, radius,
281 | n_particle, n_sample_abs, n_medium,
282 | volume_fraction, wavelen, seed)
283 |
284 | assert_equal(R, R_abs)
285 | assert_equal(T, T_abs)
286 |
287 | # Expected outputs, consistent with results expected from before refactoring
288 | R_before = 0.7862152377246211 #before correcting nevents in sample_angles: 0.81382378303119451
289 | R_abs_before = 0.7862152377246211 #before correcting nevents in sample_angles: 0.81382378303119451
290 | T_before = 0.21378476227537888 #before correcting nevents in sample_angles: 0.1861762169688054
291 | T_abs_before = 0.21378476227537888#before correcting nevents in sample_angles: 0.1861762169688054
292 |
293 | assert_almost_equal(R_before, R)
294 | assert_almost_equal(R_abs_before, R_abs)
295 | assert_almost_equal(T_before, T)
296 | assert_almost_equal(T_abs_before, T_abs)
297 |
298 | # test that the reflection is essentially the same when the imaginary
299 | # index is 0 or very close to 0
300 | n_matrix_abs = sc.Quantity(1. + 1e-10j, '')
301 | n_sample_abs = ri.n_eff(n_particle, n_matrix_abs, volume_fraction)
302 | R_abs, T_abs = calc_montecarlo(nevents, ntrajectories, radius,
303 | n_particle, n_sample_abs, n_medium,
304 | volume_fraction, wavelen, seed)
305 | assert_almost_equal(R, R_abs, decimal=6)
306 | assert_almost_equal(T, T_abs, decimal=6)
307 |
308 | def test_reflection_polydispersity():
309 | seed = 1
310 | nevents = 60
311 | ntrajectories = 30
312 |
313 | radius2 = radius
314 | concentration = sc.Quantity(np.array([0.9,0.1]), '')
315 | pdi = sc.Quantity(np.array([1e-7,1e-7]), '') # monodisperse limit
316 |
317 | # Without absorption: test that the reflectance using very small
318 | # polydispersity is the same as the monodisperse case
319 | warnings.filterwarnings("ignore", category=UserWarning) # ignore the "not enough events" warning
320 | R_mono, T_mono = calc_montecarlo(nevents, ntrajectories, radius,
321 | n_particle, n_sample, n_medium,
322 | volume_fraction, wavelen, seed,
323 | polydisperse=False)
324 | R_poly, T_poly = calc_montecarlo(nevents, ntrajectories, radius,
325 | n_particle, n_sample, n_medium,
326 | volume_fraction, wavelen, seed,
327 | radius2 = radius2,
328 | concentration = concentration, pdi = pdi,
329 | polydisperse=True)
330 | assert_almost_equal(R_mono, R_poly)
331 | assert_almost_equal(T_mono, T_poly)
332 |
333 | # Outputs before refactoring structcol
334 | R_mono_before = 0.7862152377246211 #before correcting nevents in sample_angles: 0.81382378303119451
335 | R_poly_before = 0.7862152377246211 #before correcting nevents in sample_angles: 0.81382378303119451
336 | T_mono_before = 0.21378476227537888 #before correcting nevents in sample_angles: 0.1861762169688054
337 | T_poly_before = 0.21378476227537888 #before correcting nevents in sample_angles: 0.1861762169688054
338 |
339 | assert_almost_equal(R_mono_before, R_mono)
340 | assert_almost_equal(R_poly_before, R_poly)
341 | assert_almost_equal(T_mono_before, T_mono)
342 | assert_almost_equal(T_poly_before, T_poly)
343 |
344 | # With absorption: test that the reflectance using with very small
345 | # polydispersity is the same as the monodisperse case
346 | n_particle_abs = sc.Quantity(1.5+0.0001j, '')
347 | n_matrix_abs = sc.Quantity(1.+0.0001j, '')
348 | n_sample_abs = ri.n_eff(n_particle_abs, n_matrix_abs, volume_fraction)
349 |
350 | R_mono_abs, T_mono_abs = calc_montecarlo(nevents, ntrajectories, radius,
351 | n_particle_abs, n_sample_abs,
352 | n_medium, volume_fraction, wavelen,
353 | seed, polydisperse=False)
354 | R_poly_abs, T_poly_abs = calc_montecarlo(nevents, ntrajectories, radius,
355 | n_particle_abs, n_sample_abs,
356 | n_medium, volume_fraction, wavelen,
357 | seed, radius2 = radius2,
358 | concentration = concentration,
359 | pdi = pdi, polydisperse=True)
360 |
361 | assert_almost_equal(R_mono_abs, R_poly_abs, decimal=6)
362 | assert_almost_equal(T_mono_abs, T_poly_abs, decimal=6)
363 |
364 | # Outputs before refactoring structcol
365 | R_mono_abs_before = 0.5861304578863337 #before correcting nevents in sample_angles: 0.6480185516058052
366 | R_poly_abs_before = 0.5861304624420246 #before correcting nevents in sample_angles: 0.6476683654364985
367 | T_mono_abs_before = 0.11704096147886706 #before correcting nevents in sample_angles: 0.09473841417422774
368 | T_poly_abs_before = 0.11704096346317548 #before correcting nevents in sample_angles: 0.09456832138047852
369 |
370 | assert_almost_equal(R_mono_abs_before, R_mono_abs)
371 | assert_almost_equal(R_poly_abs_before, R_poly_abs)
372 | assert_almost_equal(T_mono_abs_before, T_mono_abs)
373 | assert_almost_equal(T_poly_abs_before, T_poly_abs)
374 |
375 | # test that the reflectance is the same for a polydisperse monospecies
376 | # and a bispecies with equal types of particles
377 | concentration_mono = sc.Quantity(np.array([0.,1.]), '')
378 | concentration_bi = sc.Quantity(np.array([0.3,0.7]), '')
379 | pdi2 = sc.Quantity(np.array([1e-1, 1e-1]), '')
380 |
381 | R_mono2, T_mono2 = calc_montecarlo(nevents, ntrajectories, radius,
382 | n_particle, n_sample, n_medium,
383 | volume_fraction, wavelen, seed,
384 | radius2 = radius2,
385 | concentration = concentration_mono, pdi = pdi2,
386 | polydisperse=True)
387 | R_bi, T_bi = calc_montecarlo(nevents, ntrajectories, radius,
388 | n_particle, n_sample, n_medium,
389 | volume_fraction, wavelen, seed,
390 | radius2 = radius2,
391 | concentration = concentration_bi, pdi = pdi2,
392 | polydisperse=True)
393 |
394 | assert_equal(R_mono2, R_bi)
395 | assert_equal(T_mono2, T_bi)
396 |
397 | # test that the reflectance is the same regardless of the order in which
398 | # the radii are specified
399 | radius2 = sc.Quantity('70.0 nm')
400 | concentration2 = sc.Quantity(np.array([0.5,0.5]), '')
401 |
402 | R, T = calc_montecarlo(nevents, ntrajectories, radius, n_particle,
403 | n_sample, n_medium, volume_fraction, wavelen, seed,
404 | radius2 = radius2, concentration = concentration2,
405 | pdi = pdi,polydisperse=True)
406 | R2, T2 = calc_montecarlo(nevents, ntrajectories, radius2, n_particle,
407 | n_sample, n_medium, volume_fraction, wavelen, seed,
408 | radius2 = radius, concentration = concentration2,
409 | pdi = pdi, polydisperse=True)
410 |
411 | assert_almost_equal(R, R2)
412 | assert_almost_equal(T, T2)
413 |
414 | # test that the second size is ignored when its concentration is set to 0
415 | radius1 = sc.Quantity('150.0 nm')
416 | radius2 = sc.Quantity('100.0 nm')
417 | concentration3 = sc.Quantity(np.array([1,0]), '')
418 | pdi3 = sc.Quantity(np.array([0., 0.]), '')
419 |
420 | R3, T3 = calc_montecarlo(nevents, ntrajectories, radius1, n_particle,
421 | n_sample, n_medium, volume_fraction, wavelen, seed,
422 | radius2 = radius2, concentration = concentration3,
423 | pdi = pdi3, polydisperse=True)
424 |
425 | assert_equal(R_mono, R3)
426 | assert_equal(T_mono, T3)
427 |
428 | # test that the reflection is essentially the same when the imaginary
429 | # index is 0 or very close to 0 in a polydisperse system
430 | ## When there's only 1 mean diameter
431 | radius1 = sc.Quantity('100.0 nm')
432 | radius2 = sc.Quantity('150.0 nm')
433 | n_matrix_abs = sc.Quantity(1. + 1e-40*1j, '')
434 | n_sample_abs = ri.n_eff(n_particle, n_matrix_abs, volume_fraction)
435 | pdi4 = sc.Quantity(np.array([0.2, 0.2]), '')
436 | concentration2 = sc.Quantity(np.array([0.1,0.9]), '')
437 |
438 | R_noabs1, T_noabs1 = calc_montecarlo(nevents, ntrajectories, radius1,
439 | n_particle, n_sample_abs.real, n_medium,
440 | volume_fraction, wavelen, seed,
441 | radius2 = radius1,
442 | concentration = concentration2,
443 | pdi = pdi4, polydisperse=True)
444 |
445 | R_abs1, T_abs1 = calc_montecarlo(nevents, ntrajectories, radius1,
446 | n_particle, n_sample_abs, n_medium,
447 | volume_fraction, wavelen, seed,
448 | radius2 = radius1,
449 | concentration = concentration2,
450 | pdi = pdi4, polydisperse=True)
451 | assert_almost_equal(R_noabs1, R_abs1)
452 | assert_almost_equal(T_noabs1, T_abs1)
453 |
454 | # When there are 2 mean diameters
455 | R_noabs2, T_noabs2 = calc_montecarlo(nevents, ntrajectories, radius1,
456 | n_particle, n_sample_abs.real, n_medium,
457 | volume_fraction, wavelen, seed,
458 | radius2 = radius2,
459 | concentration = concentration2,
460 | pdi = pdi4, polydisperse=True)
461 |
462 | # something to do with the combination of absorber, 2 radii, and nevents-1
463 | R_abs2, T_abs2 = calc_montecarlo(nevents, ntrajectories, radius1,
464 | n_particle, n_sample_abs, n_medium,
465 | volume_fraction, wavelen, seed,
466 | radius2 = radius2,
467 | concentration = concentration2,
468 | pdi = pdi4, polydisperse=True)
469 |
470 | # Note: Previously (before adding lines nevents=nevents-1 to sample_angles()),
471 | # this test yielded:
472 | # R_abs2 = 0.8682177456973259
473 | # R_noabs2 = 0.8682177456973241
474 | # making the results equal to 14 decimals. This superb agreement appears to be
475 | # a coincidence of the particular combination of events and trajectories,
476 | # as the results only matched to 1 or 2 decimals for other event and trajectory
477 | # numbers. We therefore change the required decimal agreement to only
478 | # one place.
479 | assert_almost_equal(R_noabs2, R_abs2, decimal=1)
480 | assert_almost_equal(T_noabs2, T_abs2, decimal=1)
481 |
482 | def test_throw_valueerror_for_polydisperse_core_shells():
483 | # test that a valueerror is raised when trying to run polydisperse core-shells
484 | with pytest.raises(ValueError):
485 | seed = 1
486 | nevents = 10
487 | ntrajectories = 5
488 |
489 | radius_cs = sc.Quantity(np.array([100.0, 150.0]), 'nm') # specify the radii from innermost to outermost layer
490 | n_particle_cs = sc.Quantity(np.array([1.5,1.5]), '') # specify the index from innermost to outermost layer
491 | radius2 = radius
492 | concentration = sc.Quantity(np.array([0.9,0.1]), '')
493 | pdi = sc.Quantity(np.array([1e-7, 1e-7]), '') # monodisperse limit
494 |
495 | # calculate the volume fractions of each layer
496 | vf_array = np.empty(len(radius_cs))
497 | r_array = np.array([0] + radius_cs.magnitude.tolist())
498 | for r in np.arange(len(r_array)-1):
499 | vf_array[r] = (r_array[r+1]**3-r_array[r]**3) / (r_array[-1]**3) * volume_fraction
500 |
501 | n_sample_cs = ri.n_eff(n_particle_cs, n_matrix, vf_array)
502 | R_cs, T_cs = calc_montecarlo(nevents, ntrajectories, radius_cs,
503 | n_particle_cs, n_sample_cs, n_medium,
504 | volume_fraction, wavelen, seed, radius2=radius2,
505 | concentration=concentration, pdi=pdi,
506 | polydisperse=True)
507 |
508 | def test_throw_valueerror_for_polydisperse_unspecified_parameters():
509 | # test that a valueerror is raised when the system is polydisperse and radius2
510 | # concentration or pdi are not specified
511 | with pytest.raises(ValueError):
512 | seed = 1
513 | nevents = 10
514 | ntrajectories = 5
515 |
516 | radius_cs = sc.Quantity(np.array([100.0, 150.0]), 'nm') # specify the radii from innermost to outermost layer
517 | n_particle_cs = sc.Quantity(np.array([1.5,1.5]), '') # specify the index from innermost to outermost layer
518 | concentration = sc.Quantity(np.array([0.9,0.1]), '')
519 | pdi = sc.Quantity(np.array([1e-7, 1e-7]), '') # monodisperse limit
520 |
521 | # calculate the volume fractions of each layer
522 | vf_array = np.empty(len(radius_cs))
523 | r_array = np.array([0] + radius_cs.magnitude.tolist())
524 | for r in np.arange(len(r_array)-1):
525 | vf_array[r] = (r_array[r+1]**3-r_array[r]**3) / (r_array[-1]**3) * volume_fraction
526 |
527 | n_sample_cs = ri.n_eff(n_particle_cs, n_matrix, vf_array)
528 | R_cs, T_cs = calc_montecarlo(nevents, ntrajectories, radius_cs,
529 | n_particle_cs, n_sample_cs, n_medium,
530 | volume_fraction, wavelen, seed,
531 | concentration=concentration, pdi=pdi,
532 | polydisperse=True) # unspecified radius2
533 |
534 | def test_surface_roughness():
535 | # test that the reflectance with very small surface roughness is the same
536 | # as without any roughness
537 | seed = 1
538 | nevents = 100
539 | ntrajectories = 30
540 |
541 | # Reflection with no surface roughness
542 | R, T = calc_montecarlo(nevents, ntrajectories, radius, n_particle, n_sample,
543 | n_medium, volume_fraction, wavelen, seed)
544 |
545 | # Reflection with very little fine surface roughness
546 | R_fine, T_fine = calc_montecarlo(nevents, ntrajectories, radius, n_particle,
547 | n_sample, n_medium, volume_fraction,
548 | wavelen, seed, fine_roughness = 1e-4,
549 | n_matrix=n_matrix)
550 |
551 | # Reflection with very little coarse surface roughness
552 | R_coarse, T_coarse = calc_montecarlo(nevents, ntrajectories, radius,
553 | n_particle, n_sample, n_medium,
554 | volume_fraction, wavelen, seed,
555 | coarse_roughness = 1e-5)
556 |
557 | # Reflection with very little fine and coarse surface roughness
558 | R_both, T_both = calc_montecarlo(nevents, ntrajectories, radius, n_particle,
559 | n_sample, n_medium, volume_fraction,
560 | wavelen, seed, fine_roughness=1e-4,
561 | coarse_roughness = 1e-5, n_matrix=n_matrix)
562 |
563 | assert_almost_equal(R, R_fine)
564 | assert_almost_equal(T, T_fine)
565 | assert_almost_equal(R, R_coarse)
566 | assert_almost_equal(T, T_coarse)
567 | assert_almost_equal(R, R_both)
568 | assert_almost_equal(T, T_both)
569 |
570 | def calc_montecarlo(nevents, ntrajectories, radius, n_particle, n_sample,
571 | n_medium, volume_fraction, wavelen, seed, radius2=None,
572 | concentration=None, pdi=None, polydisperse=False,
573 | fine_roughness=0., coarse_roughness=0., n_matrix=None,
574 | incidence_theta_min=0., incidence_theta_max=0.):
575 |
576 | # set up a seeded random number generator that will give consistent results
577 | # between numpy versions. This is to reproduce the gold values which are
578 | # hardcoded in the tests. Note that seed is in the form of a list. Setting
579 | # the seed without the list brackets yields a different set of random
580 | # numbers.
581 | rng = np.random.RandomState([seed])
582 |
583 | incidence_theta_min=sc.Quantity(incidence_theta_min,'rad')
584 | incidence_theta_max=sc.Quantity(incidence_theta_min,'rad')
585 |
586 | # Function to run montecarlo for the tests
587 | p, mu_scat, mu_abs = mc.calc_scat(radius, n_particle, n_sample,
588 | volume_fraction, wavelen,
589 | radius2=radius2,
590 | concentration=concentration, pdi=pdi,
591 | polydisperse=polydisperse,
592 | fine_roughness=fine_roughness,
593 | n_matrix=n_matrix)
594 |
595 | if coarse_roughness > 0.:
596 | r0, k0, W0, kz0_rotated, kz0_reflected = \
597 | mc.initialize(nevents, ntrajectories, n_medium, n_sample, 'film',
598 | rng=rng, coarse_roughness=coarse_roughness,
599 | incidence_theta_min=incidence_theta_min,
600 | incidence_theta_max=incidence_theta_max)
601 | else:
602 | r0, k0, W0 = mc.initialize(nevents, ntrajectories, n_medium, n_sample,
603 | 'film', rng=rng,
604 | incidence_theta_min=incidence_theta_min,
605 | incidence_theta_max=incidence_theta_max)
606 | kz0_rotated = None
607 | kz0_reflected = None
608 |
609 | r0 = sc.Quantity(r0, 'um')
610 | k0 = sc.Quantity(k0, '')
611 | W0 = sc.Quantity(W0, '')
612 |
613 | sintheta, costheta, sinphi, cosphi, _, _= mc.sample_angles(nevents,
614 | ntrajectories,
615 | p, rng=rng)
616 | step = mc.sample_step(nevents, ntrajectories, mu_scat,
617 | fine_roughness=fine_roughness, rng=rng)
618 |
619 | trajectories = mc.Trajectory(r0, k0, W0)
620 | trajectories.absorb(mu_abs, step)
621 | trajectories.scatter(sintheta, costheta, sinphi, cosphi)
622 | trajectories.move(step)
623 |
624 | cutoff = sc.Quantity('50.0 um')
625 |
626 | # calculate R, T
627 | # (should raise warning that n_matrix and n_particle are not set, so
628 | # tir correction is based only on sample index)
629 | with pytest.warns(UserWarning):
630 | R, T = det.calc_refl_trans(trajectories, cutoff, n_medium, n_sample,
631 | 'film', kz0_rot=kz0_rotated,
632 | kz0_refl=kz0_reflected)
633 |
634 | return R, T
635 |
636 | def test_goniometer_normalization():
637 |
638 | # test the goniometer renormalization function
639 | refl = 0.002
640 | det_distance = 13.
641 | det_len = 2.4
642 | det_theta = 0
643 | refl_renorm = det.normalize_refl_goniometer(refl, det_distance, det_len,
644 | det_theta)
645 |
646 | assert_almost_equal(refl_renorm, 0.368700804483) # calculated by hand
647 |
648 | def test_goniometer_detector():
649 | # test
650 | z_pos = np.array([[0,0,0,0],[1,1,1,1],[-1,-1,2,2],[-2,-2,20,-0.0000001]])
651 | ntrajectories = z_pos.shape[1]
652 | nevents = z_pos.shape[0]
653 | x_pos = np.zeros((nevents, ntrajectories))
654 | y_pos = np.zeros((nevents, ntrajectories))
655 | ky = np.zeros((nevents-1, ntrajectories))
656 | kx = np.array([[0,0,0,0],[0,0,0,0],[0,0,0,1/np.sqrt(2)]])
657 | kz = np.array([[1,1,1,1],[-1,-1,1,1],[-1,-1,1,-1/np.sqrt(2)]])
658 | weights = np.ones((nevents, ntrajectories))
659 | trajectories = mc.Trajectory(np.array([x_pos, y_pos, z_pos]),
660 | np.array([kx, ky, kz]), weights)
661 | thickness = 10
662 | n_medium = 1
663 | n_sample = 1
664 | # Should raise warning that n_matrix and n_particle are not set, so
665 | # tir correction is based only on sample index
666 | with pytest.warns(UserWarning):
667 | R, T = det.calc_refl_trans(trajectories, thickness, n_medium, n_sample,
668 | 'film', detector=True,
669 | det_theta=sc.Quantity('45.0 degrees'),
670 | det_len=sc.Quantity('1.0 um'),
671 | det_dist=sc.Quantity('10.0 cm'),
672 | plot_detector=False)
673 |
674 | assert_almost_equal(R, 0.25)
675 |
--------------------------------------------------------------------------------
/structcol/tests/test_detector_sphere.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018, Vinothan N. Manoharan, Annie Stephenson, Victoria Hwang,
2 | # Solomon Barkley
3 | #
4 | # This file is part of the structural-color python package.
5 | #
6 | # This package is free software: you can redistribute it and/or modify it under
7 | # the terms of the GNU General Public License as published by the Free Software
8 | # Foundation, either version 3 of the License, or (at your option) any later
9 | # version.
10 | #
11 | # This package is distributed in the hope that it will be useful, but WITHOUT
12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14 | # details.
15 | #
16 | # You should have received a copy of the GNU General Public License along with
17 | # this package. If not, see .
18 | """
19 | Tests for the montecarlo model for sphere geometry (in structcol/montecarlo.py)
20 | .. moduleauthor:: Annie Stephenson
21 | .. moduleauthor:: Victoria Hwang
22 | .. moduleathor:: Solomon Barkley
23 | .. moduleauthor:: Vinothan N. Manoharan
24 |
25 | TODO: either delete this file or delete tests repeated in montecarlo.py
26 | """
27 |
28 | import structcol as sc
29 | from .. import montecarlo as mc
30 | from .. import detector as det
31 | from .. import refractive_index as ri
32 | import numpy as np
33 | from numpy.testing import assert_almost_equal
34 | import pytest
35 |
36 | # Define a system to be used for the tests
37 | nevents = 3
38 | ntrajectories = 4
39 | radius = sc.Quantity('150.0 nm')
40 | assembly_radius = 5
41 | volume_fraction = 0.5
42 | n_particle = sc.Quantity(1.5, '')
43 | n_matrix = sc.Quantity(1.0, '')
44 | n_sample = ri.n_eff(n_particle, n_matrix, volume_fraction)
45 | angles = sc.Quantity(np.linspace(0.01,np.pi, 200), 'rad')
46 | wavelen = sc.Quantity('400.0 nm')
47 |
48 | # Index of the scattering event and trajectory corresponding to the reflected
49 | # photons
50 | refl_index = np.array([2,0,2])
51 |
52 | def test_calc_refl_trans():
53 | # this test should give deterministic results
54 | small_n = sc.Quantity(1.0,'')
55 | large_n = sc.Quantity(2.0,'')
56 |
57 | # test absoprtion and stuck without fresnel
58 | z_pos = np.array([[0,0,0,0],[1,1,1,1],[-1,11,2,11],[-2,12,4,12]])
59 | x_pos = np.array([[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]])
60 | y_pos = np.array([[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]])
61 | ntrajectories = z_pos.shape[1]
62 | kx = np.zeros((3,4))
63 | ky = np.zeros((3,4))
64 | kz = np.array([[1,1,1,1],[-1,1,1,1],[-1,1,1,1]])
65 | weights = np.array([[.8, .8, .9, .8],[.7, .3, .7, 0],[.1, .1, .5, 0]])
66 | trajectories = mc.Trajectory([x_pos, y_pos, z_pos],[kx, ky, kz], weights)
67 | p, mu_scat, mu_abs = mc.calc_scat(radius, n_particle, small_n, volume_fraction, wavelen)
68 | # Should raise warning that n_matrix and n_particle are not set, so
69 | # tir correction is based only on sample index
70 | with pytest.warns(UserWarning):
71 | refl, trans = det.calc_refl_trans(trajectories, assembly_radius,
72 | small_n, small_n, 'sphere')
73 | expected_trans_array = np.array([0., .3, 0.25, 0])/ntrajectories #calculated manually
74 | expected_refl_array = np.array([.7, 0., .25, 0.])/ntrajectories #calculated manually
75 | assert_almost_equal(refl, np.sum(expected_refl_array))
76 | assert_almost_equal(trans, np.sum(expected_trans_array))
77 |
78 | # test fresnel as well
79 | # (should raise warning that n_matrix and n_particle are not set, so
80 | # tir correction is based only on sample index)
81 | with pytest.warns(UserWarning):
82 | refl, trans = det.calc_refl_trans(trajectories, assembly_radius,
83 | small_n, large_n, 'sphere')
84 | expected_trans_array = np.array([0.0345679, .25185185, 0.22222222, 0.])/ntrajectories #calculated manually
85 | expected_refl_array = np.array([.69876543, 0.12592593, 0.33333333, 0.11111111])/ntrajectories #calculated manually
86 | assert_almost_equal(refl, np.sum(expected_refl_array))
87 | assert_almost_equal(trans, np.sum(expected_trans_array))
88 |
89 | # test steps in z longer than sample thickness
90 | z_pos = np.array([[0,0,0,0],[1,1,14,12],[-1,11,2,11],[-2,12,4,12]])
91 | trajectories = mc.Trajectory([x_pos, y_pos, z_pos],[kx, ky, kz], weights)
92 | # Should raise warning that n_matrix and n_particle are not set, so
93 | # tir correction is based only on sample index
94 | with pytest.warns(UserWarning):
95 | refl, trans= det.calc_refl_trans(trajectories, assembly_radius,
96 | small_n, small_n, 'sphere')
97 | expected_trans_array = np.array([0., .3, .9, .8])/ntrajectories #calculated manually
98 | expected_refl_array = np.array([.7, 0., 0., 0.])/ntrajectories #calculated manually
99 | assert_almost_equal(refl, np.sum(expected_refl_array))
100 | assert_almost_equal(trans, np.sum(expected_trans_array))
101 |
102 | # test tir
103 | z_pos = np.array([[0,0,0,0],[1,1,1,1],[-1,11,2,11],[-2,12,4,12]])
104 | weights = np.ones((3,4))
105 | trajectories = mc.Trajectory([x_pos, y_pos, z_pos],[kx, ky, kz], weights)
106 | # Should raise warning that n_matrix and n_particle are not set, so
107 | # tir correction is based only on sample index
108 | with pytest.warns(UserWarning):
109 | refl, trans = det.calc_refl_trans(trajectories, assembly_radius,
110 | small_n, small_n, 'sphere', p=p,
111 | mu_abs=mu_abs, mu_scat=mu_scat,
112 | run_fresnel_traj=True)
113 | # since the tir=True reruns the stuck trajectory, we don't know whether it will end up reflected or transmitted
114 | # all we can know is that the end refl + trans > 0.99
115 | assert_almost_equal(refl + trans, 1.)
116 |
117 | def test_get_angles_sphere():
118 | z_pos = np.array([[0,0,0,0],[1,1,1,1],[-1,11,2,11],[-2,12,4,12]])
119 | x_pos = np.array([[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,-0,0,0]])
120 | y_pos = np.array([[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]])
121 | kx = np.zeros((3,4))
122 | ky = np.zeros((3,4))
123 | kz = np.array([[1,1,1,1],[-1,1,1,1],[-1,1,1,1]])
124 | trajectories = mc.Trajectory([x_pos, y_pos, z_pos],[kx, ky, kz], None)
125 |
126 | indices = np.array([1,1,1,1])
127 | thetas, _ = det.get_angles(indices, 'sphere', trajectories, assembly_radius, init_dir = 1)
128 | assert_almost_equal(np.sum(thetas.magnitude), 0.)
129 |
130 | def test_index_match():
131 | ntrajectories = 2
132 | nevents = 3
133 | wavelen = sc.Quantity('600.0 nm')
134 | radius = sc.Quantity('0.140 um')
135 | microsphere_radius = sc.Quantity('10.0 um')
136 | volume_fraction = sc.Quantity(0.55,'')
137 | n_particle = sc.Quantity(1.6,'')
138 | n_matrix = sc.Quantity(1.6,'')
139 | n_sample = n_matrix
140 | n_medium = sc.Quantity(1.0,'')
141 |
142 | p, mu_scat, mu_abs = mc.calc_scat(radius, n_particle, n_sample, volume_fraction, wavelen)
143 |
144 | # initialize all at center top edge of the sphere going down
145 | r0_sphere = np.zeros((3,nevents+1,ntrajectories))
146 | k0_sphere = np.zeros((3,nevents,ntrajectories))
147 | k0_sphere[2,0,:] = 1
148 | W0_sphere = np.ones((nevents, ntrajectories))
149 |
150 | # make into quantities with units
151 | r0_sphere = sc.Quantity(r0_sphere, 'um')
152 | k0_sphere = sc.Quantity(k0_sphere, '')
153 | W0_sphere = sc.Quantity(W0_sphere, '')
154 |
155 | # Generate a matrix of all the randomly sampled angles first
156 | sintheta, costheta, sinphi, cosphi, _, _ = mc.sample_angles(nevents, ntrajectories, p)
157 |
158 | # Create step size distribution
159 | step = mc.sample_step(nevents, ntrajectories, mu_scat)
160 |
161 | # make trajectories object
162 | trajectories_sphere = mc.Trajectory(r0_sphere, k0_sphere, W0_sphere)
163 | trajectories_sphere.absorb(mu_abs, step)
164 | trajectories_sphere.scatter(sintheta, costheta, sinphi, cosphi)
165 | trajectories_sphere.move(step)
166 |
167 | # calculate reflectance
168 | # (should raise warning that n_matrix and n_particle are not set, so
169 | # tir correction is based only on sample index)
170 | with pytest.warns(UserWarning):
171 | refl_sphere, trans = det.calc_refl_trans(trajectories_sphere,
172 | microsphere_radius,
173 | n_medium, n_sample,
174 | 'sphere', p=p,
175 | mu_abs=mu_abs,
176 | mu_scat=mu_scat,
177 | run_fresnel_traj = True,
178 | max_stuck = 0.0001)
179 |
180 | # calculated by hand from fresnel infinite sum
181 | refl_fresnel_int = 0.053 # calculated by hand
182 | refl_exact = refl_fresnel_int + (1-refl_fresnel_int)**2*refl_fresnel_int/(1-refl_fresnel_int**2)
183 |
184 | # under index-matched conditions, the step sizes are huge (bigger than the
185 | # sample size), and the light is scattered into the forward direction. As a
186 | # result, the reflectance is essentially deterministic, even though the
187 | # seed is not set for the random number generator.
188 | assert_almost_equal(refl_sphere, refl_exact, decimal=3)
189 |
--------------------------------------------------------------------------------
/structcol/tests/test_event_distribution.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan, Annie Stephenson
2 | #
3 | # This file is part of the structural-color python package.
4 | #
5 | # This package is free software: you can redistribute it and/or modify it under
6 | # the terms of the GNU General Public License as published by the Free Software
7 | # Foundation, either version 3 of the License, or (at your option) any later
8 | # version.
9 | #
10 | # This package is distributed in the hope that it will be useful, but WITHOUT
11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 | # details.
14 | #
15 | # You should have received a copy of the GNU General Public License along with
16 | # this package. If not, see .
17 | """
18 | Tests for the montecarlo model (in structcol/montecarlo.py)
19 |
20 | .. moduleauthor:: Annie Stephenson
21 | .. moduleauthor:: Vinothan N. Manoharan
22 | """
23 |
24 | import structcol as sc
25 | from structcol import montecarlo as mc
26 | from structcol import refractive_index as ri
27 | from structcol import event_distribution as ed
28 | from structcol import detector as det
29 | import numpy as np
30 | from numpy.testing import assert_equal, assert_almost_equal, assert_array_less
31 | import pytest
32 |
33 | # Monte Carlo parameters
34 | ntrajectories = 30
35 | # number of scattering events in each trajectory
36 | nevents = 300
37 |
38 | # source/detector properties
39 | wavelength = sc.Quantity(np.array(550.0),'nm')
40 |
41 | # sample properties
42 | particle_radius = sc.Quantity('140.0 nm')
43 | volume_fraction = sc.Quantity(0.56, '')
44 | thickness = sc.Quantity('10.0 um')
45 | particle = 'ps'
46 | matrix = 'air'
47 | boundary = 'film'
48 |
49 | # indices of refraction
50 | #
51 | # Refractive indices can be specified as pint quantities or called from the
52 | # refractive_index module. n_matrix is the # space within sample. n_medium is
53 | # outside the sample.
54 | n_particle = ri.n('polystyrene', wavelength)
55 | n_matrix = ri.n('vacuum', wavelength)
56 | n_medium = ri.n('vacuum', wavelength)
57 |
58 | # Calculate the effective refractive index of the sample
59 | n_sample = ri.n_eff(n_particle, n_matrix, volume_fraction)
60 |
61 | # Calculate the phase function and scattering and absorption coefficients from
62 | # the single scattering model (this absorption coefficient is of the scatterer,
63 | # not of an absorber added to the system)
64 | p, mu_scat, mu_abs = mc.calc_scat(particle_radius, n_particle, n_sample,
65 | volume_fraction, wavelength)
66 | lscat = 1/mu_scat.magnitude # microns
67 |
68 | # set up a seeded random number generator that will give consistent results
69 | # between numpy versions.
70 | seed = 1
71 | rng = np.random.RandomState([seed])
72 |
73 | # Initialize the trajectories
74 | r0, k0, W0 = mc.initialize(nevents, ntrajectories, n_medium, n_sample,
75 | boundary, rng=rng)
76 | r0 = sc.Quantity(r0, 'um')
77 | k0 = sc.Quantity(k0, '')
78 | W0 = sc.Quantity(W0, '')
79 |
80 | # Generate a matrix of all the randomly sampled angles first
81 | sintheta, costheta, sinphi, cosphi, theta, _ = mc.sample_angles(nevents,
82 | ntrajectories,
83 | p, rng=rng)
84 | sintheta = np.sin(theta)
85 | costheta = np.cos(theta)
86 |
87 | # Create step size distribution
88 | step = mc.sample_step(nevents, ntrajectories, mu_scat, rng=rng)
89 |
90 | # Create trajectories object
91 | trajectories = mc.Trajectory(r0, k0, W0)
92 |
93 | # Run photons
94 | trajectories.absorb(mu_abs, step)
95 | trajectories.scatter(sintheta, costheta, sinphi, cosphi)
96 | trajectories.move(step)
97 |
98 | # following calculation should raise a warning that n_particle and n_matrix are
99 | # not set
100 | with pytest.warns(UserWarning):
101 | refl_indices, trans_indices,\
102 | inc_refl_per_traj,_,_, refl_per_traj, trans_per_traj,\
103 | trans_frac, refl_frac,\
104 | refl_fresnel,\
105 | trans_fresnel,\
106 | reflectance,\
107 | transmittance,\
108 | tir_refl_bool,_,_ = det.calc_refl_trans(trajectories, thickness,
109 | n_medium, n_sample, boundary,
110 | return_extra = True)
111 |
112 | refl_events, trans_events = ed.calc_refl_trans_event(refl_per_traj,
113 | inc_refl_per_traj,
114 | trans_per_traj,
115 | refl_indices,
116 | trans_indices,
117 | nevents)
118 |
119 | def test_refl_events():
120 | '''
121 | Check that refl_events is consistent with reflectance
122 | '''
123 |
124 | # sum of refl_events should be less than reflectance because it doesn't
125 | # contain correction terms for fresnel (and stuck for cases where that
126 | # matters)
127 | assert_array_less(np.sum(refl_events), reflectance)
128 |
129 | # trajectories always propagate into the sample for first event, so none
130 | # can be reflected
131 | assert_equal(refl_events[1],0)
132 |
133 | # trajectories cannot be transmitted at interface before first scattering
134 | # event
135 | assert_equal(trans_events[0],0)
136 |
137 | def test_fresnel_events():
138 | '''
139 | Check that fresnel corrections make sense
140 | '''
141 | refl_events_fresnel_avg = ed.calc_refl_event_fresnel_avg(refl_events,
142 | refl_indices,
143 | trans_indices,
144 | refl_fresnel,
145 | trans_fresnel,
146 | refl_frac,
147 | trans_frac,
148 | nevents)
149 |
150 | pdf_refl, pdf_trans = ed.calc_pdf_scat(refl_events, trans_events, nevents)
151 |
152 | refl_events_fresnel_samp = ed.calc_refl_event_fresnel_pdf(refl_events,
153 | pdf_refl,
154 | pdf_trans,
155 | refl_indices,
156 | trans_indices,
157 | refl_fresnel,
158 | trans_fresnel,
159 | refl_frac,
160 | trans_frac,
161 | nevents, rng=rng)
162 |
163 | # check that average and sampling give same total
164 | assert_almost_equal(np.sum(refl_events_fresnel_avg),
165 | np.sum(refl_events_fresnel_samp))
166 |
167 | # check that reflectance from monte carlo gives same as fresnel reflected
168 | # summed reflectance from event distribution
169 | # TODO these should be equal to more decimals. Need to look into this.
170 | assert_almost_equal(reflectance, np.sum(refl_events_fresnel_avg), decimal=1)
171 |
172 | def test_tir_events():
173 | '''
174 | Check that totally internally reflected trajectories make sense
175 | '''
176 | tir_all,\
177 | tir_all_refl,\
178 | tir_single,\
179 | tir_single_refl,\
180 | tir_indices_single = ed.calc_tir(tir_refl_bool, refl_indices,
181 | trans_indices, inc_refl_per_traj,
182 | n_sample,
183 | n_medium,
184 | boundary,
185 | trajectories,
186 | thickness)
187 |
188 | # the reflected tir's should always be less than total tir's
189 | assert_array_less(np.sum(tir_single_refl), np.sum(tir_single))
190 | assert_array_less(np.sum(tir_all_refl), np.sum(tir_all))
191 |
--------------------------------------------------------------------------------
/structcol/tests/test_fields.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan, Victoria Hwang, Solomon Barkley,
2 | # Annie Stephenson
3 | #
4 | # This file is part of the structural-color python package.
5 | #
6 | # This package is free software: you can redistribute it and/or modify it under
7 | # the terms of the GNU General Public License as published by the Free Software
8 | # Foundation, either version 3 of the License, or (at your option) any later
9 | # version.
10 | #
11 | # This package is distributed in the hope that it will be useful, but WITHOUT
12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14 | # details.
15 | #
16 | # You should have received a copy of the GNU General Public License along with
17 | # this package. If not, see .
18 | """
19 | Tests for the phase calculations in montecarlo model
20 |
21 | .. moduleauthor:: Annie Stephenson
22 |
23 | """
24 |
25 | import structcol as sc
26 | from .. import montecarlo as mc
27 | from .. import detector as det
28 | from .. import detector_polarization_phase as detp
29 | from .. import refractive_index as ri
30 | import numpy as np
31 | from numpy.testing import assert_almost_equal
32 | import pytest
33 |
34 | def test_2pi_shift():
35 | # test that phase mod 2Pi is the same as phase.
36 | # This test should pass irrespective of the state of the random number
37 | # generator, so we do not need to explicitly specify a seed.
38 |
39 | # incident light wavelength
40 | wavelength = sc.Quantity('600.0 nm')
41 |
42 | # sample parameters
43 | radius = sc.Quantity('0.140 um')
44 | volume_fraction = sc.Quantity(0.55, '')
45 | n_imag = 2.1e-4
46 | n_particle = ri.n('polystyrene', wavelength) + n_imag
47 | n_matrix = ri.n('vacuum', wavelength)
48 | n_medium = ri.n('vacuum', wavelength)
49 | n_sample = ri.n_eff(n_particle,
50 | n_matrix,
51 | volume_fraction)
52 | thickness = sc.Quantity('50.0 um')
53 | boundary = 'film'
54 |
55 | # Monte Carlo parameters
56 | ntrajectories = 10
57 | nevents = 30
58 |
59 | # Calculate scattering quantities
60 | p, mu_scat, mu_abs = mc.calc_scat(radius, n_particle, n_sample,
61 | volume_fraction, wavelength, fields=True)
62 |
63 | # Initialize trajectories
64 | r0, k0, W0, E0 = mc.initialize(nevents, ntrajectories, n_medium, n_sample, boundary,
65 | fields=True)
66 | r0 = sc.Quantity(r0, 'um')
67 | k0 = sc.Quantity(k0, '')
68 | W0 = sc.Quantity(W0, '')
69 | E0 = sc.Quantity(E0, '')
70 |
71 | trajectories = mc.Trajectory(r0, k0, W0, fields=E0)
72 |
73 | # Sample trajectory angles
74 | sintheta, costheta, sinphi, cosphi, theta, phi = mc.sample_angles(nevents,
75 | ntrajectories, p)
76 | # Sample step sizes
77 | step = mc.sample_step(nevents, ntrajectories, mu_scat)
78 |
79 | # Update trajectories based on sampled values
80 | trajectories.scatter(sintheta, costheta, sinphi, cosphi)
81 | trajectories.move(step)
82 | trajectories.absorb(mu_abs, step)
83 | trajectories.calc_fields(theta, phi, sintheta, costheta, sinphi, cosphi,
84 | n_particle, n_sample, radius, wavelength,
85 | step, volume_fraction)
86 |
87 | # calculate reflectance
88 | # (should raise warning that n_matrix and n_particle are not set, so
89 | # tir correction is based only on sample index)
90 | with pytest.warns(UserWarning):
91 | refl_trans_result = det.calc_refl_trans(trajectories, thickness,
92 | n_medium, n_sample, boundary,
93 | return_extra=True)
94 |
95 | refl_indices = refl_trans_result[0]
96 | refl_per_traj = refl_trans_result[3]
97 | reflectance_fields, _ = detp.calc_refl_phase_fields(trajectories,
98 | refl_indices,
99 | refl_per_traj)
100 |
101 | # now do mod 2pi
102 | trajectories.fields = trajectories.fields*np.exp(2*np.pi*1j)
103 | reflectance_fields_shift, _ = detp.calc_refl_phase_fields(trajectories,
104 | refl_indices,
105 | refl_per_traj)
106 |
107 | assert_almost_equal(reflectance_fields, reflectance_fields_shift,
108 | decimal=15)
109 |
110 |
111 | def test_intensity_coherent():
112 | # tests that the intensity of the summed fields correspond to the equation for
113 | # coherent light: Ix = E_x1^2 + E_x2^2 + 2E_x1*E_x2
114 |
115 | # this test isn't based on random values, so should produce deterministic
116 | # results.
117 |
118 | # construct 2 identical trajectories that exit at same event
119 | ntrajectories = 2
120 | nevents = 3
121 | z_pos = np.array([[0,0],[1,1],[-1,-1]])
122 | kz = np.array([[1,1],[-1,1],[-1,1]])
123 | directions = np.array([kz,kz,kz])
124 | weights = np.array([[1, 1],[1, 1],[1, 1]])
125 | trajectories = mc.Trajectory([np.nan, np.nan, z_pos], directions, weights)
126 | trajectories.fields = np.zeros((3, nevents, ntrajectories))
127 | trajectories.fields[:,0,:] = 0.5
128 | trajectories.fields[:,1,:] = 1
129 | trajectories.fields[:,2,:] = 1.5
130 |
131 | # calculate reflectance phase
132 | refl_per_traj = np.array([0.5, 0.5])
133 | refl_indices = np.array([2, 2])
134 | refl_phase, _ = detp.calc_refl_phase_fields(trajectories, refl_indices, refl_per_traj)
135 | intensity_incident = np.sum(trajectories.weight[0,:])
136 | intensity = refl_phase*intensity_incident
137 |
138 | # Calculate I = (E1 + E2)*(E1 + E2) = E1*E1 + E2*E2 + E1*E2 + E2*E1
139 | ev = 2
140 | field_x = np.sqrt(trajectories.weight[ev,:])*trajectories.fields[0,ev,:]
141 | field_y = np.sqrt(trajectories.weight[ev,:])*trajectories.fields[1,ev,:]
142 | field_z = np.sqrt(trajectories.weight[ev,:])*trajectories.fields[2,ev,:]
143 | intensity_x = np.conj(field_x[0])*field_x[0] + np.conj(field_x[1])*field_x[1] + np.conj(field_x[0])*field_x[1] + np.conj(field_x[1])*field_x[0]
144 | intensity_y = np.conj(field_y[0])*field_y[0] + np.conj(field_y[1])*field_y[1] + np.conj(field_y[0])*field_y[1] + np.conj(field_y[1])*field_y[0]
145 | intensity_z = np.conj(field_z[0])*field_z[0] + np.conj(field_z[1])*field_z[1] + np.conj(field_z[0])*field_z[1] + np.conj(field_z[1])*field_z[0]
146 | intensity_2 = intensity_x + intensity_y + intensity_z
147 |
148 | # compare values
149 | assert_almost_equal(intensity, intensity_2, decimal=15)
150 |
151 | def test_pi_shift_zero():
152 | # tests if a pi shift leads to zero intensity. This test should produce a
153 | # deterministic result.
154 |
155 | # construct 2 trajectories with relative pi phase shift that exit at same event
156 | ntrajectories = 2
157 | nevents = 3
158 | z_pos = np.array([[0,0],[1,1],[-1,-1]])
159 | x_pos = np.array([[0,0],[1,1],[-1,-1]])
160 | kz = np.array([[1,1],[-1,1],[-1,1]])
161 | directions = np.array([kz,kz,kz])
162 | weights = np.array([[1, 1],[1, 1],[1, 1]])
163 | trajectories = mc.Trajectory([x_pos, np.nan, z_pos],directions, weights)
164 | trajectories.fields = np.zeros((3, nevents, ntrajectories), dtype=complex)
165 | trajectories.fields[:,2,0] = 1
166 | trajectories.fields[:,2,1] = np.exp(np.pi*1j)
167 |
168 | # calculate reflectance phase
169 | refl_per_traj = np.array([0.5, 0.5])
170 | refl_indices = np.array([2, 2])
171 | refl_fields, _ = detp.calc_refl_phase_fields(trajectories, refl_indices, refl_per_traj)
172 |
173 | # check whether reflectance phase is 0
174 | assert_almost_equal(refl_fields, 0, decimal=15)
175 |
176 |
177 | def test_field_normalized():
178 | # calculate fields and directions
179 |
180 | # This test should pass regardless of the state of the random number
181 | # generator, so we do not need to specify an explicit seed.
182 |
183 | # incident light wavelength
184 | wavelength = sc.Quantity('600.0 nm')
185 |
186 | # sample parameters
187 | radius = sc.Quantity('0.140 um')
188 | volume_fraction = sc.Quantity(0.55, '')
189 | n_imag = 2.1e-4
190 | n_particle = ri.n('polystyrene', wavelength) + n_imag*1j # refractive indices can be specified as pint quantities or
191 | n_matrix = ri.n('vacuum', wavelength) # called from the refractive_index module. n_matrix is the
192 | n_medium = ri.n('vacuum', wavelength) # space within sample. n_medium is outside the sample
193 | n_sample = ri.n_eff(n_particle, # refractive index of sample, calculated using Bruggeman approximation
194 | n_matrix,
195 | volume_fraction)
196 | boundary = 'film'
197 |
198 | # Monte Carlo parameters
199 | ntrajectories = 10 # number of trajectories
200 | nevents = 10 # number of scattering events in each trajectory
201 |
202 | # Calculate scattering quantities
203 | p, mu_scat, mu_abs = mc.calc_scat(radius, n_particle, n_sample,
204 | volume_fraction, wavelength, fields=True)
205 |
206 | # Initialize trajectories
207 | r0, k0, W0, E0 = mc.initialize(nevents, ntrajectories, n_medium, n_sample, boundary,
208 | fields=True)
209 | r0 = sc.Quantity(r0, 'um')
210 | k0 = sc.Quantity(k0, '')
211 | W0 = sc.Quantity(W0, '')
212 | E0 = sc.Quantity(E0,'')
213 |
214 | trajectories = mc.Trajectory(r0, k0, W0, fields=E0)
215 |
216 |
217 | # Sample trajectory angles
218 | sintheta, costheta, sinphi, cosphi, theta, phi= mc.sample_angles(nevents,
219 | ntrajectories,p)
220 | # Sample step sizes
221 | step = mc.sample_step(nevents, ntrajectories, mu_scat)
222 |
223 | # Update trajectories based on sampled values
224 | trajectories.scatter(sintheta, costheta, sinphi, cosphi)
225 | trajectories.move(step)
226 | trajectories.calc_fields(theta, phi, sintheta, costheta, sinphi, cosphi,
227 | n_particle, n_sample, radius, wavelength,
228 | step, volume_fraction)
229 | trajectories.absorb(mu_abs, step)
230 |
231 | # take the dot product
232 | trajectories.fields = trajectories.fields.magnitude
233 |
234 | field_mag= np.sqrt(np.conj(trajectories.fields[0,:,:])*trajectories.fields[0,:,:] +
235 | np.conj(trajectories.fields[1,:,:])*trajectories.fields[1,:,:] +
236 | np.conj(trajectories.fields[2,:,:])*trajectories.fields[2,:,:])
237 |
238 | assert_almost_equal(np.sum(field_mag)/(ntrajectories*(nevents+1)), 1, decimal=15)
239 |
240 | def test_field_perp_direction():
241 | # calculate fields and directions
242 |
243 | # This test should pass regardless of the state of the random number
244 | # generator, so we do not need to specify an explicit seed.
245 |
246 | # incident light wavelength
247 | wavelength = sc.Quantity('600.0 nm')
248 |
249 | # sample parameters
250 | radius = sc.Quantity('0.140 um')
251 | volume_fraction = sc.Quantity(0.55, '')
252 | n_imag = 2.1e-4
253 | n_particle = ri.n('polystyrene', wavelength) + n_imag*1j
254 | n_matrix = ri.n('vacuum', wavelength)
255 | n_medium = ri.n('vacuum', wavelength)
256 | n_sample = ri.n_eff(n_particle,
257 | n_matrix,
258 | volume_fraction)
259 | boundary = 'film'
260 |
261 | # Monte Carlo parameters
262 | ntrajectories = 10 # number of trajectories
263 | nevents = 10 # number of scattering events in each trajectory
264 |
265 | # Calculate scattering quantities
266 | p, mu_scat, mu_abs = mc.calc_scat(radius, n_particle, n_sample,
267 | volume_fraction, wavelength, fields=True)
268 |
269 | # Initialize trajectories
270 | r0, k0, W0, E0 = mc.initialize(nevents, ntrajectories, n_medium, n_sample, boundary,
271 | fields=True)
272 | r0 = sc.Quantity(r0, 'um')
273 | k0 = sc.Quantity(k0, '')
274 | W0 = sc.Quantity(W0, '')
275 | E0 = sc.Quantity(E0,'')
276 |
277 | trajectories = mc.Trajectory(r0, k0, W0, fields = E0)
278 |
279 |
280 | # Sample trajectory angles
281 | sintheta, costheta, sinphi, cosphi, theta, phi= mc.sample_angles(nevents,
282 | ntrajectories,p)
283 | # Sample step sizes
284 | step = mc.sample_step(nevents, ntrajectories, mu_scat)
285 |
286 | # Update trajectories based on sampled values
287 | trajectories.scatter(sintheta, costheta, sinphi, cosphi)
288 | trajectories.move(step)
289 | trajectories.calc_fields(theta, phi, sintheta, costheta, sinphi, cosphi,
290 | n_particle, n_sample, radius, wavelength, step, volume_fraction)
291 | trajectories.absorb(mu_abs, step)
292 |
293 | # take the dot product
294 | trajectories.direction = trajectories.direction.magnitude
295 | trajectories.fields = trajectories.fields.magnitude
296 |
297 | dot_prod = (trajectories.direction[0,:,:]*trajectories.fields[0,1:,:] +
298 | trajectories.direction[1,:,:]*trajectories.fields[1,1:,:] +
299 | trajectories.direction[2,:,:]*trajectories.fields[2,1:,:])
300 |
301 | assert_almost_equal(np.sum(dot_prod), 0., decimal=14)
302 |
--------------------------------------------------------------------------------
/structcol/tests/test_mie.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan
2 | #
3 | # This file is part of the structural-color python package.
4 | #
5 | # This package is free software: you can redistribute it and/or modify it under
6 | # the terms of the GNU General Public License as published by the Free Software
7 | # Foundation, either version 3 of the License, or (at your option) any later
8 | # version.
9 | #
10 | # This package is distributed in the hope that it will be useful, but WITHOUT
11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 | # details.
14 | #
15 | # You should have received a copy of the GNU General Public License along with
16 | # this package. If not, see .
17 | """
18 | Tests for the mie module
19 |
20 | .. moduleauthor:: Vinothan N. Manoharan
21 | """
22 |
23 | from .. import Quantity, index_ratio, size_parameter, np, mie
24 | from pytest import raises
25 | from numpy.testing import assert_almost_equal, assert_array_almost_equal
26 | from pint.errors import DimensionalityError
27 |
28 | def test_cross_sections():
29 | # Test cross sections against values calculated from BHMIE code (originally
30 | # calculated for testing fortran-based Mie code in holopy)
31 |
32 | # test case is PS sphere in water
33 | wavelen = Quantity('658.0 nm')
34 | radius = Quantity('0.85 um')
35 | n_matrix = Quantity(1.33, '')
36 | n_particle = Quantity(1.59 + 1e-4 * 1.0j, '')
37 | m = index_ratio(n_particle, n_matrix)
38 | x = size_parameter(wavelen, n_matrix, radius)
39 | qscat, qext, qback = mie.calc_efficiencies(m, x)
40 | g = mie.calc_g(m,x) # asymmetry parameter
41 |
42 | qscat_std, qext_std, g_std = 3.6647, 3.6677, 0.92701
43 | assert_almost_equal(qscat, qscat_std, decimal=4)
44 | assert_almost_equal(qext, qext_std, decimal=4)
45 | assert_almost_equal(g, g_std, decimal=4)
46 |
47 | # test to make sure calc_cross_sections returns the same values as
48 | # calc_efficiencies and calc_g
49 | cscat = qscat * np.pi * radius**2
50 | cext = qext * np.pi * radius**2
51 | cback = qback * np.pi * radius**2
52 | cscat2, cext2, _, cback2, g2 = mie.calc_cross_sections(m, x, wavelen/n_matrix)
53 | assert_almost_equal(cscat.to('m^2').magnitude, cscat2.to('m^2').magnitude)
54 | assert_almost_equal(cext.to('m^2').magnitude, cext2.to('m^2').magnitude)
55 | assert_almost_equal(cback.to('m^2').magnitude, cback2.to('m^2').magnitude)
56 | assert_almost_equal(g, g2.magnitude)
57 |
58 | # test that calc_cross_sections throws an exception when given an argument
59 | # with the wrong dimensions
60 | raises(DimensionalityError, mie.calc_cross_sections,
61 | m, x, Quantity('0.25 J'))
62 | raises(DimensionalityError, mie.calc_cross_sections,
63 | m, x, Quantity('0.25'))
64 |
65 | def test_form_factor():
66 | wavelen = Quantity('658.0 nm')
67 | radius = Quantity('0.85 um')
68 | n_matrix = Quantity(1.00, '')
69 | n_particle = Quantity(1.59 + 1e-4 * 1.0j, '')
70 | m = index_ratio(n_particle, n_matrix)
71 | x = size_parameter(wavelen, n_matrix, radius)
72 |
73 | angles = Quantity(np.linspace(0, 180., 19), 'deg')
74 | # these values are calculated from MiePlot
75 | # (http://www.philiplaven.com/mieplot.htm), which uses BHMIE
76 | iperp_bhmie = np.array([2046.60203864487, 1282.28646423634, 299.631502275208,
77 | 7.35748912156671, 47.4215270799552, 51.2437259188946,
78 | 1.48683515673452, 32.7216414263307, 1.4640166361956,
79 | 10.1634538431238, 4.13729254895905, 0.287316587318158,
80 | 5.1922111829055, 5.26386476102605, 1.72503962851391,
81 | 7.26013963969779, 0.918926070270738, 31.5250813730405,
82 | 93.5508557840006])
83 | ipar_bhmie = np.array([2046.60203864487, 1100.18673543798, 183.162880455348,
84 | 13.5427093640281, 57.6244243689505, 35.4490544770251,
85 | 41.0597781235887, 14.8954859951121, 34.7035437764261,
86 | 5.94544441735711, 22.1248452485893, 3.75590232882822,
87 | 10.6385606309297, 0.881297551245856, 16.2259629218812,
88 | 7.24176462105438, 76.2910238480798, 54.1983836607738,
89 | 93.5508557840006])
90 |
91 | ipar, iperp = mie.calc_ang_dist(m, x, angles)
92 | assert_array_almost_equal(ipar, ipar_bhmie)
93 | assert_array_almost_equal(iperp, iperp_bhmie)
94 |
95 | def test_efficiencies():
96 | x = np.array([0.01, 0.01778279, 0.03162278, 0.05623413, 0.1, 0.17782794,
97 | 0.31622777, 0.56234133, 1, 1.77827941, 3.16227766, 5.62341325,
98 | 10, 17.7827941, 31.6227766, 56.23413252, 100, 177.827941,
99 | 316.22776602, 562.34132519, 1000])
100 | # these values are calculated from MiePlot
101 | # (http://www.philiplaven.com/mieplot.htm), which uses BHMIE
102 | qext_bhmie = np.array([1.86E-06, 3.34E-06, 6.19E-06, 1.35E-05, 4.91E-05,
103 | 3.39E-04, 3.14E-03, 3.15E-02, 0.2972833954,
104 | 1.9411047797, 4.0883764682, 2.4192037463, 2.5962875796,
105 | 2.097410246, 2.1947770304, 2.1470056626, 2.1527225028,
106 | 2.0380806126, 2.0334715395, 2.0308028599, 2.0248011731])
107 | qsca_bhmie = np.array([3.04E-09, 3.04E-08, 3.04E-07, 3.04E-06, 3.04E-05,
108 | 3.05E-04, 3.08E-03, 3.13E-02, 0.2969918262,
109 | 1.9401873562, 4.0865768252, 2.4153820014,
110 | 2.5912825599, 2.0891233123, 2.1818510296,
111 | 2.1221614258, 2.1131226379, 1.9736114111,
112 | 1.922984002, 1.8490112847, 1.7303694187])
113 | qback_bhmie = np.array([3.62498741762823E-10, 3.62471372652178E-09,
114 | 3.623847844672E-08, 3.62110791613906E-07,
115 | 3.61242786911475E-06, 3.58482008581018E-05,
116 | 3.49577114878315E-04, 3.19256234186963E-03,
117 | 0.019955229811329, 1.22543944129328E-02,
118 | 0.114985907473273, 0.587724020116958,
119 | 0.780839362788633, 0.17952369257935,
120 | 0.068204471161473, 0.314128510891842,
121 | 0.256455963161882, 3.84713481428992E-02,
122 | 1.02022022710453, 0.51835427781473,
123 | 0.331000402174976])
124 |
125 | wavelen = Quantity('658.0 nm')
126 | n_matrix = Quantity(1.00, '')
127 | n_particle = Quantity(1.59 + 1e-4 * 1.0j, '')
128 | m = index_ratio(n_particle, n_matrix)
129 |
130 | effs = [mie.calc_efficiencies(m, x) for x in x]
131 | q_arr = np.asarray(effs)
132 | qsca = q_arr[:,0]
133 | qext = q_arr[:,1]
134 | qback = q_arr[:,2]
135 | # use two decimal places for the small size parameters because MiePlot
136 | # doesn't report sufficient precision
137 | assert_array_almost_equal(qsca[0:9], qsca_bhmie[0:9], decimal=2)
138 | assert_array_almost_equal(qext[0:9], qext_bhmie[0:9], decimal=2)
139 | # there is some disagreement at 4 decimal places in the cross
140 | # sections at large x. Not sure if this points to a bug in the algorithm
141 | # or improved precision over the bhmie results. Should be investigated
142 | # more.
143 | assert_array_almost_equal(qsca[9:], qsca_bhmie[9:], decimal=3)
144 | assert_array_almost_equal(qext[9:], qext_bhmie[9:], decimal=3)
145 |
146 | # test backscattering efficiencies (still some discrepancies at 3rd decimal
147 | # point for large size parameters)
148 | assert_array_almost_equal(qback, qback_bhmie, decimal=2)
149 |
150 | def test_absorbing_materials():
151 | # test calculations for gold, which has a high imaginary refractive index
152 | wavelen = Quantity('658.0 nm')
153 | n_matrix = Quantity(1.00, '')
154 | n_particle = Quantity(0.1425812 + 3.6813284 * 1.0j, '')
155 | m = index_ratio(n_particle, n_matrix)
156 | x = 10.0
157 |
158 | angles = Quantity(np.linspace(0, 90., 10), 'deg')
159 | # these values are calculated from MiePlot
160 | # (http://www.philiplaven.com/mieplot.htm), which uses BHMIE
161 | iperp_bhmie = np.array([4830.51401095968, 2002.39671236719,
162 | 73.6230330613015, 118.676685975947,
163 | 38.348829860926, 46.0044258298926,
164 | 31.3142368857685, 31.3709239005213,
165 | 27.8720309121251, 27.1204995833711])
166 | ipar_bhmie = np.array([4830.51401095968, 1225.28102200945,
167 | 216.265206462472, 17.0794942389782,
168 | 91.4145998381414, 39.0790253214751,
169 | 24.9801217735053, 53.2319915708624,
170 | 8.26505988320951, 47.4736966179677])
171 |
172 | ipar, iperp = mie.calc_ang_dist(m, x, angles)
173 | assert_array_almost_equal(ipar, ipar_bhmie)
174 | assert_array_almost_equal(iperp, iperp_bhmie)
175 |
176 |
--------------------------------------------------------------------------------
/structcol/tests/test_montecarlo.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan, Victoria Hwang, Solomon Barkley,
2 | # Annie Stephenson
3 | #
4 | # This file is part of the structural-color python package.
5 | #
6 | # This package is free software: you can redistribute it and/or modify it under
7 | # the terms of the GNU General Public License as published by the Free Software
8 | # Foundation, either version 3 of the License, or (at your option) any later
9 | # version.
10 | #
11 | # This package is distributed in the hope that it will be useful, but WITHOUT
12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14 | # details.
15 | #
16 | # You should have received a copy of the GNU General Public License along with
17 | # this package. If not, see .
18 | """
19 | Tests for the montecarlo model (in structcol/montecarlo.py)
20 |
21 | .. moduleauthor:: Victoria Hwang
22 | .. moduleauthor:: Solomon Barkley
23 | .. moduleauthor:: Annie Stephenson
24 | .. moduleauthor:: Vinothan N. Manoharan
25 | """
26 |
27 | import structcol as sc
28 | from .. import montecarlo as mc
29 | from .. import refractive_index as ri
30 | from .. import index_ratio, size_parameter, model
31 | import numpy as np
32 | from numpy.testing import assert_equal, assert_almost_equal
33 |
34 | # Define a system to be used for the tests
35 | nevents = 3
36 | ntrajectories = 4
37 | radius = sc.Quantity('150.0 nm')
38 | volume_fraction = 0.5
39 | n_particle = sc.Quantity(1.5, '')
40 | n_matrix = sc.Quantity(1.0, '')
41 | n_medium = sc.Quantity(1.0, '')
42 | n_sample = ri.n_eff(n_particle, n_matrix, volume_fraction)
43 | angles = sc.Quantity(np.linspace(0.01, np.pi, 200), 'rad')
44 | wavelen = sc.Quantity('400.0 nm')
45 |
46 | # Index of the scattering event and trajectory corresponding to the reflected
47 | # photons
48 | refl_index = np.array([2, 0, 2])
49 |
50 |
51 | def test_sampling():
52 | # Test that 'calc_scat' runs. Since this test just looks to see whether
53 | # sampling angles and steps works, it's better if we don't give it a seeded
54 | # random number generator, so that we can ensure that sampling works with
55 | # the default generator.
56 | p, mu_scat, mu_abs = mc.calc_scat(radius, n_particle, n_sample,
57 | volume_fraction, wavelen)
58 |
59 | # Test that 'sample_angles' runs
60 | mc.sample_angles(nevents, ntrajectories, p)
61 |
62 | # Test that 'sample_step' runs
63 | mc.sample_step(nevents, ntrajectories, mu_scat)
64 |
65 |
66 | def test_trajectories():
67 | # Initialize runs
68 | nevents = 2
69 | ntrajectories = 3
70 | r0, k0, W0 = mc.initialize(nevents, ntrajectories, n_medium, n_sample,
71 | 'film')
72 | r0 = sc.Quantity(r0, 'um')
73 | k0 = sc.Quantity(k0, '')
74 | W0 = sc.Quantity(W0, '')
75 |
76 | # Create a Trajectory object
77 | trajectories = mc.Trajectory(r0, k0, W0)
78 |
79 | # Test the absorb function
80 | mu_abs = 1/sc.Quantity(10.0, 'um')
81 | step = sc.Quantity(np.array([[1, 1, 1], [1, 1, 1]]), 'um')
82 | trajectories.absorb(mu_abs, step)
83 | # since step size is given (not sampled), this test should produce a
84 | # deterministic result
85 | assert_almost_equal(trajectories.weight.magnitude,
86 | np.array([[ 0.90483742, 0.90483742, 0.90483742],
87 | [ 0.81873075, 0.81873075, 0.81873075]]))
88 |
89 | # Make up some test theta and phi
90 | sintheta = np.array([[0., 0., 0.], [0., 0., 0.]])
91 | costheta = np.array([[-1., -1., -1.], [1., 1., 1.]])
92 | sinphi = np.array([[0., 0., 0.], [0., 0., 0.]])
93 | cosphi = np.array([[0., 0., 0.], [0., 0., 0.]])
94 |
95 | # Test the scatter function. Should also produce a deterministic result
96 | trajectories.scatter(sintheta, costheta, sinphi, cosphi)
97 |
98 | # Expected propagation directions
99 | kx = sc.Quantity(np.array([[0., 0., 0.], [0., 0., 0.]]), '')
100 | ky = sc.Quantity(np.array([[0., 0., 0.], [0., 0., 0.]]), '')
101 | kz = sc.Quantity(np.array([[1., 1., 1.], [-1., -1., -1.]]), '')
102 |
103 | assert_equal(trajectories.direction[0].magnitude, kx.magnitude)
104 | assert_equal(trajectories.direction[1].magnitude, ky.magnitude)
105 | assert_equal(trajectories.direction[2].magnitude, kz.magnitude)
106 |
107 | # Test the move function. Should also produce a deterministic result since
108 | # step sizes are given.
109 | trajectories.move(step)
110 | assert_equal(trajectories.position[2].magnitude, np.array([[0, 0, 0],
111 | [1, 1, 1],
112 | [0, 0, 0]]))
113 |
114 |
115 | def test_phase_function_absorbing_medium():
116 | # test that the phase function using the far-field Mie solutions
117 | # (mie.calc_ang_dist()) in an absorbing medium is the same as the phase
118 | # function using the Mie solutions with the asymptotic form of the
119 | # spherical Hankel functions but using a complex k
120 | # (mie.diff_scat_intensity_complex_medium() with near_fields=False)
121 | wavelen = sc.Quantity('550.0 nm')
122 | radius = sc.Quantity('105.0 nm')
123 | n_matrix = sc.Quantity(1.47 + 0.001j, '')
124 | n_particle = sc.Quantity(1.5 + 1e-1 * 1.0j, '')
125 | m = index_ratio(n_particle, n_matrix)
126 | x = size_parameter(wavelen, n_matrix, radius)
127 | k = 2 * np.pi * n_matrix / wavelen
128 | ksquared = np.abs(k)**2
129 |
130 | ## Integrating at the surface of the particle
131 | # with mie.calc_ang_dist() (this is how it's currently implemented in
132 | # monte carlo)
133 | diff_cscat_par_ff, diff_cscat_perp_ff = \
134 | model.differential_cross_section(m, x, angles, volume_fraction,
135 | structure_type='glass',
136 | form_type='sphere',
137 | diameters=radius, wavelen=wavelen,
138 | n_matrix=n_sample, k=None, distance=radius)
139 | cscat_total_par_ff = model._integrate_cross_section(diff_cscat_par_ff,
140 | 1.0/ksquared, angles)
141 | cscat_total_perp_ff = model._integrate_cross_section(diff_cscat_perp_ff,
142 | 1.0/ksquared, angles)
143 | cscat_total_ff = (cscat_total_par_ff + cscat_total_perp_ff)/2.0
144 |
145 | p_ff = (diff_cscat_par_ff + diff_cscat_perp_ff)/(ksquared * 2 * cscat_total_ff)
146 | p_par_ff = diff_cscat_par_ff/(ksquared * 2 * cscat_total_par_ff)
147 | p_perp_ff = diff_cscat_perp_ff/(ksquared * 2 * cscat_total_perp_ff)
148 |
149 | # with mie.diff_scat_intensity_complex_medium()
150 | diff_cscat_par, diff_cscat_perp = \
151 | model.differential_cross_section(m, x, angles, volume_fraction,
152 | structure_type='glass',
153 | form_type='sphere',
154 | diameters=radius, wavelen=wavelen,
155 | n_matrix=n_sample, k=k, distance=radius)
156 | cscat_total_par = model._integrate_cross_section(diff_cscat_par,
157 | 1.0/ksquared, angles)
158 | cscat_total_perp = model._integrate_cross_section(diff_cscat_perp,
159 | 1.0/ksquared, angles)
160 | cscat_total = (cscat_total_par + cscat_total_perp)/2.0
161 |
162 | p = (diff_cscat_par + diff_cscat_perp)/(ksquared * 2 * cscat_total)
163 | p_par = diff_cscat_par/(ksquared * 2 * cscat_total_par)
164 | p_perp = diff_cscat_perp/(ksquared * 2 * cscat_total_perp)
165 |
166 | # test random values of the phase functions
167 | assert_almost_equal(p_ff[3].magnitude, p[3].magnitude, decimal=15)
168 | assert_almost_equal(p_par_ff[50].magnitude, p_par[50].magnitude, decimal=15)
169 | assert_almost_equal(p_perp[83].magnitude, p_perp_ff[83].magnitude, decimal=15)
170 |
171 | ### Same thing but with a binary and polydisperse mixture
172 | ## Integrating at the surface of the particle
173 | # with mie.calc_ang_dist() (this is how it's currently implemented in
174 | # monte carlo)
175 | radius2 = sc.Quantity('150.0 nm')
176 | concentration = sc.Quantity(np.array([0.2, 0.7]), '')
177 | pdi = sc.Quantity(np.array([0.1, 0.1]), '')
178 | diameters = sc.Quantity(np.array([radius.magnitude, radius2.magnitude])*2,
179 | radius.units)
180 |
181 | diff_cscat_par_ff, diff_cscat_perp_ff = \
182 | model.differential_cross_section(m, x, angles, volume_fraction,
183 | structure_type='polydisperse',
184 | form_type='polydisperse',
185 | diameters=diameters, pdi=pdi,
186 | concentration=concentration,
187 | wavelen=wavelen,
188 | n_matrix=n_sample, k=None,
189 | distance=diameters/2)
190 | cscat_total_par_ff = model._integrate_cross_section(diff_cscat_par_ff,
191 | 1.0/ksquared, angles)
192 | cscat_total_perp_ff = model._integrate_cross_section(diff_cscat_perp_ff,
193 | 1.0/ksquared, angles)
194 | cscat_total_ff = (cscat_total_par_ff + cscat_total_perp_ff)/2.0
195 |
196 | p_ff2 = (diff_cscat_par_ff + diff_cscat_perp_ff)/(ksquared * 2 * cscat_total_ff)
197 | p_par_ff2 = diff_cscat_par_ff/(ksquared * 2 * cscat_total_par_ff)
198 | p_perp_ff2 = diff_cscat_perp_ff/(ksquared * 2 * cscat_total_perp_ff)
199 |
200 | # with mie.diff_scat_intensity_complex_medium()
201 | diff_cscat_par, diff_cscat_perp = \
202 | model.differential_cross_section(m, x, angles, volume_fraction,
203 | structure_type='polydisperse',
204 | form_type='polydisperse',
205 | diameters=diameters, pdi=pdi,
206 | concentration=concentration,
207 | wavelen=wavelen,
208 | n_matrix=n_sample, k=k,
209 | distance=diameters/2)
210 | cscat_total_par = model._integrate_cross_section(diff_cscat_par,
211 | 1.0/ksquared, angles)
212 | cscat_total_perp = model._integrate_cross_section(diff_cscat_perp,
213 | 1.0/ksquared, angles)
214 | cscat_total = (cscat_total_par + cscat_total_perp)/2.0
215 |
216 | p2 = (diff_cscat_par + diff_cscat_perp)/(ksquared * 2 * cscat_total)
217 | p_par2 = diff_cscat_par/(ksquared * 2 * cscat_total_par)
218 | p_perp2 = diff_cscat_perp/(ksquared * 2 * cscat_total_perp)
219 |
220 | # test random values of the phase functions
221 | assert_almost_equal(p_ff2[3].magnitude, p2[3].magnitude, decimal=15)
222 | assert_almost_equal(p_par_ff2[50].magnitude, p_par2[50].magnitude,
223 | decimal=15)
224 | assert_almost_equal(p_perp2[83].magnitude, p_perp_ff2[83].magnitude,
225 | decimal=15)
226 |
--------------------------------------------------------------------------------
/structcol/tests/test_montecarlo_bulk.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan, Annie Stephenson
2 | #
3 | # This file is part of the structural-color python package.
4 | #
5 | # This package is free software: you can redistribute it and/or modify it under
6 | # the terms of the GNU General Public License as published by the Free Software
7 | # Foundation, either version 3 of the License, or (at your option) any later
8 | # version.
9 | #
10 | # This package is distributed in the hope that it will be useful, but WITHOUT
11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 | # details.
14 | #
15 | # You should have received a copy of the GNU General Public License along with
16 | # this package. If not, see .
17 | """
18 | Tests for the montecarlo bulk model
19 |
20 | .. moduleauthor:: Anna B. Stephenson
21 | .. moduleauthor:: Vinothan N. Manoharan
22 | """
23 |
24 | import numpy as np
25 | import structcol as sc
26 | import structcol.refractive_index as ri
27 | from structcol import montecarlo as mc
28 | from structcol import detector as det
29 | from structcol import phase_func_sphere as pfs
30 | from numpy.testing import assert_almost_equal, assert_warns
31 | import pytest
32 |
33 | ### Set parameters ###
34 |
35 | # Properties of source
36 | wavelength = sc.Quantity('600.0 nm') # wavelengths at which to calculate reflectance
37 |
38 | # Geometric properties of sample
39 | #
40 | # radius of the sphere particles
41 | particle_radius = sc.Quantity('0.130 um')
42 | # volume fraction of the particles in the sphere boundary
43 | volume_fraction_particles = sc.Quantity(0.6, '')
44 | # volume fraction of the spheres in the bulk film
45 | volume_fraction_bulk = sc.Quantity(0.55,'')
46 | # diameter of the sphere boundary
47 | sphere_boundary_diameter = sc.Quantity(10.0,'um')
48 | boundary = 'sphere'
49 | boundary_bulk = 'film'
50 |
51 | # Refractive indices
52 | #
53 | # refractive index of particle
54 | n_particle = ri.n('vacuum', wavelength)
55 | # refractive index of matrix
56 | n_matrix = ri.n('polystyrene', wavelength)
57 | # refractive index of the bulk matrix
58 | n_matrix_bulk = ri.n('vacuum', wavelength)
59 | # refractive index of medium outside the bulk sample.
60 | n_medium = ri.n('vacuum', wavelength)
61 |
62 | # Monte Carlo parameters
63 | #
64 | # number of trajectories to run with a spherical boundary
65 | ntrajectories = 2000
66 | # number of scattering events for each trajectory in a spherical boundary
67 | nevents = 300
68 |
69 |
70 | def calc_sphere_mc():
71 | # set up a seeded random number generator that will give consistent results
72 | # between numpy versions.
73 | seed = 1
74 | rng = np.random.RandomState([seed])
75 |
76 |
77 | # caculate the effective index of the sample
78 | n_sample = ri.n_eff(n_particle, n_matrix, volume_fraction_particles)
79 |
80 | # Calculate the phase function and scattering and absorption coefficients
81 | #from the single scattering model
82 | # (this absorption coefficient is of the scatterer, not of an absorber
83 | #added to the system)
84 | p, mu_scat, mu_abs = mc.calc_scat(particle_radius, n_particle, n_sample,
85 | volume_fraction_particles, wavelength)
86 |
87 | # Initialize the trajectories
88 | r0, k0, W0 = mc.initialize(nevents, ntrajectories, n_matrix_bulk, n_sample,
89 | boundary,
90 | sample_diameter = sphere_boundary_diameter,
91 | rng=rng)
92 | r0 = sc.Quantity(r0, 'um')
93 | k0 = sc.Quantity(k0, '')
94 | W0 = sc.Quantity(W0, '')
95 |
96 | # Create trajectories object
97 | trajectories = mc.Trajectory(r0, k0, W0)
98 |
99 | # Generate a matrix of all the randomly sampled angles first
100 | sintheta, costheta, sinphi, cosphi, _, _ = mc.sample_angles(nevents,
101 | ntrajectories,
102 | p, rng=rng)
103 |
104 | # Create step size distribution
105 | step = mc.sample_step(nevents, ntrajectories, mu_scat, rng=rng)
106 |
107 | # Run photons
108 | trajectories.absorb(mu_abs, step)
109 | trajectories.scatter(sintheta, costheta, sinphi, cosphi)
110 | trajectories.move(step)
111 |
112 | # Calculate reflection and transmission
113 | # (should raise warning that n_matrix and n_particle are not set, so
114 | # tir correction is based only on sample index)
115 | with pytest.warns(UserWarning):
116 | (refl_indices,
117 | trans_indices,
118 | _, _, _,
119 | refl_per_traj, trans_per_traj,
120 | _,_,_,_,
121 | reflectance_sphere,
122 | _,_,
123 | norm_refl, norm_trans) = det.calc_refl_trans(trajectories,
124 | sphere_boundary_diameter,
125 | n_matrix_bulk, n_sample,
126 | boundary, p=p,
127 | mu_abs=mu_abs,
128 | mu_scat=mu_scat,
129 | run_fresnel_traj = False,
130 | return_extra = True)
131 |
132 | return (refl_indices, trans_indices, refl_per_traj, trans_per_traj,
133 | reflectance_sphere, norm_refl, norm_trans)
134 |
135 | ### Calculate phase function and lscat ###
136 | # use output of calc_refl_trans to calculate phase function, mu_scat,
137 | # and mu_abs for the bulk
138 | p_bulk, mu_scat_bulk, mu_abs_bulk = pfs.calc_scat_bulk(refl_per_traj,
139 | trans_per_traj,
140 | trans_indices,
141 | norm_refl, norm_trans,
142 | volume_fraction_bulk,
143 | sphere_boundary_diameter,
144 | n_matrix_bulk,
145 | wavelength)
146 | return p_bulk, mu_scat_bulk, mu_abs_bulk
147 |
148 | def test_mu_scat_abs_bulk():
149 |
150 | # make sure there is no absorption when all refractive indices are real
151 | (refl_indices, trans_indices,
152 | refl_per_traj, trans_per_traj,
153 | reflectance_sphere,
154 | norm_refl, norm_trans) = calc_sphere_mc()
155 |
156 |
157 | _, _, mu_abs_bulk = pfs.calc_scat_bulk(refl_per_traj,
158 | trans_per_traj,
159 | refl_indices,
160 | trans_indices,
161 | norm_refl, norm_trans,
162 | volume_fraction_bulk,
163 | sphere_boundary_diameter,
164 | n_matrix_bulk,
165 | wavelength)
166 |
167 | assert_almost_equal(mu_abs_bulk.magnitude, 0)
168 |
169 |
170 | # make sure mu_abs reaches limit when there is no scattering
171 | with assert_warns(UserWarning):
172 | _, mu_scat_bulk, mu_abs_bulk = pfs.calc_scat_bulk(np.zeros((ntrajectories)),
173 | np.zeros((ntrajectories)),
174 | refl_indices,
175 | trans_indices,
176 | norm_refl, norm_trans,
177 | volume_fraction_bulk,
178 | sphere_boundary_diameter,
179 | n_matrix_bulk,
180 | wavelength)
181 |
182 | number_density = volume_fraction_bulk.magnitude/(4/3*np.pi*
183 | (sphere_boundary_diameter.magnitude/2)**3)
184 | mu_abs_max = number_density*np.pi*(sphere_boundary_diameter.magnitude/2)**2
185 |
186 | assert_almost_equal(mu_abs_bulk.magnitude, mu_abs_max)
187 |
188 | # check that mu_scat_bulk is 0 when no scattering
189 | assert_almost_equal(mu_scat_bulk.magnitude, 0)
190 |
191 |
192 | # check the mu_scat_bulk reaches limit when there is only scattering
193 | norm_refl[2,:]= 1/np.sqrt(3)
194 | norm_refl[1,:]= 1/np.sqrt(3)
195 | norm_refl[0,:]= 1/np.sqrt(3)
196 | norm_trans[2,:]= 0
197 | norm_trans[1,:]= 0
198 | norm_trans[0,:]= 0
199 |
200 | _, mu_scat_bulk, _ = pfs.calc_scat_bulk(1/ntrajectories*np.ones((ntrajectories)), # refl_per_traj
201 | np.zeros((ntrajectories)), # trans_per_traj
202 | np.ones(ntrajectories)+3, # refl_indices
203 | np.zeros(ntrajectories), # trans_indices
204 | norm_refl, norm_trans,
205 | volume_fraction_bulk,
206 | sphere_boundary_diameter,
207 | n_matrix_bulk,
208 | wavelength)
209 |
210 | number_density = volume_fraction_bulk.magnitude/(4/3*np.pi*
211 | (sphere_boundary_diameter.magnitude/2)**3)
212 | mu_scat_max = number_density*2*np.pi*(sphere_boundary_diameter.magnitude/2)**2
213 |
214 | assert_almost_equal(mu_scat_bulk.magnitude, mu_scat_max)
215 |
--------------------------------------------------------------------------------
/structcol/tests/test_montecarlo_sphere.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018, Vinothan N. Manoharan, Annie Stephenson, Victoria Hwang,
2 | # Solomon Barkley
3 | #
4 | # This file is part of the structural-color python package.
5 | #
6 | # This package is free software: you can redistribute it and/or modify it under
7 | # the terms of the GNU General Public License as published by the Free Software
8 | # Foundation, either version 3 of the License, or (at your option) any later
9 | # version.
10 | #
11 | # This package is distributed in the hope that it will be useful, but WITHOUT
12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14 | # details.
15 | #
16 | # You should have received a copy of the GNU General Public License along with
17 | # this package. If not, see .
18 | """
19 | Tests for the montecarlo model for sphere geometry (in structcol/montecarlo.py)
20 | .. moduleauthor:: Annie Stephenson
21 | .. moduleauthor:: Victoria Hwang
22 | .. moduleathor:: Solomon Barkley
23 | .. moduleauthor:: Vinothan N. Manoharan
24 | """
25 |
26 | import structcol as sc
27 | from .. import montecarlo as mc
28 | from .. import refractive_index as ri
29 | import numpy as np
30 |
31 | # Define a system to be used for the tests
32 | nevents = 3
33 | ntrajectories = 4
34 | radius = sc.Quantity('150.0 nm')
35 | assembly_radius = 5
36 | volume_fraction = 0.5
37 | n_particle = sc.Quantity(1.5, '')
38 | n_matrix = sc.Quantity(1.0, '')
39 | n_sample = ri.n_eff(n_particle, n_matrix, volume_fraction)
40 | angles = sc.Quantity(np.linspace(0.01,np.pi, 200), 'rad')
41 | wavelen = sc.Quantity('400.0 nm')
42 |
43 | # Index of the scattering event and trajectory corresponding to the reflected
44 | # photons
45 | refl_index = np.array([2,0,2])
46 |
47 | def test_trajectories():
48 | # Initialize runs. Since this test just checks to make sure a trajectory
49 | # object can be created, we don't need to give it a seeded random number
50 | # generator.
51 | nevents = 2
52 | ntrajectories = 3
53 | r0, k0, W0 = mc.initialize(nevents, ntrajectories, n_matrix, n_sample,
54 | 'sphere', sample_diameter=sc.Quantity('1.0 um'))
55 | r0 = sc.Quantity(r0, 'um')
56 | k0 = sc.Quantity(k0, '')
57 | W0 = sc.Quantity(W0, '')
58 |
59 | # Create a Trajectory object
60 | trajectories = mc.Trajectory(r0, k0, W0)
61 |
--------------------------------------------------------------------------------
/structcol/tests/test_refractive_index.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan, Victoria Hwang
2 | #
3 | # This file is part of the structural-color python package.
4 | #
5 | # This package is free software: you can redistribute it and/or modify it under
6 | # the terms of the GNU General Public License as published by the Free Software
7 | # Foundation, either version 3 of the License, or (at your option) any later
8 | # version.
9 | #
10 | # This package is distributed in the hope that it will be useful, but WITHOUT
11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 | # details.
14 | #
15 | # You should have received a copy of the GNU General Public License along with
16 | # this package. If not, see .
17 | """
18 | Tests for the refractive_index module of structcol
19 |
20 | .. moduleauthor:: Vinothan N. Manoharan
21 | .. moduleauthor:: Victoria Hwang
22 | """
23 |
24 | from .. import refractive_index as ri
25 | from .. import Quantity
26 | from numpy.testing import assert_equal, assert_almost_equal, assert_warns
27 | from pytest import raises
28 | from pint.errors import DimensionalityError
29 | import numpy as np
30 |
31 | def test_n():
32 | # make sure that a material not in the dictionary raises a KeyError
33 | raises(KeyError, ri.n, 'badkey', Quantity('0.5 um'))
34 |
35 | # make sure that specifying no units throws an exception
36 | raises(DimensionalityError, ri.n, 'polystyrene', 0.5)
37 |
38 | # and specifying the wrong units, too
39 | raises(DimensionalityError, ri.n, 'polystyrene', Quantity('0.5 J'))
40 |
41 | # the next few tests make sure that the various dispersion formulas give values
42 | # of n close to those listed by refractiveindex.info (or other source) at the
43 | # boundaries of the visible spectrum. This is mostly to make sure that the
44 | # coefficients of the dispersion formulas are entered properly
45 |
46 | def test_water():
47 | # values from refractiveindex.info
48 | assert_almost_equal(ri.n('water', Quantity('0.40930 um')).magnitude,
49 | Quantity('1.3427061376724').magnitude)
50 | assert_almost_equal(ri.n('water', Quantity('0.80700 um')).magnitude,
51 | Quantity('1.3284883366632').magnitude)
52 |
53 | def test_npmma():
54 | # values from refractiveindex.info
55 | assert_almost_equal(ri.n('pmma', Quantity('0.42 um')).magnitude,
56 | Quantity('1.5049521933717').magnitude)
57 | assert_almost_equal(ri.n('pmma', Quantity('0.804 um')).magnitude,
58 | Quantity('1.4866523830528').magnitude)
59 |
60 | def test_nps():
61 | # values from refractiveindex.info
62 | assert_almost_equal(ri.n('polystyrene', Quantity('0.4491 um')).magnitude,
63 | Quantity('1.6137854760669').magnitude)
64 | assert_almost_equal(ri.n('polystyrene', Quantity('0.7998 um')).magnitude,
65 | Quantity('1.5781660671827').magnitude)
66 |
67 | def test_rutile():
68 | # values from refractiveindex.info
69 | assert_almost_equal(ri.n('rutile', Quantity('0.4300 um')).magnitude,
70 | Quantity('2.8716984534676').magnitude)
71 | assert_almost_equal(ri.n('rutile', Quantity('0.8040 um')).magnitude,
72 | Quantity('2.5187663081355').magnitude)
73 |
74 | def test_fused_silica():
75 | # values from refractiveindex.info
76 | assert_almost_equal(ri.n('fused silica', Quantity('0.3850 um')).magnitude,
77 | Quantity('1.4718556531995').magnitude)
78 | assert_almost_equal(ri.n('fused silica', Quantity('0.8050 um')).magnitude,
79 | Quantity('1.4532313266004').magnitude)
80 | def test_zirconia():
81 | # values from refractiveindex.info
82 | assert_almost_equal(ri.n('zirconia', Quantity('.405 um')).magnitude,
83 | Quantity('2.3135169070958').magnitude)
84 | assert_almost_equal(ri.n('zirconia', Quantity('.6350 um')).magnitude,
85 | Quantity('2.1593242574339').magnitude)
86 |
87 |
88 | def test_vacuum():
89 | assert_almost_equal(ri.n('vacuum', Quantity('0.400 um')).magnitude, Quantity('1.0').magnitude)
90 | assert_almost_equal(ri.n('vacuum', Quantity('0.800 um')).magnitude, Quantity('1.0').magnitude)
91 |
92 | def test_cargille():
93 | assert_almost_equal(ri.n_cargille(1,'AAA',Quantity('0.400 um')).magnitude,
94 | Quantity('1.3101597437500001').magnitude)
95 | assert_almost_equal(ri.n_cargille(1,'AAA',Quantity('0.700 um')).magnitude,
96 | Quantity('1.303526242857143').magnitude)
97 | assert_almost_equal(ri.n_cargille(1,'AA',Quantity('0.400 um')).magnitude,
98 | Quantity('1.4169400062500002').magnitude)
99 | assert_almost_equal(ri.n_cargille(1,'AA',Quantity('0.700 um')).magnitude,
100 | Quantity('1.3987172673469388').magnitude)
101 | assert_almost_equal(ri.n_cargille(1,'A',Quantity('0.400 um')).magnitude,
102 | Quantity('1.4755715625000001').magnitude)
103 | assert_almost_equal(ri.n_cargille(1,'A',Quantity('0.700 um')).magnitude,
104 | Quantity('1.458145836734694').magnitude)
105 | assert_almost_equal(ri.n_cargille(1,'B',Quantity('0.400 um')).magnitude,
106 | Quantity('1.6720350625').magnitude)
107 | assert_almost_equal(ri.n_cargille(1,'B',Quantity('0.700 um')).magnitude,
108 | Quantity('1.6283854489795917').magnitude)
109 | assert_almost_equal(ri.n_cargille(1,'E',Quantity('0.400 um')).magnitude,
110 | Quantity('1.5190772875').magnitude)
111 | assert_almost_equal(ri.n_cargille(1,'E',Quantity('0.700 um')).magnitude,
112 | Quantity('1.4945156653061225').magnitude)
113 | assert_almost_equal(ri.n_cargille(0,'acrylic',Quantity('0.400 um')).magnitude,
114 | Quantity('1.50736788125').magnitude)
115 | assert_almost_equal(ri.n_cargille(0,'acrylic',Quantity('0.700 um')).magnitude,
116 | Quantity('1.4878716959183673').magnitude)
117 |
118 | def test_neff():
119 | # test that at low volume fractions, Maxwell-Garnett and Bruggeman roughly
120 | # match for a non-core-shell particle
121 | n_particle = Quantity(2.7, '')
122 | n_matrix = Quantity(2.2, '')
123 | vf = Quantity(0.001, '')
124 |
125 | neff_mg = ri.n_eff(n_particle, n_matrix, vf, maxwell_garnett=True)
126 | neff_bg = ri.n_eff(n_particle, n_matrix, vf, maxwell_garnett=False)
127 |
128 | assert_almost_equal(neff_mg.magnitude, neff_bg.magnitude)
129 |
130 | # test that the non-core-shell particle with Maxwell-Garnett matches with
131 | # the core-shell of shell index of air with Bruggeman at low volume fractions
132 | n_particle2 = Quantity(np.array([2.7, 2.2]), '')
133 | vf2 = Quantity(np.array([0.001, 0.1]), '')
134 | neff_bg2 = ri.n_eff(n_particle2, n_matrix, vf2, maxwell_garnett=False)
135 |
136 | assert_almost_equal(neff_mg.magnitude, neff_bg2.magnitude)
137 | assert_almost_equal(neff_bg.magnitude, neff_bg2.magnitude)
138 |
139 | # test that the effective indices for a non-core-shell and a core-shell of
140 | # shell index of air match using Bruggeman at intermediate volume fractions
141 | vf3 = Quantity(0.5, '')
142 | neff_bg3 = ri.n_eff(n_particle, n_matrix, vf3, maxwell_garnett=False)
143 |
144 | vf3_cs = Quantity(np.array([0.5, 0.1]), '')
145 | neff_bg3_cs = ri.n_eff(n_particle2, n_matrix, vf3_cs, maxwell_garnett=False)
146 |
147 | assert_almost_equal(neff_bg3.magnitude, neff_bg3_cs.magnitude)
148 |
149 | # repeat the tests using complex indices
150 | n_particle_complex = Quantity(2.7+0.001j, '')
151 | n_matrix_complex = Quantity(2.2+0.001j, '')
152 |
153 | neff_mg_complex = ri.n_eff(n_particle_complex, n_matrix_complex, vf, maxwell_garnett=True)
154 | neff_bg_complex = ri.n_eff(n_particle_complex, n_matrix_complex, vf, maxwell_garnett=False)
155 |
156 | assert_almost_equal(neff_mg_complex.magnitude, neff_bg_complex.magnitude)
157 |
158 | # test that the non-core-shell particle with Maxwell-Garnett matches with
159 | # the core-shell of shell index of air with Bruggeman at low volume fractions
160 | n_particle2_complex = Quantity(np.array([2.7+0.001j, 2.2+0.001j]), '')
161 | neff_bg2_complex = ri.n_eff(n_particle2_complex, n_matrix_complex, vf2, maxwell_garnett=False)
162 |
163 | assert_almost_equal(neff_mg_complex.magnitude, neff_bg2_complex.magnitude)
164 | assert_almost_equal(neff_bg_complex.magnitude, neff_bg2_complex.magnitude)
165 |
166 | # test that the effective indices for a non-core-shell and a core-shell of
167 | # shell index of air match using Bruggeman at intermediate volume fractions
168 | neff_bg3_complex = ri.n_eff(n_particle_complex, n_matrix_complex, vf3, maxwell_garnett=False)
169 |
170 | neff_bg3_cs_complex = ri.n_eff(n_particle2_complex, n_matrix_complex, vf3_cs, maxwell_garnett=False)
171 |
172 | assert_almost_equal(neff_bg3_complex.magnitude, neff_bg3_cs_complex.magnitude)
173 |
174 | def test_data():
175 | # Test that we can input data for refractive index
176 | wavelength = Quantity(np.array([400.0, 500.0, 600.0]), 'nm')
177 | data = Quantity(np.array([1.5,1.55,1.6]), '')
178 | assert_equal(ri.n('data', wavelength, index_data=data, wavelength_data=wavelength).magnitude.all(), data.magnitude.all())
179 |
180 | # Test that it also works for complex values
181 | data_complex = np.array([1.5+0.01j,1.55+0.02j,1.6+0.03j])
182 | assert_equal(ri.n('data', wavelength, index_data=data, wavelength_data=wavelength).all(), data_complex.all())
183 |
184 | # Test that keyerror is raised when no index is specified for 'data'
185 | raises(KeyError, ri.n, 'data', Quantity('0.5 um'), index_data=None)
186 |
187 | # Test warning message when user specifies index for a material other than 'data'
188 | assert_warns(Warning, ri.n, 'water', Quantity('0.5 um'), index_data=data)
189 |
--------------------------------------------------------------------------------
/structcol/tests/test_structcol.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan
2 | #
3 | # This file is part of the structural-color python package.
4 | #
5 | # This package is free software: you can redistribute it and/or modify it under
6 | # the terms of the GNU General Public License as published by the Free Software
7 | # Foundation, either version 3 of the License, or (at your option) any later
8 | # version.
9 | #
10 | # This package is distributed in the hope that it will be useful, but WITHOUT
11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 | # details.
14 | #
15 | # You should have received a copy of the GNU General Public License along with
16 | # this package. If not, see .
17 | """
18 | Tests various features of the structcol package not found in submodules
19 |
20 | .. moduleauthor:: Vinothan N. Manoharan
21 | """
22 |
23 | from .. import Quantity, q, np
24 | from numpy.testing import assert_equal
25 | from pytest import raises
26 | from pint.errors import DimensionalityError
27 |
28 | def test_q():
29 | # make sure that the q function works correctly on arrays and quantities
30 | # with dimensions
31 |
32 | # test angle conversion
33 | assert_equal(q(Quantity('450 nm'), Quantity('pi/2 rad')).magnitude,
34 | q(Quantity('450 nm'), Quantity('90 degrees')).magnitude)
35 |
36 | # test to make sure function returns an array if given an array argument
37 | wavelen = Quantity(np.arange(500.0, 800.0, 10.0), 'nm')
38 | assert_equal(wavelen.shape, (30,))
39 | q_values = q(wavelen, Quantity('90 degrees'))
40 | assert_equal(q_values.shape, wavelen.shape)
41 | angle = np.transpose(Quantity(np.arange(0, 180., 1.0), 'degrees'))
42 | assert_equal(angle.shape, (180,))
43 | q_values = q(Quantity('0.5 um'), angle)
44 | assert_equal(q_values.shape, angle.shape)
45 |
46 | # test to make sure function returns a 2D array if given arrays for both
47 | # theta and wavelen
48 | q_values = q(wavelen.reshape(-1,1), angle.reshape(1,-1))
49 | assert_equal(q_values.shape, (wavelen.shape[0], angle.shape[0]))
50 |
51 | # test dimension checking
52 | raises(DimensionalityError, q, Quantity('0.5 J'), Quantity('0.5 rad'))
53 | raises(DimensionalityError, q, Quantity('450 nm'), Quantity('0.5 m'))
54 |
--------------------------------------------------------------------------------
/structcol/tests/test_structure.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016, Vinothan N. Manoharan, Victoria Hwang, Annie Stephenson
2 | #
3 | # This file is part of the structural-color python package.
4 | #
5 | # This package is free software: you can redistribute it and/or modify it under
6 | # the terms of the GNU General Public License as published by the Free Software
7 | # Foundation, either version 3 of the License, or (at your option) any later
8 | # version.
9 | #
10 | # This package is distributed in the hope that it will be useful, but WITHOUT
11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 | # details.
14 | #
15 | # You should have received a copy of the GNU General Public License along with
16 | # this package. If not, see .
17 | """
18 | Tests for the structure module
19 |
20 | .. moduleauthor:: Vinothan N. Manoharan
21 | .. moduleauthor:: Victoria Hwang
22 | .. moduleauthor:: Annie Stephenson
23 | """
24 |
25 | from .. import Quantity, np, structure
26 | from .. import size_parameter
27 | from .. import refractive_index as ri
28 | from numpy.testing import assert_equal, assert_almost_equal
29 | from pytest import raises
30 | from pint.errors import DimensionalityError
31 |
32 |
33 | def test_structure_factor_percus_yevick():
34 | # Test structure factor as calculated by solution of Ornstein-Zernike
35 | # integral equation and Percus-Yevick closure approximation
36 |
37 | # test that function handles dimensionless arguments, and only
38 | # dimensionless arguments
39 | structure.factor_py(Quantity('0.1'), Quantity('0.4'))
40 | structure.factor_py(0.1, 0.4)
41 | raises(DimensionalityError, structure.factor_py,
42 | Quantity('0.1'), Quantity('0.1 m'))
43 | raises(DimensionalityError, structure.factor_py,
44 | Quantity('0.1 m'), Quantity('0.1'))
45 |
46 | # test vectorization by doing calculation over range of qd and phi
47 | qd = np.arange(0.1, 20, 0.01)
48 | phi = np.array([0.15, 0.3, 0.45])
49 | # this little trick allows us to calculate the structure factor on a 2d
50 | # grid of points (turns qd into a column vector and phi into a row vector).
51 | # Could also use np.ogrid
52 | s = structure.factor_py(qd.reshape(-1,1), phi.reshape(1,-1))
53 |
54 | # compare to values from Cipelletti, Trappe, and Pine, "Scattering
55 | # Techniques", in "Fluids, Colloids and Soft Materials: An Introduction to
56 | # Soft Matter Physics", 2016 (plot on page 137)
57 | # (I extracted values from the plot using a digitizer
58 | # (http://arohatgi.info/WebPlotDigitizer/app/). They are probably good to
59 | # only one decimal place, so this is a fairly crude test.)
60 | max_vals = np.max(s, axis=0) # max values of S(qd) at different phi
61 | max_qds = qd[np.argmax(s, axis=0)] # values of qd at which S(qd) has max
62 | assert_almost_equal(max_vals[0], 1.17, decimal=1)
63 | assert_almost_equal(max_vals[1], 1.52, decimal=1)
64 | assert_almost_equal(max_vals[2], 2.52, decimal=1)
65 | assert_almost_equal(max_qds[0], 6.00, decimal=1)
66 | assert_almost_equal(max_qds[1], 6.37, decimal=1)
67 | assert_almost_equal(max_qds[2], 6.84, decimal=1)
68 |
69 | def test_structure_factor_percus_yevick_core_shell():
70 | # Test that the structure factor is the same for core-shell particles and
71 | # non-core-shell particles at low volume fraction (assuming the core diameter
72 | # is the same as the particle diameter for the non-core-shell case)
73 |
74 | wavelen = Quantity('400.0 nm')
75 | angles = Quantity(np.pi, 'rad')
76 | n_matrix = Quantity(1.0, '')
77 |
78 | # Structure factor for non-core-shell particles
79 | radius = Quantity('100.0 nm')
80 | n_particle = Quantity(1.5, '')
81 | volume_fraction = Quantity(0.0001, '') # IS VF TOO LOW?
82 | n_sample = ri.n_eff(n_particle, n_matrix, volume_fraction)
83 | x = size_parameter(wavelen, n_sample, radius)
84 | qd = 4*x*np.sin(angles/2)
85 | s = structure.factor_py(qd, volume_fraction)
86 |
87 | # Structure factor for core-shell particles with core size equal to radius
88 | # of non-core-shell particle
89 | radius_cs = Quantity(np.array([100.0, 105.0]), 'nm')
90 | n_particle_cs = Quantity(np.array([1.5, 1.0]), '')
91 | volume_fraction_shell = volume_fraction * (radius_cs[1]**3 / radius_cs[0]**3 -1)
92 | volume_fraction_cs = Quantity(np.array([volume_fraction.magnitude, volume_fraction_shell.magnitude]), '')
93 |
94 | n_sample_cs = ri.n_eff(n_particle_cs, n_matrix, volume_fraction_cs)
95 | x_cs = size_parameter(wavelen, n_sample_cs, radius_cs[1]).flatten()
96 | qd_cs = 4*x_cs*np.sin(angles/2)
97 | s_cs = structure.factor_py(qd_cs, np.sum(volume_fraction_cs))
98 |
99 | assert_almost_equal(s.magnitude, s_cs.magnitude, decimal=5)
100 |
101 |
102 | def test_structure_factor_polydisperse():
103 | # test that the analytical structure factor for polydisperse systems matches
104 | # Percus-Yevick in the monodisperse limit
105 |
106 | # Percus-Yevick
107 | qd = Quantity(5.0, '')
108 | phi = Quantity(0.5, '')
109 | S_py = structure.factor_py(qd, phi)
110 |
111 | # Polydisperse S
112 | d = Quantity('100.0 nm')
113 | c = Quantity(1.0, '')
114 | pdi = Quantity(1e-5, '')
115 | q2 = qd / d
116 |
117 | S_poly = structure.factor_poly(q2, phi, d, c, pdi)
118 |
119 | assert_almost_equal(S_py.magnitude, S_poly.magnitude)
120 |
121 |
122 | def test_structure_factor_data():
123 | qd = np.array([1, 2])
124 | qd_data = np.array([0.5, 2.5])
125 | s_data = np.array([1, 1])
126 | s = structure.factor_data(qd, s_data, qd_data)
127 | assert_equal(s[0], 1)
128 |
--------------------------------------------------------------------------------