├── .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 | --------------------------------------------------------------------------------