├── .gitignore ├── LICENSE ├── README.md ├── environment.yml ├── pymie ├── __init__.py ├── mie.py ├── mie_specfuncs.py ├── multilayer_sphere_lib.py └── tests │ ├── __init__.py │ ├── gold │ ├── gold_2_sphere_allow_overlap.yaml │ ├── gold_dda_csg.yaml │ ├── gold_dda_csg_rotated_div.yaml │ ├── gold_dda_voxelated_complex.yaml │ ├── gold_farfield_matricies.yaml │ ├── gold_janus_dda.yaml │ ├── gold_mie_multiple_fields.yaml │ ├── gold_mie_multiple_holo.yaml │ ├── gold_mie_radiometric.yaml │ ├── gold_mie_scat_matrix.yaml │ ├── gold_multilayer.yaml │ ├── gold_shell.yaml │ ├── gold_single_field.yaml │ ├── gold_single_holo.yaml │ ├── gold_tmatrix_cylinder.yaml │ └── gold_tmatrix_spheroid.yaml │ ├── test_mie.py │ └── test_multilayer.py ├── pyproject.toml └── setup.py /.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 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-mie 2 | Pure Python package for Mie scattering calculations. This package is used by, 3 | for example, the [structural-color python 4 | package](https://github.com/manoharan-lab/structural-color). 5 | 6 | The original code was developed by Jerome Fung in the [Manoharan Lab at Harvard 7 | University](http://manoharan.seas.harvard.edu). This research was supported by 8 | the National Science Foundation under grant numbers CBET-0747625, DMR-0820484, 9 | DMR-1306410, and DMR-1420570. The code has since been updated. It now works in 10 | Python 3, and it can handle quantities with dimensions (using 11 | [pint](https://github.com/hgrecco/pint)). 12 | -------------------------------------------------------------------------------- /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 pymie 7 | # 8 | # To update dependencies after changing this environment file: 9 | # conda env update --name pymie --file environment.yml --prune 10 | # 11 | # can also use mamba instead of conda in the above 12 | name: pymie 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 benchmarking 33 | - asv 34 | 35 | # for development 36 | - pyright 37 | - ruff 38 | -------------------------------------------------------------------------------- /pymie/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Vinothan N. Manoharan, Sofia Makgiriadou 2 | # 3 | # This file is part of the python-mie 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 python-mie (pymie) python package is a pure Python library for Mie 20 | scattering calculations 21 | 22 | Notes 23 | ----- 24 | Based on work by Jerome Fung in the Manoharan Lab at Harvard University 25 | 26 | Requires pint: 27 | PyPI: https://pypi.python.org/pypi/Pint/ 28 | Github: https://github.com/hgrecco/pint 29 | Docs: https://pint.readthedocs.io/en/latest/ 30 | 31 | .. moduleauthor :: Vinothan N. Manoharan 32 | .. moduleauthor :: Sofia Magkiriadou . 33 | """ 34 | 35 | import numpy as np 36 | from pint import UnitRegistry 37 | 38 | # Load the default unit registry from pint and use it everywhere. 39 | # Using the unit registry (and wrapping all functions) ensures that we don't 40 | # make unit mistakes 41 | ureg = UnitRegistry() 42 | Quantity = ureg.Quantity 43 | 44 | @ureg.check('[length]', '[]') 45 | def q(wavelen, theta): 46 | """ 47 | Calculates the magnitude of the momentum-transfer wavevector 48 | 49 | Parameters 50 | ---------- 51 | wavelen: structcol.Quantity [length] 52 | wavelength in vacuum 53 | theta: structcol.Quantity [dimensionless] 54 | scattering angle (polar angle with z pointing along the incident 55 | direction) 56 | 57 | Returns 58 | ------- 59 | structcol.Quantity [1/length] 60 | magnitude of wavevector 61 | """ 62 | return 4*np.pi/wavelen * np.sin(theta/2.0) 63 | 64 | @ureg.check('[]', '[]') 65 | def index_ratio(n_particle, n_matrix): 66 | """ 67 | Calculates the ratio of refractive indices (m in Mie theory) 68 | 69 | Parameters 70 | ---------- 71 | n_particle: structcol.Quantity [dimensionless] 72 | refractive index of particle at a particular wavelength 73 | can be complex 74 | n_matrix: structcol.Quantity [dimensionless] 75 | refractive index of matrix at a particular wavelength 76 | 77 | Notes 78 | ----- 79 | Returns a scalar rather than a Quantity because scipy special functions 80 | don't seem to be able to handle pint Quantities 81 | 82 | Returns 83 | ------- 84 | float 85 | """ 86 | return (n_particle/n_matrix).magnitude 87 | 88 | @ureg.check('[length]', '[]', '[length]') 89 | def size_parameter(wavelen, n_matrix, radius): 90 | """ 91 | Calculates the size parameter x=k_matrix*a needed for Mie calculations 92 | 93 | Parameters 94 | ---------- 95 | wavelen: structcol.Quantity [length] 96 | wavelength in vacuum 97 | n_matrix: structcol.Quantity [dimensionless] 98 | refractive index of matrix at wavelength=wavelen 99 | radius: structcol.Quantity [length] 100 | radius of particle 101 | 102 | Notes 103 | ----- 104 | Returns a scalar rather than a Quantity because scipy special functions 105 | don't seem to be able to handle pint Quantities 106 | 107 | Returns 108 | ------- 109 | complex or float 110 | """ 111 | # must use to('dimensionless') in case the wavelength and radius are 112 | # specified in different units; pint doesn't automatically make 113 | # ratios such as 'nm'/'um' dimensionless 114 | sp = (2 * np.pi * n_matrix / wavelen * radius) 115 | if isinstance(sp, Quantity): 116 | sp = sp.to('dimensionless').magnitude 117 | return sp 118 | -------------------------------------------------------------------------------- /pymie/mie.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011-2013, 2016 Vinothan N. Manoharan, Thomas G. Dimiduk, 2 | # Rebecca W. Perry, Jerome Fung, Ryan McGorty, Anna Wang, and Sofia Magkiriadou 3 | # 4 | # This file is part of the python-mie 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 Mie scattering calculations. 21 | 22 | Notes 23 | ----- 24 | Based on miescatlib.py in holopy. Also includes some functions from Jerome's 25 | old miescat_1d.py library. 26 | 27 | Numerical stability not guaranteed for large nstop, so be careful when 28 | calculating very large size parameters. A better-tested (and faster) version of 29 | this code is in the HoloPy package (http://manoharan.seas.harvard.edu/holopy). 30 | 31 | References 32 | ---------- 33 | [1] Bohren, C. F. and Huffman, D. R. ""Absorption and Scattering of Light by 34 | Small Particles" (1983) 35 | [2] Wiscombe, W. J. "Improved Mie Scattering Algorithms" Applied Optics 19, no. 36 | 9 (1980): 1505. doi:10.1364/AO.19.001505 37 | 38 | .. moduleauthor :: Jerome Fung 39 | .. moduleauthor :: Vinothan N. Manoharan 40 | .. moduleauthor :: Sofia Magkiriadou 41 | """ 42 | import warnings 43 | 44 | import numpy as np 45 | from scipy.special import spherical_jn, spherical_yn 46 | from scipy.special import legendre_p_all 47 | from scipy.integrate import trapezoid 48 | 49 | from . import Quantity, index_ratio, mie_specfuncs 50 | from . import multilayer_sphere_lib as msl 51 | from . import size_parameter, ureg 52 | from .mie_specfuncs import DEFAULT_EPS1, DEFAULT_EPS2 # default tolerances 53 | 54 | # User-facing functions for the most often calculated quantities (form factor, 55 | # efficiencies, asymmetry parameter) 56 | 57 | # all arguments should be dimensionless 58 | @ureg.check('[]', '[]', '[]', None, None) 59 | def calc_ang_dist(m, x, angles, mie = True, check = False): 60 | """ 61 | Calculates the angular distribution of light intensity for parallel and 62 | perpendicular polarization for a sphere. 63 | 64 | Parameters 65 | ---------- 66 | m: complex particle relative refractive index, n_part/n_med 67 | x: size parameter, x = ka = 2*pi*n_med/lambda * a (sphere radius a) 68 | angles: ndarray(structcol.Quantity [dimensionless]) 69 | array of angles. Must be entered as a Quantity to allow specifying 70 | units (degrees or radians) explicitly 71 | mie: Boolean (optional) 72 | if true (default) does full Mie calculation; if false, uses RG 73 | approximation 74 | check: Boolean (optional) 75 | if true, outputs scattering efficiencies 76 | 77 | Returns 78 | ------- 79 | ipar: |S_2|^2 80 | iperp: |S_1|^2 81 | (These are the differential scattering X-section*k^2 for polarization 82 | parallel and perpendicular to scattering plane, respectively. See 83 | Bohren & Huffman ch. 3 for details.) 84 | """ 85 | # convert to radians from whatever units the user specifies 86 | if isinstance(angles, Quantity): 87 | angles = angles.to('rad').magnitude 88 | 89 | if isinstance(x, Quantity): 90 | x = x.to('').magnitude 91 | 92 | #initialize arrays for holding ipar and iperp 93 | ipar = np.array([]) 94 | iperp = np.array([]) 95 | 96 | if mie: 97 | # Mie scattering preliminaries 98 | nstop = _nstop(np.array(x).max()) 99 | 100 | # if the index ratio m is an array with more than 1 element, it's a 101 | # multilayer particle 102 | if len(np.atleast_1d(m)) > 1: 103 | coeffs = msl.scatcoeffs_multi(m, x) 104 | else: 105 | coeffs = _scatcoeffs(m, x, nstop) 106 | n = np.arange(nstop)+1. 107 | prefactor = (2*n+1.)/(n*(n+1.)) 108 | 109 | S2, S1 = _amplitude_scattering_matrix(nstop, prefactor, coeffs, angles) 110 | ipar = np.absolute(S2)**2 111 | iperp = np.absolute(S1)**2 112 | 113 | if check: 114 | opt = _amplitude_scattering_matrix(nstop, prefactor, 115 | coeffs, 0).real 116 | qscat, qext, qback = calc_efficiencies(m, x) 117 | print('Number of terms:') 118 | print(nstop) 119 | print('Scattering, extinction, and backscattering efficiencies:') 120 | print(qscat, qext, qback) 121 | print('Extinction efficiency from optical theorem:') 122 | print((4./x**2)*opt) 123 | print('Asymmetry parameter') 124 | print(calc_g(m, x)) 125 | 126 | else: 127 | prefactor = -1j * (2./3.) * x**3 * np.absolute(m - 1) 128 | S2, S1 = _amplitude_scattering_matrix_RG(prefactor, x, angles) 129 | ipar = np.absolute(S2)**2 130 | iperp = np.absolute(S1)**2 131 | 132 | return ipar, iperp 133 | 134 | @ureg.check(None, None, '[length]', None, None) 135 | def calc_cross_sections(m, x, wavelen_media, eps1 = DEFAULT_EPS1, 136 | eps2 = DEFAULT_EPS2): 137 | """ 138 | Calculate (dimensional) scattering, absorption, and extinction cross 139 | sections, and asymmetry parameter for spherically symmetric scatterers. 140 | 141 | Parameters 142 | ---------- 143 | m: complex relative refractive index 144 | x: size parameter 145 | wavelen_media: structcol.Quantity [length] 146 | wavelength of incident light *in media* (usually this would be the 147 | wavelength in the effective index of the particle-matrix composite) 148 | 149 | Returns 150 | ------- 151 | cross_sections : tuple (5) 152 | Dimensional scattering, absorption, extinction, and backscattering 153 | cross sections, and (asymmetry parameter g) 154 | 155 | Notes 156 | ----- 157 | The backscattering cross-section is 1/(4*pi) times the radar backscattering 158 | cross-section; that is, it corresponds to the differential scattering 159 | cross-section in the backscattering direction. See B&H 4.6. 160 | 161 | The radiation pressure cross section C_pr is given by 162 | C_pr = C_ext - C_sca. 163 | 164 | The radiation pressure force on a sphere is 165 | 166 | F = (n_med I_0 C_pr) / c 167 | 168 | where I_0 is the incident intensity. See van de Hulst, p. 14. 169 | """ 170 | # This is adapted from mie.py in holopy 171 | # TODO take arrays for m and x to describe a multilayer sphere and return 172 | # multilayer scattering coefficients 173 | 174 | lmax = _nstop(np.array(x).max()) 175 | # if the index ratio m is an array with more than 1 element, it's a 176 | # multilayer particle 177 | if len(np.atleast_1d(m)) > 1: 178 | albl = msl.scatcoeffs_multi(m, x, eps1=eps1, eps2=eps2) 179 | else: 180 | albl = _scatcoeffs(m, x, lmax, eps1=eps1, eps2=eps2) 181 | 182 | cscat, cext, cback = tuple(np.abs(wavelen_media)**2 * c/2/np.pi for c in 183 | _cross_sections(albl[0], albl[1])) 184 | 185 | cabs = cext - cscat # conservation of energy 186 | 187 | asym = np.abs(wavelen_media)**2 / np.pi / cscat * \ 188 | _asymmetry_parameter(albl[0], albl[1]) 189 | 190 | return cscat, cext, cabs, cback, asym 191 | 192 | def calc_efficiencies(m, x): 193 | """ 194 | Scattering, extinction, backscattering efficiencies 195 | 196 | Note that the backscattering efficiency is 1/(4*pi) times the radar 197 | backscattering efficiency; that is, it corresponds to the differential 198 | scattering cross-section in the backscattering direction, divided by the 199 | geometrical cross-section 200 | """ 201 | nstop = _nstop(np.array(x).max()) 202 | # if the index ratio m is an array with more than 1 element, it's a 203 | # multilayer particle 204 | if len(np.atleast_1d(m)) > 1: 205 | coeffs = msl.scatcoeffs_multi(m, x) 206 | else: 207 | coeffs = _scatcoeffs(m, x, nstop) 208 | 209 | cscat, cext, cback = _cross_sections(coeffs[0], coeffs[1]) 210 | 211 | qscat = cscat * 2./np.abs(x)**2 212 | qext = cext * 2./np.abs(x)**2 213 | qback = cback * 1./np.abs(x)**2 214 | 215 | # in order: scattering, extinction and backscattering efficiency 216 | return qscat, qext, qback 217 | 218 | def calc_g(m, x): 219 | """ 220 | Asymmetry parameter 221 | """ 222 | nstop = _nstop(np.array(x).max()) 223 | # if the index ratio m is an array with more than 1 element, it's a 224 | # multilayer particle 225 | if len(np.atleast_1d(m)) > 1: 226 | coeffs = msl.scatcoeffs_multi(m, x) 227 | else: 228 | coeffs = _scatcoeffs(m, x, nstop) 229 | 230 | cscat = _cross_sections(coeffs[0], coeffs[1])[0] * 2./np.array(x).max()**2 231 | g = ((4./(np.array(x).max()**2 * cscat)) 232 | * _asymmetry_parameter(coeffs[0], coeffs[1])) 233 | return g 234 | 235 | @ureg.check(None, None, '[length]', ('[]','[]', '[]')) 236 | def calc_integrated_cross_section(m, x, wavelen_media, theta_range): 237 | """ 238 | Calculate (dimensional) integrated cross section using quadrature 239 | 240 | Parameters 241 | ---------- 242 | m: complex relative refractive index 243 | x: size parameter 244 | wavelen_media: structcol.Quantity [length] 245 | wavelength of incident light *in media* 246 | theta_range: tuple of structcol.Quantity [dimensionless] 247 | first two elements specify the range of polar angles over which to 248 | integrate the scattering. Last element specifies the number of angles. 249 | 250 | Returns 251 | ------- 252 | cross_section : float 253 | Dimensional integrated cross-section 254 | """ 255 | theta_min = theta_range[0].to('rad').magnitude 256 | theta_max = theta_range[1].to('rad').magnitude 257 | angles = Quantity(np.linspace(theta_min, theta_max, theta_range[2]), 'rad') 258 | form_factor = calc_ang_dist(m, x, angles) 259 | 260 | integrand_par = form_factor[0]*np.sin(angles) 261 | integrand_perp = form_factor[1]*np.sin(angles) 262 | 263 | # scipy.integrate.trapezoid does not yet preserve units, so we will remove 264 | # the units before calling and put them back afterward. Can simplify code 265 | # when these github issues are fixed: 266 | # https://github.com/hgrecco/pint/issues/114 267 | # https://github.com/hgrecco/pint/issues/2101 268 | 269 | integral_par = 2 * np.pi * trapezoid(integrand_par, x=angles.magnitude) 270 | integral_perp = 2 * np.pi * trapezoid(integrand_perp, x=angles.magnitude) 271 | 272 | # multiply by 1/k**2 to get the dimensional value 273 | return wavelen_media**2/4/np.pi/np.pi * (integral_par + integral_perp)/2.0 274 | 275 | def calc_energy(radius, n_medium, m, x, nstop, 276 | eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 277 | ''' 278 | Calculates the electromagnetic energy inside a dielectric sphere 279 | according to equation 11 in 280 | Bott and Zdunkowski, J. Opt. Soc. Am. A, vol 4, no. 8, 1987 281 | 282 | Parameters 283 | ---------- 284 | radius: float 285 | radius of the scatterer (Quantity in [length]) 286 | n_medium: float 287 | refractive index of the medium in which scatterer is embedded 288 | (Quantity, dimensionless) 289 | m: float 290 | complex relative refractive index 291 | x: float 292 | size parameter 293 | nstop: float 294 | maximum order 295 | 296 | Returns 297 | ------- 298 | W: float (Quantity in [energy]) 299 | electromagnetic energy inside the dielectic sphere 300 | 301 | ''' 302 | W0 = _W0(radius, n_medium) 303 | gamma_n, An = _time_coeffs(m, x, nstop, eps1 = eps1, eps2 = eps2) 304 | n = np.arange(1,nstop+1) 305 | y = m*x 306 | W = 3/4*W0*np.sum((2*n + 1)/y**2 *gamma_n*(1+An**2-n*(n+1)/y**2)) 307 | 308 | return W 309 | 310 | def calc_dwell_time(radius, n_medium, n_particle, wavelen, 311 | min_angle=0.01, num_angles=200, 312 | eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 313 | ''' 314 | Calculates the dwell time, the time 315 | according to 3.37 in 316 | Lagendijk and van Tiggelen, Physics Reports 270 (1996) 143-215 317 | 318 | Parameters 319 | ---------- 320 | radius: float 321 | radius of the scatterer (Quantity in [length]) 322 | n_medium: float (Quantity, dimensionless) 323 | refractive index of the medium in which scatterer is embedded 324 | n_particle: float (Quantity, dimensionless) 325 | refractive index of the scatterer 326 | wavelen: structcol.Quantity [length] 327 | wavelength of incident light in vacuum 328 | min_angle: float (in radians) 329 | minimum angle to integrate over for total cross section 330 | num_angles: float 331 | number of angles to integrate over for total cross section 332 | eps1, eps2: needed for calculating scattcoeffs 333 | 334 | Returns 335 | ------- 336 | dwell_time: float (Quantity in [to,e]) 337 | time wave spends inside the dielectic sphere 338 | ''' 339 | m = index_ratio(n_particle, n_medium) 340 | x = size_parameter(wavelen, n_medium, radius) 341 | nstop = _nstop(x) 342 | wavelen_media = wavelen/n_medium 343 | 344 | # calculate the energy contained in sphere 345 | W = calc_energy(radius, n_medium, m, x, nstop, eps1 = eps1, eps2 = eps2) 346 | 347 | # define speed of light 348 | # get this from Pint in a somewhat indirect way: 349 | c = Quantity(1.0, 'speed_of_light').to('m/s') 350 | 351 | # calculate total cross section 352 | if np.imag(x)>0: 353 | angles = Quantity(np.linspace(min_angle, np.pi, num_angles), 'rad') 354 | distance = radius.max() 355 | k = 2*np.pi/wavelen_media 356 | (diff_cscat_par, 357 | diff_cscat_perp) = diff_scat_intensity_complex_medium(m, 358 | x, angles, 359 | k*distance) 360 | 361 | cscat = integrate_intensity_complex_medium(diff_cscat_par, 362 | diff_cscat_perp, 363 | distance, 364 | angles, k)[0] 365 | else: 366 | cscat = calc_cross_sections(m, x, wavelen_media, 367 | eps1 = eps1, eps2 = eps2)[0] 368 | 369 | # calculate dwell time 370 | dwell_time = W/(cscat*c) 371 | 372 | return dwell_time 373 | 374 | def calc_reflectance(radius, n_medium, n_particle, wavelen, 375 | min_angle=np.pi/2, num_angles=50, 376 | eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 377 | 378 | m = index_ratio(n_particle, n_medium) 379 | x = size_parameter(wavelen, n_medium, radius) 380 | wavelen_media = wavelen/n_medium 381 | geometric_cross_sec = np.pi*radius**2 382 | 383 | thetas = Quantity(np.linspace(min_angle, np.pi, num_angles), 'rad') 384 | 385 | # calculate reflectance cross section 386 | if np.imag(x)>0: 387 | angles = Quantity(np.linspace(min_angle, np.pi, num_angles), 'rad') 388 | distance = radius.max() 389 | k = 2*np.pi/wavelen_media 390 | (diff_cscat_par, 391 | diff_cscat_perp) = diff_scat_intensity_complex_medium(m, 392 | x, thetas, 393 | k*distance) 394 | 395 | refl_cscat = integrate_intensity_complex_medium(diff_cscat_par, 396 | diff_cscat_perp, 397 | distance, 398 | angles, k)[0] 399 | else: 400 | 401 | refl_cscat = calc_integrated_cross_section(m, x, wavelen_media, 402 | (thetas[0], thetas[-1], 403 | num_angles)) 404 | 405 | reflectance = refl_cscat/geometric_cross_sec/wavelen_media.magnitude**2 406 | reflectance = reflectance.magnitude 407 | 408 | return reflectance 409 | 410 | # Mie functions used internally 411 | 412 | def _pis_and_taus(nstop, thetas): 413 | ''' 414 | Calculate pi and tau angular functions at an array of theta out to order n 415 | 416 | Parameters 417 | ---------- 418 | nstop: float 419 | maximum order 420 | thetas: ndarray or float 421 | scattering angles 422 | 423 | Returns 424 | ------- 425 | pis, taus (order 1 to n): ndarray 426 | angular functions, each has shape (thetas.shape, nstop) 427 | 428 | Notes 429 | ----- 430 | Pure python version of mieangfuncs.pisandtaus in holopy. See B/H eqn 4.46, 431 | Wiscombe eqns 3-4. 432 | ''' 433 | # make n a float if it's not already by taking the maximum value given 434 | nstop = np.max(nstop) 435 | 436 | # make theta an array if it's not already 437 | thetas = np.atleast_1d(thetas) 438 | 439 | # get the shape of thetas to reshape arrays later 440 | ang_shape = list(thetas.shape) 441 | 442 | # flatten to make calculations easier 443 | if isinstance(thetas, Quantity): 444 | thetas = thetas.to('rad').magnitude 445 | thetas = np.ndarray.flatten(thetas) 446 | 447 | mu = np.cos(thetas) 448 | 449 | # returns P_n and derivatives up to degree n for all values in mu array. 450 | # legendre0 has shape (2, nmax, len(mu)), where legendre0[0,:,:] is P_n and 451 | # legendre0[1,:,:] is the derivative. 452 | legendre0 = legendre_p_all(nstop, mu, diff_n=1) 453 | 454 | # Perform calculations on pis to get taus. We rearrange the order of the 455 | # axes to the order that we used in previous versions of the code, where 456 | # the Legendre polynomial calculation was not automatically vectorized. 457 | pis = np.swapaxes(legendre0[1, 0:nstop+1, :], 0, 1) 458 | pishift = np.concatenate((np.zeros((len(thetas),1)), pis), 459 | axis=1)[:, :nstop+1] 460 | n = np.arange(nstop+1) 461 | mus = np.swapaxes(np.tile(mu, (nstop+1,1)),0,1) 462 | taus = n*pis*mus - (n+1)*pishift 463 | 464 | # reshape to match thetas original shape 465 | ang_shape.append(nstop+1) 466 | pis = np.reshape(pis, ang_shape) 467 | taus = np.reshape(taus, ang_shape) 468 | return pis[...,1:nstop+1], taus[...,1:nstop+1] 469 | 470 | def _scatcoeffs(m, x, nstop, eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 471 | # see B/H eqn 4.88 472 | # implement criterion used by BHMIE plus a couple more orders to be safe 473 | # nmx = np.array([nstop, np.round(np.absolute(m*x))]).max() + 20 474 | # Dnmx = mie_specfuncs.log_der_1(m*x, nmx, nstop) 475 | # above replaced with Lentz algorithm 476 | Dnmx = mie_specfuncs.dn_1_down(m * x, nstop + 1, nstop, 477 | mie_specfuncs.lentz_dn1(m * x, nstop + 1, 478 | eps1, eps2)) 479 | n = np.arange(nstop+1) 480 | psi, xi = mie_specfuncs.riccati_psi_xi(x, nstop) 481 | psishift = np.concatenate((np.zeros(1), psi))[0:nstop+1] 482 | xishift = np.concatenate((np.zeros(1), xi))[0:nstop+1] 483 | an = ( (Dnmx/m + n/x)*psi - psishift ) / ( (Dnmx/m + n/x)*xi - xishift ) 484 | bn = ( (Dnmx*m + n/x)*psi - psishift ) / ( (Dnmx*m + n/x)*xi - xishift ) 485 | return np.array([an[1:nstop+1], bn[1:nstop+1]]) # output begins at n=1 486 | 487 | def _internal_coeffs(m, x, n_max, eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 488 | ''' 489 | Calculate internal Mie coefficients c_n and d_n given 490 | relative index, size parameter, and maximum order of expansion. 491 | 492 | Follow Bohren & Huffman's convention. Note that van de Hulst and Kerker 493 | have different conventions (labeling of c_n and d_n and factors of m) 494 | for their internal coefficients. 495 | ''' 496 | ratio = mie_specfuncs.R_psi(x, m * x, n_max, eps1, eps2) 497 | D1x, D3x = mie_specfuncs.log_der_13(x, n_max, eps1, eps2) 498 | D1mx = mie_specfuncs.dn_1_down(m * x, n_max + 1, n_max, 499 | mie_specfuncs.lentz_dn1(m * x, n_max + 1, 500 | eps1, eps2)) 501 | cl = m * ratio * (D3x - D1x) / (D3x - m * D1mx) 502 | dl = m * ratio * (D3x - D1x) / (m * D3x - D1mx) 503 | return np.array([cl[1:], dl[1:]]) # start from l = 1 504 | 505 | def _trans_coeffs(m, x, n_max, eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 506 | ''' 507 | Calculate the transmission Mie coefficients c_n and d_n given 508 | relative index, size parameter, and maximum order of expansion. 509 | 510 | Note that the implementation here follows van de Hulst [1], 511 | in accordance with equation 3 from Bott and Zdunkowski [2]. 512 | These coefficients are implemented in this convention for use in 513 | calculating the electromagnetic energy in the sphere, 514 | which is needed to calculate dwell times. 515 | 516 | [1] H. C. van de Hulst, Light Scattering by Small Particles 517 | (Wiley, New York, 1957), pp. 119-130. 518 | 519 | [2] Bott and Zdunkowski [2], J. Opt. Soc. Am. A, vol 4, no. 8, 1987. 520 | ''' 521 | nstop=n_max 522 | n = np.arange(nstop+1) 523 | psi, _ = mie_specfuncs.riccati_psi_xi(m*x, nstop) 524 | psishift = np.concatenate((np.zeros(1), psi))[0:nstop+1] 525 | psi_prime = psishift - n*psi/(m*x) 526 | psi = psi[1:nstop+1] 527 | psi_prime = psi_prime[1:nstop+1] 528 | 529 | _, xi = mie_specfuncs.riccati_psi_xi(x, nstop) 530 | xishift = np.concatenate((np.zeros(1), xi))[0:nstop+1] 531 | xi_prime = xishift - n*xi/x 532 | xi = xi[1:nstop+1] 533 | xi_prime = xi_prime[1:nstop+1] 534 | 535 | cn = 1j/(xi*psi_prime - m*psi*xi_prime) 536 | dn = 1j/(m*psi_prime*xi - psi*xi_prime) 537 | 538 | return np.array([cn, dn]) 539 | 540 | def _time_coeffs(m, x, nstop, eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 541 | ''' 542 | Calculate what we refer to as the time Mie coefficients gamma_n and An, 543 | given the relative inted, size parameter, maximum order of expansion. 544 | 545 | We follow the convention of equation 11 in 546 | Bott and Zdunkowski, J. Opt. Soc. Am. A, vol 4, no. 8, 1987 547 | 548 | using the recurrence relation in Bohren & Huffman's eq 4.88 for psi prime. 549 | and the expressions for cn and dn from equation 3 in Bott and Zdunkowski. 550 | ''' 551 | 552 | n = np.arange(nstop+1) 553 | n_max = np.max(n) 554 | psi, _ = mie_specfuncs.riccati_psi_xi(m*x, nstop) 555 | psishift = np.concatenate((np.zeros(1), psi))[1:nstop+1] 556 | psi = psi[1:nstop+1] 557 | n = n[1:nstop+1] 558 | cn, dn = _trans_coeffs(m,x, n_max, eps1=eps1, eps2=eps2) 559 | 560 | # calculate gamma_n and An 561 | gamma_n = (m**2*(m*cn*psi)*np.conj(m*cn*psi) 562 | + m**2*(m*dn*psi)*np.conj(m*dn*psi)) 563 | An = (psishift-n*psi/(m*x))/psi 564 | 565 | return gamma_n, An 566 | 567 | def _W0(radius, n_medium): 568 | ''' 569 | Calculates the time-averaged electromagnetic energy of a sphere having the 570 | electromagnetic properties of the surrounding medium, according to eq. 9 571 | of Bott and Zdunkowski, J. Opt. Soc. Am. A, vol 4, no. 8, 1987 572 | 573 | W0=2/3*np.pi*radius^3*E0^2*permittivity_medium 574 | 575 | where radius is the radius of the scatterer, permittivity_medium is the 576 | permittivity of the surrounding medium, and E_0 is the field incident on 577 | the scatterer 578 | 579 | We use units such that the energy density in vacuum is 1, 580 | where energy density in vacuum is expressed as: 581 | 582 | energy_density = 1/2*E0^2*permitttivity_medium 583 | 584 | So plugging this expression into the equation for W0, we have: 585 | W0 = 2/3*pi*radius^3*2*energy_density 586 | 587 | ''' 588 | energy_density = 1 589 | W0=2/3*np.pi*radius**3*2*energy_density 590 | 591 | return W0 592 | 593 | def _nstop(x): 594 | # takes size parameter, outputs order to compute to according to 595 | # Wiscombe, Applied Optics 19, 1505 (1980). 596 | # 7/7/08: generalize to apply same criterion when x is complex 597 | #return (np.round(np.absolute(x+4.05*x**(1./3.)+2))).astype('int') 598 | 599 | # Criterion for calculating near-field properties with exact Mie solutions 600 | # (J. R. Allardice and E. C. Le Ru, Applied Optics, Vol. 53, No. 31 (2014). 601 | return (np.round(np.absolute(x+11*x**(1./3.)+1))).astype('int') 602 | 603 | def _asymmetry_parameter(al, bl): 604 | ''' 605 | Inputs: an, bn coefficient arrays from Mie solution 606 | 607 | See discussion in Bohren & Huffman p. 120. 608 | The output of this function omits the prefactor of 4/(x^2 Q_sca). 609 | ''' 610 | lmax = al.shape[0] 611 | l = np.arange(lmax) + 1 612 | selfterm = (l[:-1] * (l[:-1] + 2.) / (l[:-1] + 1.) * 613 | np.real(al[:-1] * np.conj(al[1:]) + 614 | bl[:-1] * np.conj(bl[1:]))).sum() 615 | crossterm = ((2. * l + 1.)/(l * (l + 1)) * 616 | np.real(al * np.conj(bl))).sum() 617 | return selfterm + crossterm 618 | 619 | def _cross_sections(al, bl): 620 | ''' 621 | Calculates scattering and extinction cross sections 622 | given arrays of Mie scattering coefficients al and bl. 623 | 624 | See Bohren & Huffman eqns. 4.61 and 4.62. 625 | 626 | The output omits a scaling prefactor of 2 * pi / k^2 = lambda_media^2/2/pi. 627 | ''' 628 | lmax = al.shape[0] 629 | 630 | l = np.arange(lmax) + 1 631 | prefactor = (2. * l + 1.) 632 | 633 | cscat = (prefactor * (np.abs(al)**2 + np.abs(bl)**2)).sum() 634 | cext = (prefactor * np.real(al + bl)).sum() 635 | 636 | # see p. 122 and discussion in that section. The formula on p. 122 637 | # calculates the backscattering cross-section according to the traditional 638 | # definition, which includes a factor of 4*pi for historical reasons. We 639 | # jettison the factor of 4*pi to get values that correspond to the 640 | # differential scattering cross-section in the backscattering direction. 641 | alts = 2. * (np.arange(lmax) % 2) - 1 642 | cback = (np.abs((prefactor * alts * (al - bl)).sum())**2)/4.0/np.pi 643 | 644 | return cscat, cext, cback 645 | 646 | def _cross_sections_complex_medium_fu(al, bl, cl, dl, radius, n_particle, 647 | n_medium, x_scatterer, x_medium, 648 | wavelen): 649 | ''' 650 | Calculates dimensional scattering, absorption, and extinction cross 651 | sections for scatterers in an absorbing medium. This function does not 652 | handle multilayered particles. 653 | 654 | al, bl: Mie scattering coefficients 655 | cl, dl: Mie internal coefficients 656 | radius: radius of the scatterer (Quantity in [length]) 657 | n_particle: refractive index of the scatterer (Quantity, dimensionless) 658 | n_medium: refractive index of the medium in which scatterer is embedded 659 | (Quantity, dimensionless) 660 | x_scatterer: size parameter using the particle's refractive index 661 | x_medium: size parameter using the medium's refractive index 662 | wavelen: wavelength of light in vacuum (Quantity in [length]) 663 | 664 | Reference 665 | --------- 666 | Q. Fu and W. Sun, "Mie theory for light scattering by a spherical particle 667 | in an absorbing medium". Applied Optics, 40, 9 (2001). 668 | 669 | ''' 670 | # if the imaginary part of the medium index is close to 0, then use the 671 | # limit value of prefactor1 for the calculations 672 | if n_medium.imag.magnitude <= 1e-7: 673 | prefactor1 = wavelen / (np.pi * radius**2 * n_medium.real) 674 | else: 675 | eta = 4*np.pi*radius*n_medium.imag/wavelen 676 | prefactor1 = eta**2 * wavelen / (2*np.pi*radius**2*n_medium.real* 677 | (1+(eta-1)*np.exp(eta))) 678 | 679 | lmax = al.shape[0] 680 | l = np.arange(lmax) + 1 681 | prefactor2 = (2. * l + 1.) 682 | 683 | # calculate the scattering efficiency 684 | _, xi = mie_specfuncs.riccati_psi_xi(x_medium, lmax) 685 | xishift = np.concatenate((np.zeros(1), xi))[0:lmax+1] 686 | xi = xi[1:] 687 | xishift = xishift[1:] 688 | 689 | Bn = (np.abs(al)**2 * (xishift - l*xi/x_medium) * np.conj(xi) - 690 | np.abs(bl)**2 * xi * 691 | np.conj(xishift - l*xi/x_medium)) / (2*np.pi*n_medium/wavelen) 692 | Qscat = prefactor1 * np.sum(prefactor2 * Bn.imag) 693 | 694 | # calculate the absorption and extinction efficiencies 695 | psi, _ = mie_specfuncs.riccati_psi_xi(x_scatterer, lmax) 696 | psishift = np.concatenate((np.zeros(1), psi))[0:lmax+1] 697 | psi = psi[1:] 698 | psishift = psishift[1:] 699 | 700 | An = (np.abs(cl)**2 * psi * np.conj(psishift - l*psi/x_scatterer) - 701 | np.abs(dl)**2 * (psishift - l*psi/x_scatterer)* 702 | np.conj(psi)) / (2*np.pi*n_particle/wavelen) 703 | Qabs = prefactor1 * np.sum(prefactor2 * An.imag) 704 | Qext = prefactor1 * np.sum(prefactor2 * (An+Bn).imag) 705 | 706 | # calculate the cross sections 707 | Cscat = Qscat *np.pi * radius**2 708 | Cabs = Qabs *np.pi * radius**2 709 | Cext = Qext *np.pi * radius**2 710 | 711 | return(Cscat, Cabs, Cext) 712 | 713 | def _cross_sections_complex_medium_sudiarta(al, bl, x, radius): 714 | ''' 715 | Calculates dimensional scattering, absorption, and extinction cross 716 | sections for scatterers in an absorbing medium. 717 | 718 | al, bl: Mie scattering coefficients 719 | x: size parameter using the medium's refractive index 720 | radius: radius of the scatterer (Quantity in [length]) 721 | 722 | Reference 723 | --------- 724 | I. W. Sudiarta and P. Chylek, "Mie-scattering formalism for spherical 725 | particles embedded in an absorbing medium", J. Opt. Soc. Am. A, 18, 6 726 | (2001). 727 | 728 | ''' 729 | radius = np.array(radius.magnitude).max() * radius.units 730 | x = np.array(x).max() 731 | 732 | k = x/radius 733 | lmax = al.shape[0] 734 | l = np.arange(lmax) + 1 735 | prefactor = (2. * l + 1.) 736 | 737 | # if the imaginary part of k is close to 0 (because the medium index is 738 | # close to 0), then use the limit value of factor for the calculations 739 | if k.imag.magnitude <= 1e-8: 740 | factor = 1/2 741 | else: 742 | exponent = np.exp(2*radius*k.imag) 743 | factor = (exponent/(2*radius*k.imag)+(1-exponent)/(2*radius*k.imag)**2) 744 | I_denom = k.real * factor 745 | 746 | _, xi = mie_specfuncs.riccati_psi_xi(x, lmax) 747 | xishift = np.concatenate((np.zeros(1), xi))[0:lmax+1] 748 | xi = xi[1:] 749 | xishift = xishift[1:] 750 | xideriv = xishift - l*xi/x 751 | 752 | psi, _ = mie_specfuncs.riccati_psi_xi(x, lmax) 753 | psishift = np.concatenate((np.zeros(1), psi))[0:lmax+1] 754 | psi = psi[1:] 755 | psishift = psishift[1:] 756 | psideriv = psishift - l*psi/x 757 | 758 | # calculate the scattering cross section 759 | term1 = (-1j * np.abs(al)**2 *xideriv * np.conj(xi) + 760 | 1j* np.abs(bl)**2 * xi * np.conj(xideriv)) 761 | 762 | numer1 = (np.sum(prefactor * term1) * k).real 763 | Cscat = np.pi / np.abs(k)**2 * numer1 / I_denom 764 | 765 | # calculate the absorption cross section 766 | term2 = (1j*np.conj(psi)*psideriv - 1j*psi*np.conj(psideriv) + 767 | 1j*bl*np.conj(psideriv)*xi + 1j*np.conj(bl)*psi*np.conj(xideriv) + 768 | 1j*np.abs(al)**2*xideriv*np.conj(xi) - 769 | 1j*np.abs(bl)**2*xi*np.conj(xideriv) - 770 | 1j*al*np.conj(psi)*xideriv - 1j*np.conj(al)*psideriv*np.conj(xi)) 771 | numer2 = (np.sum(prefactor * term2) * k).real 772 | Cabs = np.pi / np.abs(k)**2 * numer2 / I_denom 773 | 774 | # calculate the extinction cross section 775 | term3 = (1j*np.conj(psi)*psideriv - 1j*psi*np.conj(psideriv) + 776 | 1j*bl*np.conj(psideriv)*xi + 1j*np.conj(bl)*psi*np.conj(xideriv) - 777 | 1j*al*np.conj(psi)*xideriv - 1j*np.conj(al)*psideriv*np.conj(xi)) 778 | numer3 = (np.sum(prefactor * term3) * k).real 779 | Cext = np.pi / np.abs(k)**2 * numer3 / I_denom 780 | 781 | return(Cscat, Cabs, Cext) 782 | 783 | 784 | def _scat_fields_complex_medium(m, x, thetas, kd, near_field=False): 785 | ''' 786 | Calculates the scattered fields as a function of scattering angle theta 787 | using the full Mie solutions. These solutions are valid both in the near 788 | and far field. When the medium has a zero imaginary component of the 789 | refractive index (is non-absorbing), the full solutions at the far field 790 | match the standard far-field Mie solutions given by calc_cross_sections. 791 | This is not the case when there is absorption because the standard 792 | far-field solutions assume an arbitrary distance far away, so they don't 793 | depend on the distance from the scatterer. And when the medium absorbs, the 794 | cross sections should really depend on the distance away at which we 795 | integrate the differential cross sections. The phase function, (diff cross 796 | section / total cross section) is the same when calculated with the full 797 | Mie solutions in the far field as when calculated with the far-field Mie 798 | solutions because this ratio does not depend on how far we integrate from 799 | the scatterer. 800 | 801 | The differential scattered intensity is computed by substituting the 802 | scattered electric and magnetic fields into the radial component of the 803 | Poynting vector: 804 | 805 | I_par = Es_theta * conj(Hs_phi) 806 | I_perp = Es_phi * conj(Hs_theta) 807 | 808 | where conj() indicates the complex conjugate. The radial component of the 809 | Poynting vector is then 1/2 * Re(I_par - I_perp). 810 | 811 | Parameters 812 | ---------- 813 | m: complex relative refractive index 814 | x: size parameter using the medium's refractive index 815 | thetas: array of scattering angles (Quantity in rad) 816 | kd: k * distance, where k = 2*np.pi*n_matrix/wavelen, and distance is the 817 | distance away from the center of the particle. The standard far-field 818 | solution is obtained when distance >> radius in a non absorbing medium. 819 | (Quantity, dimensionless) 820 | near_field: boolean 821 | Set to True to include the near-fields. Sometimes the full solutions 822 | that include the near fields aren't wanted, for ex when the total cross 823 | section calculation includes the structure factor, and the combination 824 | of the angle-dependent differential cross section multiplied by the 825 | structure factor gives very high cross sections at the surface of the 826 | particle. When we want to neglect the effect of the near fields and 827 | still integrate at the surface of the particle, we use the asymptotic 828 | form of the spherical Hankel function in the far field (p. 94 of Bohren 829 | and Huffman). 830 | 831 | Returns 832 | ------- 833 | Es_theta, Es_phi, Hs_phi, Hs_theta: arrays 834 | scattered field components for an array of theta 835 | 836 | References 837 | ---------- 838 | C. F. Bohren and D. R. Huffman. Absorption and scattering of light by 839 | small particles. Wiley-VCH (2004), chapter 4.4.1. 840 | Q. Fu and W. Sun, "Mie theory for light scattering by a spherical particle 841 | in an absorbing medium". Applied Optics, 40, 9 (2001). 842 | ''' 843 | # convert units from whatever units the user specifies 844 | if isinstance(thetas, Quantity): 845 | thetas = thetas.to('rad').magnitude 846 | if isinstance(kd, Quantity): 847 | kd = kd.to('').magnitude 848 | 849 | # calculate mie coefficients 850 | nstop = _nstop(np.array(x).max()) 851 | n = np.arange(nstop)+1. 852 | 853 | # if the index ratio m is an array with more than 1 element, it's a 854 | # multilayer particle 855 | if len(np.atleast_1d(m)) > 1: 856 | an, bn = msl.scatcoeffs_multi(m, x) 857 | else: 858 | an, bn = _scatcoeffs(m, x, nstop) 859 | 860 | # calculate prefactor (omitting the incident electric field because it 861 | # cancels out when calculating the scattered intensity) 862 | En = 1j**n * (2*n+1) / (n*(n+1)) 863 | 864 | # calculate pis and taus at the scattering angles theta 865 | pis, taus = _pis_and_taus(nstop, thetas) 866 | 867 | # calculate the scattered electric and magnetic fields (omitting the 868 | # sin(phi) and cos(phi) factors because they will be accounted for when 869 | # integrating to get the scattering cross section) 870 | 871 | # required for calculations with polarized light 872 | th_shape = list(thetas.shape) 873 | th_shape.append(len(n)) 874 | 875 | En = np.broadcast_to(En, th_shape) 876 | an = np.broadcast_to(an, th_shape) 877 | bn = np.broadcast_to(bn, th_shape) 878 | 879 | # if full Mie solutions are wanted (including the near field effects given 880 | # by the spherical Hankel terms). The near fields don't change the total 881 | # cross section much, but the angle-dependence of the differential cross 882 | # section will be very different from the ones obtained with the far-field 883 | # approximations. If kd is large (if we're in the far field) in a non 884 | # absorbing medium, then the full solutions reduce down to the standard 885 | # far-field solutions given by calc_cross_sections(). 886 | if near_field: 887 | # calculate spherical Bessel function and derivative 888 | nstop_array = np.arange(0,nstop+1) 889 | jn = spherical_jn(nstop_array, kd) 890 | yn = spherical_yn(nstop_array, kd) 891 | zn = jn + 1j*yn 892 | zn = zn[1:] 893 | 894 | _, xi = mie_specfuncs.riccati_psi_xi(kd, nstop) 895 | xishift = np.concatenate((np.zeros(1), xi))[0:nstop+1] 896 | xi = xi[1:] 897 | xishift = xishift[1:] 898 | bessel_deriv = xishift - n*xi/kd 899 | zn = np.broadcast_to(zn, th_shape) 900 | bessel_deriv = np.broadcast_to(bessel_deriv, th_shape) 901 | 902 | Es_theta = np.sum(En 903 | * (1j * an * taus * bessel_deriv/kd - bn * pis * zn), 904 | axis=-1) 905 | Es_phi = np.sum(En 906 | * (-1j * an * pis * bessel_deriv/kd + bn * taus * zn), 907 | axis=-1) 908 | Hs_phi = np.sum(En 909 | * (1j * bn * pis * bessel_deriv/kd - an * taus * zn), 910 | axis=-1) 911 | Hs_theta = np.sum(En 912 | * (1j * bn * taus * bessel_deriv/kd - an * pis * zn), 913 | axis=-1) 914 | 915 | # if the near field effects aren't desired, use the asymptotic form of the 916 | # spherical Hankel function in the far field (p. 94 of Bohren and Huffman) 917 | else: 918 | Es_theta = np.sum((2*n+1) / (n*(n+1)) * (an * taus + bn * pis), 919 | axis=-1)* np.exp(1j*kd)/(-1j*kd) 920 | Es_phi = -np.sum((2*n+1) / (n*(n+1)) * (an * pis + bn * taus), 921 | axis=-1)* np.exp(1j*kd)/(-1j*kd) 922 | Hs_phi = np.sum((2*n+1) / (n*(n+1))*(bn * pis + an * taus), 923 | axis=-1)* np.exp(1j*kd)/(-1j*kd) 924 | Hs_theta = np.sum((2*n+1) / (n*(n+1))* (bn * taus + an * pis), 925 | axis=-1)* np.exp(1j*kd)/(-1j*kd) 926 | # note that these solutions are not currently used anywhere in mie.py. 927 | # When the fields are multiplied to calculate the intensity, the 928 | # exponential terms reduce down to a term that depends on kd (see 929 | # diff_scat_intensity_complex_medium(). So these equations lead to 930 | # intensities that are the same as those calculated with the scattering 931 | # matrix in diff_scat_intensity_complex_medium(). 932 | # We leave the expressions here in case users ever have a need to know 933 | # the actual fields, rather than the intensities. 934 | 935 | return Es_theta, Es_phi, Hs_theta, Hs_phi 936 | 937 | def diff_scat_intensity_complex_medium(m, x, thetas, kd, 938 | coordinate_system = 'scattering plane', phis = None, near_field=False, 939 | incident_vector=None): 940 | ''' 941 | Calculates the differential scattered intensity in an absorbing medium. 942 | User can choose whether to include near fields. 943 | 944 | When coordinate_system == 'scattering plane':. 945 | The solutions are given as a function of scattering angle theta. 946 | 947 | The differential scattered intensity is computed by substituting the 948 | scattered electric and magnetic fields into the radial component of the 949 | Poynting vector: 950 | 951 | I_par = Es_theta * conj(Hs_phi) 952 | I_perp = Es_phi * conj(Hs_theta) 953 | 954 | where conj() indicates the complex conjugate. The radial component of 955 | the Poynting vector is then 1/2 * Re(I_par - I_perp). 956 | 957 | When coordinate_system == 'cartesian': 958 | The solutions are given as a function of scattering angle theta and 959 | azimuthal angle phi. 960 | 961 | The differential scattered intensity is computed by substituting the 962 | scattered electric and magnetic fields into the z-component of the 963 | Poynting vector: 964 | 965 | I_x = Es_x * conj(Hs_x) 966 | I_y = -Es_y * conj(Hs_y) 967 | 968 | where conj() indicates the complex conjugate. The radial component of 969 | the Poynting vector is then 1/2 * Re(I_x - I_y). 970 | 971 | Parameters 972 | ---------- 973 | m: complex relative refractive index 974 | x: size parameter using the medium's refractive index 975 | thetas: array of scattering angles (Quantity in rad) 976 | kd: k * distance, where k = 2*np.pi*n_matrix/wavelen, and distance is the 977 | distance away from the center of the particle. The standard far-field 978 | solutions are obtained when distance >> radius in a non-absorbing 979 | medium. (Quantity, dimensionless) 980 | coordinate_system: string 981 | default value 'scattering plane' means scattering calculations will be 982 | carried out in the basis defined by basis vectors parallel and 983 | perpendicular to scattering plane. Variable also accepts value 984 | 'cartesian' which scattering calculations will be carried out in the 985 | basis defined by basis vectors x and y in the lab frame, with z 986 | as the direction of propagation. 987 | phis: None or ndarray 988 | azimuthal angles for which to calculate the diff scat intensity. In the 989 | 'scattering plane' coordinate system, the scattering matrix does not 990 | depend on phi, so phi should be set to None. In the 'cartesian' 991 | coordinate system, the scattering matrix does depend on phi, so an 992 | array of values should be provided. 993 | near_field: boolean 994 | True to include the near-fields (default is False). Cannot be set to 995 | True while using coordinate_system='cartesian' because near field 996 | solutions are not implemented for cartesian coordinate system. Also 997 | cannot be set to True if using an incident_vector that is not None 998 | (unpolarized for 'scattering plane' coordinate system). Often, the full 999 | solutions that include the near fields aren't wanted, for ex when the 1000 | total cross section calculation includes the structure factor, and the 1001 | combination of the angle-dependent differential cross section 1002 | multiplied by the structure factor gives very high cross sections at 1003 | the surface of the particle. When we want to neglect the effect of the 1004 | near fields and still integrate at the surface of the particle, we use 1005 | the asymptotic form of the spherical Hankel function in the far field 1006 | (p. 94 of Bohren and Huffman). 1007 | incident_vector: None or tuple 1008 | vector describing the incident electric field. It is multiplied by the 1009 | amplitude scattering matrix to find the vector scattering amplitude. If 1010 | coordinate_system is 'scattering plane', then this vector should be in 1011 | the 'scattering plane' basis, where the first element is the parallel 1012 | component and the second element is the perpendicular component. If 1013 | coordinate_system is 'cartesian', then this vector should be in the 1014 | 'cartesian' basis, where the first element is the x-component and the 1015 | second element is the y-component. Note that the vector for unpolarized 1016 | light is the same in either basis, since either way it should be an 1017 | equal mix between the two othogonal polarizations: (1,1). Note that if 1018 | incident_vector is None, the function assigns a value based on the 1019 | coordinate system. For 'scattering plane', the assigned value is (1,1) 1020 | because most scattering plane calculations we're interested in involve 1021 | unpolarized light. For 'cartesian', the assigned value is (1,0) because 1022 | if we are going to the trouble to use the cartesian coordinate system, 1023 | it is usually because we want to do calculations using polarization, 1024 | and these calculations are much easier to convert to measured 1025 | quantities when in the cartesian coordinate system. 1026 | 1027 | Returns 1028 | ------- 1029 | I components: tuple 1030 | tuple of the two orthogonal components of scattered intensity. If in 1031 | cartesian coordinate system, each component is a function of theta and 1032 | phi values. If in scattering plane coordinate system, each component 1033 | is an array of theta values (dimensionless).These intensities are 1034 | technically 'unitless.' The intensities would get their units from 1035 | the E_n term in the fields, which gets its units from an E_0 term, 1036 | which is taken to be 1 here. To get an intensity with real units 1037 | you would need to multiply these by |E_0|**2 where E_0 is the amplitude 1038 | of the incident wave at the origin. 1039 | 1040 | References 1041 | ---------- 1042 | C. F. Bohren and D. R. Huffman. Absorption and scattering of light by 1043 | small particles. Wiley-VCH (2004), chapter 4.4.1. 1044 | Q. Fu and W. Sun, "Mie theory for light scattering by a spherical particle 1045 | in an absorbing medium". Applied Optics, 40, 9 (2001). 1046 | 1047 | ''' 1048 | if isinstance(kd, Quantity): 1049 | kd = kd.to('') 1050 | 1051 | if near_field: 1052 | if coordinate_system == 'scattering plane': 1053 | # calculate scattered fields in scattering plane coordinate system 1054 | Es_theta, Es_phi, Hs_theta, Hs_phi = _scat_fields_complex_medium(m, 1055 | x,thetas, kd, near_field=near_field) 1056 | I_1 = Es_theta * np.conj(Hs_phi) # I_par 1057 | I_2 = -Es_phi * np.conj(Hs_theta) # I_perp 1058 | else: 1059 | raise ValueError('Near fields have not been implemented for the \ 1060 | specified coordinate system. set near_field to False to\ 1061 | calculate scattered intensity') 1062 | 1063 | 1064 | else: 1065 | # calculate vector scattering amplitude 1066 | vec_scat_amp_1, vec_scat_amp_2 = vector_scattering_amplitude(m, x, 1067 | thetas, 1068 | coordinate_system=coordinate_system, 1069 | phis=phis, 1070 | incident_vector = incident_vector) 1071 | 1072 | # calculate the intensities. We multiply by a factor that accounts for 1073 | # the dependence of the intensity on the distance away d from the 1074 | # scatterer, which is necessary when the medium is absorbing. The 1075 | # factor is derived from the multiplication of the exponential term 1076 | # (the asymptotic form at large d of the spherical Hankel equations, 1077 | # which account for near fields, see _scat_fields_complex_medium()) 1078 | # with its conjugate, assuming that k can be complex. The form reduces 1079 | # down to 1/(kd)^2 when k is real, which is the factor usually used to 1080 | # get the final intensity in a non-absorbing medium (p. 113 of Bohren 1081 | # and Huffman). 1082 | factor = np.exp(-2*kd.imag) / ((kd.real)**2 + (kd.imag)**2) 1083 | I_1 = (np.abs(vec_scat_amp_1)**2)*factor.to('') # par or x 1084 | I_2 = (np.abs(vec_scat_amp_2)**2)*factor.to('') # perp or y 1085 | 1086 | return I_1.real, I_2.real # the intensities should be real 1087 | 1088 | def integrate_intensity_complex_medium(I_1, I_2, distance, thetas, k, 1089 | phi_min=Quantity(0.0, 'rad'), 1090 | phi_max=Quantity(2*np.pi, 'rad'), 1091 | coordinate_system = 'scattering plane', 1092 | phis = None): 1093 | ''' 1094 | Calculates the scattering cross section by integrating the differential 1095 | scattered intensity at a distance of our choice in an absorbing medium. 1096 | Choosing the right distance is essential in an absorbing medium because the 1097 | differential scattering intensities decrease with increasing distance. 1098 | The integration is done over scattering angles theta and azimuthal angles 1099 | phi using the trapezoid rule. 1100 | 1101 | Parameters 1102 | ---------- 1103 | I_1, I_2: nd arrays 1104 | differential scattered intensities, can be functions of theta or of 1105 | theta and phi. If a function of theta and phi, the theta dimension MUST 1106 | come first 1107 | distance: float (Quantity in [length]) 1108 | distance away from the scatterer 1109 | thetas: nd array (Quantity in rad) 1110 | scattering angles 1111 | k: wavevector given by 2 * pi * n_medium / wavelength 1112 | (Quantity in [1/length]) 1113 | phi_min: float (Quantity in rad). 1114 | minimum azimuthal angle, default set to 0 1115 | optional, only necessary if coordinate_system is 'scattering plane' 1116 | phi_max: float (Quantity in rad). 1117 | maximum azimuthal angle, default set to 2pi 1118 | optional, only necessary if coordinate_system is 'scattering plane' 1119 | phis: None or ndarray 1120 | azimuthal angles 1121 | 1122 | Returns 1123 | ------- 1124 | sigma: float (in units of length**2) 1125 | integrated cross section 1126 | sigma_1: float (in units of length**2) 1127 | integrated cross section for first component of basis 1128 | sigma_2: float (in units of length**2) 1129 | integrated cross section for second component of basis 1130 | dsigma_1: ndarray (in units of length**2) 1131 | differential cross section for first component of basis 1132 | dsigma_2: ndarray (in units of length**2) 1133 | differential cross section for second component of basis 1134 | 1135 | ''' 1136 | # convert to radians from whatever units the user specifies 1137 | if isinstance(thetas, Quantity): 1138 | thetas = thetas.to('rad').magnitude 1139 | 1140 | # this line converts the unitless intensities to cross section 1141 | # Multiply by distance (= to radius of particle in montecarlo.py) because 1142 | # this is the integration factor over solid angles (see eq. 4.58 in 1143 | # Bohren and Huffman). 1144 | if isinstance(distance.magnitude,(list, np.ndarray)): 1145 | if distance[0]==distance[1]: 1146 | distance = distance[0] 1147 | dsigma_1 = I_1 * distance**2 1148 | dsigma_2 = I_2 * distance**2 1149 | 1150 | if coordinate_system == 'scattering plane': 1151 | if phis is not None: 1152 | warnings.warn('''azimuthal angles specified for scattering plane 1153 | calculations. Scattering plane calculations do not 1154 | depend on azimuthal angle, so specified values will 1155 | be ignored''') 1156 | 1157 | # convert to radians 1158 | phi_min = phi_min.to('rad').magnitude 1159 | phi_max = phi_max.to('rad').magnitude 1160 | 1161 | # strip units from integrand 1162 | if isinstance(dsigma_1, Quantity): 1163 | integrand_par = dsigma_1.magnitude * np.abs(np.sin(thetas)) 1164 | else: 1165 | integrand_par = dsigma_1 * np.abs(np.sin(thetas)) 1166 | if isinstance(dsigma_2, Quantity): 1167 | integrand_perp = dsigma_2.magnitude * np.abs(np.sin(thetas)) 1168 | else: 1169 | integrand_perp = dsigma_2 * np.abs(np.sin(thetas)) 1170 | 1171 | # Integrate over theta 1172 | integral_par = trapezoid(integrand_par, x=thetas) 1173 | integral_perp = trapezoid(integrand_perp, x=thetas) 1174 | 1175 | # restore units to integral 1176 | if isinstance(dsigma_1, Quantity): 1177 | integral_par = Quantity(integral_par, dsigma_1.units) 1178 | if isinstance(dsigma_2, Quantity): 1179 | integral_perp = Quantity(integral_perp, dsigma_2.units) 1180 | 1181 | # integrate over phi: multiply by factor to integrate over phi 1182 | # (this factor is the integral of cos(phi)**2 and sin(phi)**2 in 1183 | # parallel and perpendicular polarizations, respectively) 1184 | sigma_1 = (integral_par * (phi_max/2 + np.sin(2*phi_max)/4 - 1185 | phi_min/2 - np.sin(2*phi_min)/4)) 1186 | sigma_2 = (integral_perp * (phi_max/2 - np.sin(2*phi_max)/4 - 1187 | phi_min/2 + np.sin(2*phi_min)/4)) 1188 | 1189 | elif coordinate_system == 'cartesian': 1190 | if phis is None: 1191 | raise ValueError('phis set to None, but azimuthal angle must be \ 1192 | specified for scattering calculations in \ 1193 | cartesian coordinate system') 1194 | 1195 | # convert to radians 1196 | if isinstance(phis, Quantity): 1197 | phis = phis.to('rad').magnitude 1198 | 1199 | # Integrate over theta and phi 1200 | thetas_bc = thetas.reshape((len(thetas),1)) # reshape for broadcasting 1201 | 1202 | # strip units from integrand 1203 | if isinstance(dsigma_1, Quantity): 1204 | integrand_1 = dsigma_1.magnitude * np.abs(np.sin(thetas_bc)) 1205 | else: 1206 | integrand_1 = dsigma_1 * np.abs(np.sin(thetas_bc)) 1207 | if isinstance(dsigma_2, Quantity): 1208 | integrand_2 = dsigma_2.magnitude * np.abs(np.sin(thetas_bc)) 1209 | else: 1210 | integrand_2 = dsigma_2 * np.abs(np.sin(thetas_bc)) 1211 | 1212 | sigma_1 = trapezoid(trapezoid(integrand_1, x=thetas, axis=0), x=phis) 1213 | sigma_2 = trapezoid(trapezoid(integrand_2, x=thetas, axis=0), x=phis) 1214 | 1215 | # restore units to integral 1216 | if isinstance(dsigma_1, Quantity): 1217 | sigma_1 = Quantity(sigma_1, dsigma_1.units) 1218 | if isinstance(dsigma_2, Quantity): 1219 | sigma_2 = Quantity(sigma_2, dsigma_2.units) 1220 | else: 1221 | raise ValueError('The coordinate system specified has not yet been \ 1222 | implemented. Change to \'cartesian\' or \'scattering plane\'') 1223 | 1224 | # multiply by factor that accounts for attenuation in the incident light 1225 | # (see Sudiarta and Chylek (2001), eq 10). 1226 | # if the imaginary part of k is close to 0 (because the medium index is 1227 | # close to 0), then use the limit value of factor for the calculations 1228 | if k.imag <= Quantity(1e-8, '1/nm'): 1229 | factor = 2 1230 | else: 1231 | exponent = np.exp(2*distance*k.imag) 1232 | factor = 1 / (exponent / (2*distance*k.imag)+ 1233 | (1 - exponent) / (2*distance*k.imag)**2) 1234 | 1235 | # calculate the averaged sigma 1236 | sigma = (sigma_1 + sigma_2)/2 * factor 1237 | 1238 | return(sigma, sigma_1*factor, sigma_2*factor, dsigma_1*factor/2, 1239 | dsigma_2*factor/2) 1240 | 1241 | def diff_abs_intensity_complex_medium(m, x, thetas, ktd): 1242 | ''' 1243 | Calculates the differential absorbed intensity as a function of scattering 1244 | angle theta when the medium has a non-zero imaginary component of the 1245 | refractive index. This differential absorbed intensity is computed by 1246 | substituting the internal electric and magnetic fields (from Fu and Sun) 1247 | into the radial component of the Poynting vector: 1248 | 1249 | I_par = -Et_theta * conj(Ht_phi) 1250 | I_perp = Et_phi * conj(Ht_theta) 1251 | 1252 | where conj() indicates the complex conjugate. The radial component of the 1253 | Poynting vector is then 1/2 * Re(I_par + I_perp). 1254 | 1255 | Parameters 1256 | ---------- 1257 | m: complex relative refractive index 1258 | x: size parameter using the medium's refractive index 1259 | thetas: array of scattering angles (Quantity in rad) 1260 | ktd: kt * distance, where kt = 2*np.pi*n_particle/wavelen, and distance is 1261 | the distance away from the center of the particle. The far-field 1262 | solution is obtained when distance >> radius. (Quantity, dimensionless) 1263 | 1264 | Returns 1265 | ------- 1266 | I_par, I_perp: differential absorption intensities for an array of theta 1267 | (dimensionless). 1268 | 1269 | Reference 1270 | --------- 1271 | Q. Fu and W. Sun, "Mie theory for light scattering by a spherical particle 1272 | in an absorbing medium". Applied Optics, 40, 9 (2001). 1273 | 1274 | ''' 1275 | # convert units from whatever units the user specifies 1276 | thetas = thetas.to('rad').magnitude 1277 | ktd = ktd.to('').magnitude 1278 | 1279 | # calculate mie coefficients 1280 | nstop = _nstop(np.array(x).max()) 1281 | n = np.arange(nstop)+1. 1282 | cn, dn = _internal_coeffs(m, x, nstop) 1283 | 1284 | # calculate prefactor (omitting the incident electric field because it 1285 | # cancels out when calculating the scattered intensity) 1286 | En = 1j**n * (2*n+1) / (n*(n+1)) 1287 | 1288 | # calculate spherical Bessel function and derivative 1289 | nstop_array = np.arange(0,nstop+1) 1290 | zn = spherical_jn(nstop_array, ktd) 1291 | zn = zn[1:] 1292 | 1293 | psi, _ = mie_specfuncs.riccati_psi_xi(ktd, nstop) 1294 | psishift = np.concatenate((np.zeros(1), psi))[0:nstop+1] 1295 | psi = psi[1:] 1296 | psishift = psishift[1:] 1297 | bessel_deriv = psishift - n*psi/ktd 1298 | 1299 | # calculate pis and taus at the scattering angles theta 1300 | pis, taus = _pis_and_taus(nstop, thetas) 1301 | 1302 | # calculate the scattered electric and magnetic fields (omitting the 1303 | # sin(phi) and cos(phi) factors because they will be accounted for when 1304 | # integrating to get the scattering cross section) 1305 | En = np.broadcast_to(En, [len(thetas), len(En)]) 1306 | cn = np.broadcast_to(cn, [len(thetas), len(cn)]) 1307 | dn = np.broadcast_to(dn, [len(thetas), len(dn)]) 1308 | zn = np.broadcast_to(zn, [len(thetas), len(zn)]) 1309 | bessel_deriv = np.broadcast_to(bessel_deriv, 1310 | [len(thetas),len(bessel_deriv)]) 1311 | 1312 | Et_theta = np.sum(En* (cn * pis * zn - 1j * dn * taus * bessel_deriv/ktd), 1313 | axis=1) # * cos(phi) 1314 | Et_phi = np.sum(En* (-cn * taus * zn + 1j * dn * pis * bessel_deriv/ktd), 1315 | axis=1) # * sin(phi) 1316 | Ht_theta = np.sum(En* (dn * pis * zn - 1j * cn * taus * bessel_deriv/ktd), 1317 | axis=1) # * sin(phi) 1318 | Ht_phi = np.sum(En* (dn * taus * zn - 1j * cn * pis * bessel_deriv/ktd), 1319 | axis=1) # * cos(phi) 1320 | 1321 | # calculate the scattered intensities 1322 | I_par = -m* Et_theta * np.conj(Ht_phi) 1323 | I_perp = m* Et_phi * np.conj(Ht_theta) 1324 | 1325 | return I_par.real, I_perp.real 1326 | 1327 | def amplitude_scattering_matrix(m, x, thetas, 1328 | coordinate_system = 'scattering plane', 1329 | phis = None): 1330 | """ 1331 | Calculates the amplitude scattering matrix for an n-dim array of thetas 1332 | (and phis if in cartesian coordinate system) 1333 | 1334 | Elements of the amplitude scattering matrix are arranged as: 1335 | [S2 S3] 1336 | [S4 S1] 1337 | 1338 | Change of basis from scattering plane to lab frame cartesian is calculated 1339 | by multiplying (M^-1)*S*M where S is the amplitude scattering matrix and 1340 | M is the change of basis matrix. The change of basis matrix M is: 1341 | 1342 | [cosphi sinphi] 1343 | [sinphi -cosphi] 1344 | 1345 | (from Bohren and Huffman, 3.2, page 61) 1346 | 1347 | This matrix is equal to it's inverse, so we can get the scattering matrix 1348 | in the cartesian coordinate system by multiplying: 1349 | 1350 | [cosphi sinphi] * [S2 S3] * [cosphi sinphi] 1351 | [sinphi -cosphi] [S4 S1] [sinphi -cosphi] 1352 | 1353 | in Mie theory, we have S3 = S4 = 0, so this simplifies to: 1354 | 1355 | = [cosphi sinphi] * [S2 0] * [cosphi sinphi] 1356 | [sinphi -cosphi] [0 S1] [sinphi -cosphi] 1357 | 1358 | = [cosphi sinphi] * [S2*cosphi S2*sinphi] 1359 | [sinphi -cosphi] [S1*sinphi -S1*cosphi] 1360 | 1361 | = [S2*cosphi**2 + S1*sinphi**2 S2*sinphi*cosphi - S1*sinphi*cosphi] 1362 | [S2*cosphi*sinphi-S1*cosphi*sinphi S2*sinphi**2 + S1*cosphi**2] 1363 | 1364 | see pages 22-23,51-53 in Annie Stephenson lab notebook #3 for orignal notes 1365 | 1366 | Parameters: 1367 | ---------- 1368 | m: float, or array 1369 | index ratio between the particle and sample, array if multilayer 1370 | particle 1371 | x: float, or array 1372 | size parameter, array if multilayer particle 1373 | thetas: nd array 1374 | theta angles 1375 | coordinate_system: string 1376 | default value 'scattering plane' means scattering calculations will be 1377 | carried out in the basis defined by basis vectors parallel and 1378 | perpendicular to scattering plane. Variable also accepts value 1379 | 'cartesian' which scattering calculations will be carried out in the 1380 | basis defined by basis vectors x and y in the lab frame, with z 1381 | as the direction of propagation. 1382 | phis: None or ndarray 1383 | azimuthal angles for which to calculate the scattering matrix. In the 1384 | 'scattering plane' coordinate system, the scattering matrix does not 1385 | depend on phi, so phi should be set to None. In the 'cartesian' 1386 | coordinate system, the scattering matrix does depend on phi, so an 1387 | array of values should be provided. 1388 | 1389 | Returns: 1390 | -------- 1391 | S1, S2, S3, S4: tuple of nd arrays 1392 | amplitude scattering matrix elements for all theta. S2 and S1 have the 1393 | same shape as theta. 1394 | """ 1395 | # calculate n-array 1396 | nstop = _nstop(np.array(x).max()) 1397 | n = np.arange(nstop)+1. 1398 | prefactor = (2*n+1)/(n*(n+1)) 1399 | 1400 | # calculate mie coefficients 1401 | # if the index ratio m is an array with more than 1 element, it's a 1402 | # multilayer particle 1403 | if len(np.atleast_1d(m)) > 1: 1404 | coeffs = msl.scatcoeffs_multi(m, x) 1405 | else: 1406 | coeffs = _scatcoeffs(m, x, nstop) 1407 | 1408 | # calculate amplitude scattering matrix in 'scattering plane' coordinate 1409 | # system 1410 | S2_sp, S1_sp = _amplitude_scattering_matrix(nstop, prefactor, 1411 | coeffs, thetas) 1412 | S3_sp = 0 1413 | S4_sp = 0 1414 | 1415 | if coordinate_system == 'cartesian': 1416 | # raise error if no phis are specified 1417 | if phis is None: 1418 | raise ValueError('phis set to None, but azimuthal angle must be \ 1419 | specified for scattering calculations in \ 1420 | cartesian coordinate system') 1421 | 1422 | # calculate sines and cosines 1423 | cosphi = np.cos(phis) 1424 | sinphi = np.sin(phis) 1425 | 1426 | # calculate elements of scattering matrix 1427 | S1_xy = S2_sp*(sinphi)**2 + S1_sp*(cosphi)**2 1428 | S2_xy = S2_sp*(cosphi)**2 + S1_sp*(sinphi)**2 1429 | S3_xy = S2_sp*sinphi*cosphi - S1_sp*sinphi*cosphi 1430 | S4_xy = S2_sp*cosphi*sinphi - S1_sp*cosphi*sinphi 1431 | 1432 | return S1_xy, S2_xy, S3_xy, S4_xy 1433 | elif coordinate_system == 'scattering plane': 1434 | if phis is not None: 1435 | warnings.warn('azimuthal angles specified for scattering plane \ 1436 | calculations. Scattering plane calculations do not \ 1437 | depend on azimuthal angle, so specified values will \ 1438 | be ignored') 1439 | return S1_sp, S2_sp, S3_sp, S4_sp 1440 | else: 1441 | raise ValueError('The coordinate system specified has not yet been \ 1442 | implemented. Change to \'cartesian\' or \'scattering plane\'') 1443 | 1444 | 1445 | def vector_scattering_amplitude(m, x, thetas, incident_vector = None, 1446 | coordinate_system = 'scattering plane', 1447 | phis = None): 1448 | ''' 1449 | Calculates the vector scattering amplitude for an nd array of thetas and 1450 | phis. For more info on the vector scattering amplitude and how to 1451 | calculate it, see Bohren and Huffman, pg 70-73 of section 3.4 Extinction, 1452 | Scattering, and Absorption. 1453 | 1454 | When coordinate_system == 'scattering plane', the default incident 1455 | electric field vector assumes that the incident light is unpolarized, 1456 | so it is equally split between the parallel and perpendicular components 1457 | of the electric field, so the vector scattering amplitude can be 1458 | calculated by: 1459 | 1460 | [S2 0] * [1] = [S2] 1461 | [0 S1] [1] [S1] 1462 | 1463 | where the vector is normalized after the multiplication 1464 | 1465 | When coordinate_system == 'cartesian', if the incident electric field 1466 | vector indicates the incident light is unpolarized, it is equally 1467 | split between the x and y components of the electric field, so the vector 1468 | scattering amplitude can be calculated by: 1469 | 1470 | [S2 S3] * [1] = [S2 + S3] 1471 | [S4 S1] [1] [S4 + S1] 1472 | 1473 | however, in most cases, if we are in the 'cartesian' coordinate system, we 1474 | are more interested in calculating the the scattered light given and 1475 | initial polarization of the initial light, since this calculation cannot be 1476 | done in the 'scattering plane' coordinate system. If we assume the initial 1477 | polarization is in the +x direction, the vector scattering amplitude will 1478 | be: 1479 | 1480 | [S2 S3] * [1] = [S2] 1481 | [S4 S1] [0] = [S4] 1482 | 1483 | where the vector is normalized after the multiplication. The default value 1484 | is then +x when set to incident_vector is set to None. 1485 | 1486 | Parameters: 1487 | ---------- 1488 | m: float 1489 | index ratio between the particle and sample 1490 | x: float 1491 | size parameter 1492 | thetas: nd array 1493 | scattering angles 1494 | incident_vector: None or tuple 1495 | vector describing the incident electric field. It is multiplied by the 1496 | amplitude scattering matrix to find the vector scattering amplitude. If 1497 | coordinate_system is 'scattering plane', then this vector should be in 1498 | the 'scattering plane' basis, where the first element is the parallel 1499 | component and the second element is the perpendicular component. If 1500 | coordinate_system is 'cartesian', then this vector should be in the 1501 | 'cartesian' basis, where the first element is the x-component and the 1502 | second element is the y-component. Note that the vector for unpolarized 1503 | light is the same in either basis, since either way it should be an 1504 | equal mix between the two othogonal polarizations: (1,1). Note that if 1505 | incident_vector is None, the function assigns a value based on the 1506 | coordinate system. For 'scattering plane', the assigned value is (1,1) 1507 | because most scattering plane calculations we're interested in involve 1508 | unpolarized light. For 'cartesian', the assigned value is (1,0) because 1509 | if we are going to the trouble to use the cartesian coordinate system, 1510 | it is usually because we want to do calculations using polarization, 1511 | and these calculations are much easier to convert to measured 1512 | quantities when in the cartesian coordinate system. 1513 | coordinate_system: string 1514 | describes the coordinate system. Can be either 'scattering plane' or 1515 | 'cartesian' 1516 | phis: nd array or None 1517 | azimuthal angles 1518 | 1519 | 1520 | Returns: 1521 | -------- 1522 | vector scattering amplitude: tuple 1523 | tuple describing the vector scattering amplitude in the specified 1524 | coordinate system. not normalized. 1525 | ''' 1526 | # calculate the amplitude scattering matrix 1527 | S1, S2, S3, S4 = amplitude_scattering_matrix(m, x, thetas, 1528 | coordinate_system = \ 1529 | coordinate_system, 1530 | phis = phis) 1531 | 1532 | if coordinate_system == 'scattering plane': 1533 | if incident_vector is None: 1534 | incident_vector = (1,1) # assume unpolarized 1535 | vec_scat_amp_par = S2*incident_vector[0] 1536 | vec_scat_amp_perp = S1*incident_vector[1] 1537 | 1538 | return vec_scat_amp_par, vec_scat_amp_perp 1539 | 1540 | else: 1541 | if incident_vector is None: 1542 | incident_vector = (1,0) # assume x-polarized 1543 | vec_scat_amp_x = S2*incident_vector[0] + S3*incident_vector[1] 1544 | vec_scat_amp_y = S4*incident_vector[0] + S1*incident_vector[1] 1545 | 1546 | return vec_scat_amp_x, vec_scat_amp_y 1547 | 1548 | 1549 | def _amplitude_scattering_matrix(n_stop, prefactor, coeffs, thetas): 1550 | # amplitude scattering matrix from Mie coefficients 1551 | pis, taus = _pis_and_taus(n_stop, thetas) 1552 | S1 = np.sum(prefactor*(coeffs[0]*pis + coeffs[1]*taus), axis=-1) 1553 | S2 = np.sum(prefactor*(coeffs[0]*taus + coeffs[1]*pis), axis=-1) 1554 | return S2, S1 1555 | 1556 | def _amplitude_scattering_matrix_RG(prefactor, x, thetas): 1557 | # amplitude scattering matrix from Rayleigh-Gans approximation 1558 | u = 2 * x * np.sin(thetas/2.) 1559 | S1 = prefactor * (3./u**3) * (np.sin(u) - u*np.cos(u)) 1560 | S2 = prefactor * (3./u**3) * (np.sin(u) - u*np.cos(u)) * np.cos(thetas) 1561 | return S2, S1 1562 | 1563 | 1564 | 1565 | # TODO: copy multilayer code from multilayer_sphere_lib.py in holopy and 1566 | # integrate with functions for calculating scattering cross sections and form 1567 | # factor. 1568 | -------------------------------------------------------------------------------- /pymie/mie_specfuncs.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011-2013, Vinothan N. Manoharan, Thomas G. Dimiduk, 2 | # Rebecca W. Perry, Jerome Fung, Ryan McGorty, and Anna Wang 3 | # 4 | # This file is part of the python-mie 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 | Compute special functions needed for the computation of scattering coefficients 20 | in the Lorenz-Mie scattering solution and related problems such as layered 21 | spheres. 22 | 23 | Notes 24 | ----- 25 | Forked from holopy on 16 Aug 2016; added python version of lentz_dn1 and 26 | changed log_der_1 to dn_1_down, following the code in mieangfuncs.f90. These 27 | changes implement the Lentz continued fraction algorithm. 28 | 29 | These functions are not to be used for calculations at each field point. 30 | Rather, they should be used once for the calculation of scattering 31 | coefficients, which can then be used for field calculations. 32 | 33 | References 34 | ---------- 35 | [1] D. W. Mackowski, R. A. Altenkirch, and M. P. Menguc, "Internal absorption 36 | cross sections in a stratified sphere," Applied Optics 29, 1551-1559, (1990). 37 | 38 | [2] Yang, "Improved recursive algorithm for light scattering by a multilayered 39 | sphere," Applied Optics 42, 1710-1720, (1993). 40 | 41 | .. moduleauthor:: Jerome Fung 42 | .. moduleauthor:: Vinothan N. Manoharan 43 | """ 44 | import numpy as np 45 | from numpy import arange, array, exp, imag, real, sin, zeros 46 | from scipy.special import riccati_jn, riccati_yn, spherical_jn, spherical_yn 47 | 48 | # default tolerances 49 | DEFAULT_EPS1 = 1e-3 50 | DEFAULT_EPS2 = 1e-16 51 | 52 | def riccati_psi_xi(x, nstop): 53 | """ 54 | Construct riccati hankel function of 1st kind by linear combination of 55 | RB's based on j_n and y_n 56 | """ 57 | if np.imag(x) != 0.: 58 | # if x is complex, calculate spherical bessel functions and compute the 59 | # complex riccati-bessel solutions 60 | nstop_array = np.arange(0,nstop+1) 61 | psin = spherical_jn(nstop_array, x)*x 62 | xin = psin + 1j*spherical_yn(nstop_array, x)*x 63 | rbh = array([psin, xin]) 64 | else: 65 | x = x.real 66 | psin = riccati_jn(nstop, x) 67 | # scipy sign on y_n consistent with B/H 68 | xin = psin[0] + 1j*riccati_yn(nstop, x)[0] 69 | rbh = array([psin[0], xin]) 70 | 71 | return rbh 72 | 73 | def lentz_dn1(z, n, eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 74 | """ 75 | Calculate logarithmic derivative D_n(z) of the Riccati-Bessel 76 | function for a single value of n using the Lentz (1976) 77 | continued fraction method. 78 | 79 | Notes 80 | ----- 81 | Implements check/workaround for ill-conditioning described under "Algorithm 82 | Improvement" in Lentz (1976); see also Wiscombe/NCAR Mie report. 83 | 84 | Parameters 85 | ---------- 86 | z: complex argument 87 | n: order of the logarithmic derivative 88 | eps1: value of continued fraction numerator or denominator 89 | triggering ill-conditioning workaround. Recommend 90 | 1e-3. 91 | eps2: converge when additional products in continued fraction 92 | differ by less than eps2 from 1. Recommend 1e-16. 93 | 94 | Returns 95 | ------- 96 | value of D_n(z) 97 | """ 98 | def a_i(i): 99 | return (-1.)**(i + 1) * 2. * (n + i - 0.5) / z 100 | 101 | numerator = a_i(2) + 1. / a_i(1) 102 | denominator = a_i(2) 103 | 104 | product = a_i(1) * numerator / denominator 105 | ratio = product 106 | 107 | ctr = 3 108 | 109 | while abs(product.real - 1) > eps2 or abs(product.imag) > eps2: 110 | ai = a_i(ctr) 111 | numerator = ai + 1. / numerator 112 | denominator = ai + 1. / denominator 113 | 114 | if abs(numerator / ai) < eps1 or abs(denominator / ai) < eps1: 115 | # ill conditioning 116 | xi1 = 1. + a_i(ctr + 1) * numerator 117 | xi2 = 1. + a_i(ctr + 1) * denominator 118 | ratio = ratio * xi1 / xi2 119 | #print('ill conditioned', ratio) 120 | numerator = a_i(ctr + 2) + numerator / xi1 121 | denominator = a_i(ctr + 2) + denominator / xi2 122 | ctr = ctr + 2 123 | product = numerator / denominator 124 | ratio = ratio * product 125 | ctr = ctr + 1 126 | return ratio - n / z 127 | 128 | def dn_1_down(z, nmx, nstop, start_val): 129 | ''' 130 | Computes logarithmic derivative of Riccati-Bessel function psi_n(z) 131 | by downward recursion as in BHMIE. 132 | 133 | Parameters 134 | ---------- 135 | z: complex argument 136 | nmx: order from which downward recursion begins. 137 | nstop: integer, maximum order 138 | start_val: value from which recursion begins 139 | 140 | Notes 141 | ----- 142 | psi_n(z) is related to the spherical Bessel function j_n(z). 143 | ''' 144 | dn = zeros(nmx+1, dtype = 'complex128') 145 | dn[nmx] = start_val 146 | 147 | for i in np.arange(nmx-1, -1, -1): 148 | dn[i] = (i+1.)/z - 1.0/(dn[i+1] + (i+1.)/z) 149 | return dn[0:nstop+1] 150 | 151 | 152 | def log_der_13(z, nstop, eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 153 | ''' 154 | Calculate logarithmic derivatives of Riccati-Bessel functions psi 155 | and xi for complex arguments. Riccati-Bessel conventions follow 156 | Bohren & Huffman. 157 | 158 | See Mackowski et al., Applied Optics 29, 1555 (1990). 159 | 160 | Parameters 161 | ---------- 162 | z: complex number 163 | nstop: maximum order of computation 164 | eps1: underflow criterion for Lentz continued fraction for Dn1 165 | eps2: convergence criterion for Lentz continued fraction for Dn1 166 | ''' 167 | z = np.complex128(z) # convert to double precision 168 | 169 | # Calculate Dn_1 (based on \psi(z)) using downward recursion. 170 | # See Mackowski eqn. 62 171 | #nmx = np.maximum(nstop, int(np.round_(np.absolute(z)))) + 25 172 | #dn1 = log_der_1(z, nmx, nstop) 173 | dn1 = dn_1_down(z, nstop + 1, nstop, lentz_dn1(z, nstop + 1, eps1, eps2)) 174 | 175 | # Calculate Dn_3 (based on \xi) by up recurrence 176 | # initialize 177 | dn3 = zeros(nstop+1, dtype = 'complex128') 178 | psixi = zeros(nstop+1, dtype = 'complex128') 179 | dn3[0] = 1.j 180 | psixi[0] = -1j*exp(1.j*z)*sin(z) 181 | for dindex in arange(1, nstop+1): 182 | # Mackowski eqn 63 183 | psixi[dindex] = psixi[dindex-1] * ( (dindex/z) - dn1[dindex-1]) * ( 184 | (dindex/z) - dn3[dindex-1]) 185 | # Mackowski eqn 64 186 | dn3[dindex] = dn1[dindex] + 1j/psixi[dindex] 187 | 188 | return dn1, dn3 189 | 190 | # calculate ratio of RB's defined in Yang eqn. 23 by up recursion relation 191 | def Qratio(z1, z2, nstop, dns1 = None, dns2 = None, 192 | eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 193 | ''' 194 | Calculate ratio of Riccati-Bessel functions defined in Yang eq. 23 195 | by up recursion. 196 | 197 | Logarithmic derivatives calculated automatically if not specified. 198 | ''' 199 | # convert z1 and z2 to 128 bit complex to prevent division problems 200 | z1 = np.complex128(z1) 201 | z2 = np.complex128(z2) 202 | 203 | if dns1 is None: 204 | logdersz1 = log_der_13(z1, nstop, eps1, eps2) 205 | logdersz2 = log_der_13(z2, nstop, eps1, eps2) 206 | d1z1 = logdersz1[0] 207 | d3z1 = logdersz1[1] 208 | d1z2 = logdersz2[0] 209 | d3z2 = logdersz2[1] 210 | else: 211 | d1z1 = dns1[0] 212 | d3z1 = dns1[1] 213 | d1z2 = dns2[0] 214 | d3z2 = dns2[1] 215 | 216 | qns = zeros(nstop+1, dtype = 'complex128') 217 | 218 | # initialize according to Yang eqn. 34 219 | a1 = real(z1) 220 | a2 = real(z2) 221 | b1 = imag(z1) 222 | b2 = imag(z2) 223 | qns[0] = exp(-2.*(b2-b1)) * (exp(-1j*2.*a1)-exp(-2.*b1)) / (exp(-1j*2.*a2) 224 | - exp(-2.*b2)) 225 | # Loop to do upwards recursion in eqn. 33 226 | for i in arange(1, nstop+1): 227 | qns[i] = qns[i-1]* (((d3z1[i] + i/z1) * (d1z2[i] + i/z2)) 228 | / ((d3z2[i] + i/z2) * (d1z1[i] + i/z1))) 229 | return qns 230 | 231 | def R_psi(z1, z2, nmax, eps1 = DEFAULT_EPS1, eps2 = DEFAULT_EPS2): 232 | ''' 233 | Calculate ratio of Riccati-Bessel function psi: psi(z1)/psi(z2). 234 | 235 | See Mackowski eqns. 65-66. 236 | ''' 237 | output = zeros(nmax + 1, dtype = 'complex128') 238 | output[0] = sin(z1) / sin(z2) 239 | dnz1 = dn_1_down(z1, nmax + 1, nmax, lentz_dn1(z1, nmax + 1, eps1, eps2)) 240 | dnz2 = dn_1_down(z2, nmax + 1, nmax, lentz_dn1(z2, nmax + 1, eps1, eps2)) 241 | 242 | # use up recursion 243 | for i in arange(1, nmax + 1): 244 | output[i] = output[i - 1] * (dnz2[i] + i / z2) / (dnz1[i] + i / z1) 245 | return output 246 | -------------------------------------------------------------------------------- /pymie/multilayer_sphere_lib.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011-2016, Vinothan N. Manoharan, Thomas G. Dimiduk, 2 | # Rebecca W. Perry, Jerome Fung, Ryan McGorty, Anna Wang, Solomon Barkley 3 | # 4 | # This file is part of the python-mie 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 | multilayer_sphere_lib.py 21 | 22 | Author: 23 | Jerome Fung (fung@physics.harvard.edu) 24 | 25 | Copied from holopy on 12 Sept 2017. Functions to calculate the scattering from 26 | a spherically symmetric particle with an arbitrary number of layers with 27 | different refractive indices. 28 | 29 | Key reference for multilayer algorithm: 30 | Yang, "Improved recursive algorithm for light scattering by a multilayered 31 | sphere," Applied Optics 42, 1710-1720, (1993). 32 | 33 | ''' 34 | 35 | import numpy as np 36 | 37 | try: 38 | from . import mie 39 | from .mie_specfuncs import Qratio, log_der_13, riccati_psi_xi 40 | except ImportError: 41 | pass 42 | 43 | def scatcoeffs_multi(marray, xarray, eps1 = 1e-3, eps2 = 1e-16): 44 | ''' 45 | Calculate scattered field expansion coefficients (in the Mie formalism) 46 | for a particle with an arbitrary number of spherically symmetric layers. 47 | 48 | Parameters 49 | ---------- 50 | marray : array_like, complex128 51 | array of layer indices, innermost first 52 | xarray : array_like, real 53 | array of layer size parameters (k * outer radius), innermost first 54 | eps1 : float, optional 55 | underflow criterion for Lentz continued fraction for Dn1 56 | eps2 : float, optional 57 | convergence criterion for Lentz continued fraction for Dn1 58 | 59 | Returns 60 | ------- 61 | scat_coeffs : ndarray (complex) 62 | Scattering coefficients 63 | ''' 64 | # ensure correct data types 65 | marray = np.array(marray, dtype = 'complex128') 66 | xarray = np.array(xarray, dtype = 'complex128') 67 | 68 | # sanity check: marray and xarray must be same size 69 | if marray.size != xarray.size: 70 | raise ValueError('Arrays of layer indices \ 71 | and size parameters must be the same length!') 72 | 73 | # need number of layers L 74 | nlayers = marray.size 75 | 76 | # calculate nstop based on outermost radius 77 | nstop = mie._nstop(xarray.max()) 78 | 79 | # initialize H_n^a and H_n^b in the core, see eqns. 12a and 13a 80 | intl = log_der_13(marray[0]*xarray[0], nstop, eps1, eps2)[0] 81 | hans = intl 82 | hbns = intl 83 | 84 | # lay is l-1 (index on layers used by Yang) 85 | for lay in np.arange(1, nlayers): 86 | z1 = marray[lay]*xarray[lay-1] # m_l x_{l-1} 87 | z2 = marray[lay]*xarray[lay] # m_l x_l 88 | 89 | # calculate logarithmic derivatives D_n^1 and D_n^3 90 | derz1s = log_der_13(z1, nstop, eps1, eps2) 91 | derz2s = log_der_13(z2, nstop, eps1, eps2) 92 | 93 | # calculate G1, G2, Gtilde1, Gtilde2 according to 94 | # eqns 26-29 95 | # using H^a_n and H^b_n from previous layer 96 | G1 = marray[lay]*hans - marray[lay-1]*derz1s[0] 97 | G2 = marray[lay]*hans - marray[lay-1]*derz1s[1] 98 | Gt1 = marray[lay-1]*hbns - marray[lay]*derz1s[0] 99 | Gt2 = marray[lay-1]*hbns - marray[lay]*derz1s[1] 100 | 101 | # calculate ratio Q_n^l for this layer 102 | Qnl = Qratio(z1, z2, nstop, dns1 = derz1s, dns2 = derz2s, eps1 = eps1, 103 | eps2 = eps2) 104 | 105 | # now calculate H^a_n and H^b_n in current layer 106 | # see eqns 24 and 25 107 | hans = (G2*derz2s[0] - Qnl*G1*derz2s[1]) / (G2 - Qnl*G1) 108 | hbns = (Gt2*derz2s[0] - Qnl*Gt1*derz2s[1]) / (Gt2 - Qnl*Gt1) 109 | # repeat for next layer 110 | 111 | # Relate H^a and H^b in the outer layer to the Mie scat coeffs 112 | # see Yang eqns 14 and 15 113 | psiandxi = riccati_psi_xi(xarray.max(), nstop) # n = 0 to nstop 114 | n = np.arange(nstop+1) 115 | psi = psiandxi[0] 116 | xi = psiandxi[1] 117 | # this doesn't bother to calculate psi/xi_{-1} correctly, 118 | # but OK since we're throwing out a_0, b_0 where it appears 119 | psishift = np.concatenate((np.zeros(1), psi))[0:nstop+1] 120 | xishift = np.concatenate((np.zeros(1), xi))[0:nstop+1] 121 | 122 | an = ((hans/marray[nlayers-1] + n/xarray[nlayers-1])*psi - psishift) / ( 123 | (hans/marray[nlayers-1] + n/xarray[nlayers-1])*xi - xishift) 124 | bn = ((hbns*marray[nlayers-1] + n/xarray[nlayers-1])*psi - psishift) / ( 125 | (hbns*marray[nlayers-1] + n/xarray[nlayers-1])*xi - xishift) 126 | return np.array([an[1:nstop+1], bn[1:nstop+1]]) # output begins at n=1 127 | -------------------------------------------------------------------------------- /pymie/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Vinothan N. Manoharan 2 | # 3 | # This file is part of the python-mie 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 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_2_sphere_allow_overlap.yaml: -------------------------------------------------------------------------------- 1 | {max: 1.7459357526495638, mean: 0.999060827609981, min: 0.33480141499066735, std: 0.15066013315104868} 2 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_dda_csg.yaml: -------------------------------------------------------------------------------- 1 | {max: 1.0005366090960874, mean: 0.9999906325889926, min: 0.9994683690303922, std: 0.0003507857472346805} 2 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_dda_csg_rotated_div.yaml: -------------------------------------------------------------------------------- 1 | {max: 1.000935566740021, mean: 1.0000168265814438, min: 0.9990313008603279, std: 0.000627081215288408} 2 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_dda_voxelated_complex.yaml: -------------------------------------------------------------------------------- 1 | {max: 1.1294484866989978, mean: 0.9977780976012643, min: 0.8611591740286973, std: 0.05217470484970986} 2 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_farfield_matricies.yaml: -------------------------------------------------------------------------------- 1 | {max: !complex '(22.749108552222705-9.762546244860781j)', mean: !complex '(2.491547585126993-0.8260254715859601j)', 2 | min: !complex '(-2.5739297913388652+1.3060176643556747j)', std: 6.847119833544576} 3 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_janus_dda.yaml: -------------------------------------------------------------------------------- 1 | {max: 1.1098569834542904, mean: 1.0035905160865395, min: 0.9366864939812133, std: 0.027809374972009925} 2 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_mie_multiple_fields.yaml: -------------------------------------------------------------------------------- 1 | {max: !complex '(0.3423440742449068+0.8116722942687284j)', mean: !complex '(-0.009927906736841696-3.676282010677288e-05j)', 2 | min: !complex '(-1.9027199658379985+1.2432447107723739j)', std: 0.14168445471411045} 3 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_mie_multiple_holo.yaml: -------------------------------------------------------------------------------- 1 | {max: 3.1023677286680638, mean: 0.9984291583785121, min: 0.07217091913696634, std: 0.2110798488085864} 2 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_mie_radiometric.yaml: -------------------------------------------------------------------------------- 1 | # Results calculated with BHMIE 2 | qscat: 3.6647 3 | qabs: 0.0030019 4 | qext: 3.6677 5 | costheta: 0.92701 -------------------------------------------------------------------------------- /pymie/tests/gold/gold_mie_scat_matrix.yaml: -------------------------------------------------------------------------------- 1 | # See Bohren & Huffman, p. 482, for theta = 45. Note that these quantities 2 | # are normalized in a funny way detailed in the BHMIE listing. 3 | S11: 0.0701845 4 | pol: 0.959953e-2 5 | S33: 0.959825 6 | S34: -0.280434 7 | 8 | 9 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_multilayer.yaml: -------------------------------------------------------------------------------- 1 | # Data from Yang, Applied Optics 2003, Table 3, cases 2-4. 2 | # We use Q_ext, Q_sca, and Q_back only. 3 | # The value for Q_back for case 2 has been changed to the value obtained 4 | # by increasing the order n from which 0-based down recurrence for the logarithmic 5 | # derivative Dn1 begins. (Now computed using Lentz continued fraction method.) 6 | - 7 | - 2.20718 8 | - 1.87320 9 | - 2.62509 10 | - 11 | - 2.09947 12 | - 1.29372 13 | - 0.19948 14 | - 15 | - 2.08933 16 | - 1.12213 17 | - 0.03399 18 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_shell.yaml: -------------------------------------------------------------------------------- 1 | {max: 1.2186879308893677, mean: 0.9965851940215087, min: 0.6191796977296266, std: 0.08197631749541447} 2 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_single_field.yaml: -------------------------------------------------------------------------------- 1 | {max: !complex '(0.2624960266447181-0.047211358207028056j)', mean: !complex '(-0.006480329215872162+0.000318938245911942j)', 2 | min: !complex '(-0.4634587818715449+0.08057064927467021j)', std: 0.10711769016717519} 3 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_single_holo.yaml: -------------------------------------------------------------------------------- 1 | {max: 1.3408710481763622, mean: 0.9888332979242959, min: 0.522825223093458, std: 0.14229041347350355} 2 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_tmatrix_cylinder.yaml: -------------------------------------------------------------------------------- 1 | {max: 1.9631506550222222, mean: 1.0032470193724627, min: 0.3688734330302049, std: 0.13272352556772724} 2 | 3 | -------------------------------------------------------------------------------- /pymie/tests/gold/gold_tmatrix_spheroid.yaml: -------------------------------------------------------------------------------- 1 | {max: 1.2824285849187202, mean: 0.9998322367033866, min: 0.7579572473346301, std: 0.061313581518483457} 2 | 3 | -------------------------------------------------------------------------------- /pymie/tests/test_mie.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Vinothan N. Manoharan 2 | # 3 | # This file is part of the python-mie 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 numpy.testing import assert_almost_equal, assert_array_almost_equal, assert_approx_equal 25 | from pint.errors import DimensionalityError 26 | import pytest 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 | pytest.raises(DimensionalityError, mie.calc_cross_sections, 61 | m, x, Quantity('0.25 J')) 62 | pytest.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 | def test_multilayer_spheres(): 177 | # test that form factors and cross sections are the same for a 178 | # non-multilayer particle and for an equivalent multilayer particle. 179 | 180 | # form factor and cross section for non-multilayer 181 | m = 1.15 182 | n_sample = Quantity(1.5, '') 183 | wavelen = Quantity('500.0 nm') 184 | angles = Quantity(np.linspace(np.pi/2, np.pi, 20), 'rad') 185 | radius = Quantity('100.0 nm') 186 | x = size_parameter(wavelen, n_sample, radius) 187 | 188 | f_par, f_perp = mie.calc_ang_dist(m, x, angles) 189 | cscat, cext, cabs, cback, asym = mie.calc_cross_sections(m, x, wavelen) 190 | 191 | # form factor and cross section for a multilayer particle with a core that 192 | # is the same as the non-multilayer and a shell thickness of zero 193 | marray = [1.15, 1.15] # layer index ratios, innermost first 194 | multi_radius = Quantity(np.array([100.0, 100.0]),'nm') 195 | xarray = size_parameter(wavelen, n_sample, multi_radius) 196 | 197 | f_par_multi, f_perp_multi = mie.calc_ang_dist(marray, xarray, angles) 198 | cscat_multi, cext_multi, cabs_multi, cback_multi, asym_multi = mie.calc_cross_sections(marray, xarray, wavelen) 199 | 200 | assert_array_almost_equal(f_par, f_par_multi) 201 | assert_array_almost_equal(f_perp, f_perp_multi) 202 | assert_array_almost_equal(cscat.to('um^2').magnitude, cscat_multi.to('um^2').magnitude) 203 | assert_array_almost_equal(cext.to('um^2').magnitude, cext_multi.to('um^2').magnitude) 204 | assert_array_almost_equal(cabs.to('um^2').magnitude, cabs_multi.to('um^2').magnitude) 205 | assert_array_almost_equal(cback.to('um^2').magnitude, cback_multi.to('um^2').magnitude) 206 | assert_array_almost_equal(asym.magnitude, asym_multi.magnitude) 207 | 208 | # form factor and cross section for a multilayer particle with a core that 209 | # is the same as the non-multilayer and a shell index matched with the 210 | # medium (vacuum) 211 | marray2 = [1.15, 1.] # layer index ratios, innermost first 212 | multi_radius2 = Quantity(np.array([100.0, 110.0]),'nm') 213 | xarray2 = size_parameter(wavelen, n_sample, multi_radius2) 214 | 215 | f_par_multi2, f_perp_multi2 = mie.calc_ang_dist(marray2, xarray2, angles) 216 | cscat_multi2, cext_multi2, cabs_multi2, cback_multi2, asym_multi2 = mie.calc_cross_sections(marray2, xarray2, wavelen) 217 | 218 | assert_array_almost_equal(f_par, f_par_multi2) 219 | assert_array_almost_equal(f_perp, f_perp_multi2) 220 | assert_array_almost_equal(cscat.to('um^2').magnitude, cscat_multi2.to('um^2').magnitude) 221 | assert_array_almost_equal(cext.to('um^2').magnitude, cext_multi2.to('um^2').magnitude) 222 | assert_array_almost_equal(cabs.to('um^2').magnitude, cabs_multi2.to('um^2').magnitude) 223 | assert_array_almost_equal(cback.to('um^2').magnitude, cback_multi2.to('um^2').magnitude) 224 | assert_array_almost_equal(asym.magnitude, asym_multi2.magnitude) 225 | 226 | # form factor and cross section for a 3-layer-particle with a core that 227 | # is the same as the non-multilayer and shell thicknesses of zero 228 | marray3 = [1.15, 1.15, 1.15] # layer index ratios, innermost first 229 | multi_radius3 = Quantity(np.array([100.0, 100.0, 100.0]),'nm') 230 | xarray3 = size_parameter(wavelen, n_sample, multi_radius3) 231 | 232 | f_par_multi3, f_perp_multi3 = mie.calc_ang_dist(marray3, xarray3, angles) 233 | cscat_multi3, cext_multi3, cabs_multi3, cback_multi3, asym_multi3 = mie.calc_cross_sections(marray3, xarray3, wavelen) 234 | 235 | assert_array_almost_equal(f_par, f_par_multi3) 236 | assert_array_almost_equal(f_perp, f_perp_multi3) 237 | assert_array_almost_equal(cscat.to('um^2').magnitude, cscat_multi3.to('um^2').magnitude) 238 | assert_array_almost_equal(cext.to('um^2').magnitude, cext_multi3.to('um^2').magnitude) 239 | assert_array_almost_equal(cabs.to('um^2').magnitude, cabs_multi3.to('um^2').magnitude) 240 | assert_array_almost_equal(cback.to('um^2').magnitude, cback_multi3.to('um^2').magnitude) 241 | assert_array_almost_equal(asym.magnitude, asym_multi3.magnitude) 242 | 243 | # form factor and cross section for a 3-layer-particle with a core that 244 | # is the same as the non-multilayer and a shell index matched with the 245 | # medium (vacuum) 246 | marray4= [1.15, 1., 1.] # layer index ratios, innermost first 247 | multi_radius4 = Quantity(np.array([100, 110, 120]),'nm') 248 | xarray4 = size_parameter(wavelen, n_sample, multi_radius4) 249 | 250 | f_par_multi4, f_perp_multi4 = mie.calc_ang_dist(marray4, xarray4, angles) 251 | cscat_multi4, cext_multi4, cabs_multi4, cback_multi4, asym_multi4 = mie.calc_cross_sections(marray4, xarray4, wavelen) 252 | 253 | assert_array_almost_equal(f_par, f_par_multi4) 254 | assert_array_almost_equal(f_perp, f_perp_multi4) 255 | assert_array_almost_equal(cscat.to('um^2').magnitude, cscat_multi4.to('um^2').magnitude) 256 | assert_array_almost_equal(cext.to('um^2').magnitude, cext_multi4.to('um^2').magnitude) 257 | assert_array_almost_equal(cabs.to('um^2').magnitude, cabs_multi4.to('um^2').magnitude) 258 | assert_array_almost_equal(cback.to('um^2').magnitude, cback_multi4.to('um^2').magnitude) 259 | assert_array_almost_equal(asym.magnitude, asym_multi4.magnitude) 260 | 261 | def test_multilayer_absorbing_spheres(): 262 | # test that the form factor and cross sections are the same for a real 263 | # index ratio m and a complex index ratio with a 0 imaginary component 264 | marray_real = [1.15, 1.2] 265 | marray_imag = [1.15 + 0j, 1.2 + 0j] 266 | n_sample = Quantity(1.5, '') 267 | wavelen = Quantity('500.0 nm') 268 | multi_radius = Quantity(np.array([100.0, 110.0]),'nm') 269 | xarray = size_parameter(wavelen, n_sample, multi_radius) 270 | angles = Quantity(np.linspace(np.pi/2, np.pi, 20), 'rad') 271 | 272 | f_par_multi_real, f_perp_multi_real = mie.calc_ang_dist(marray_real, xarray, angles) 273 | f_par_multi_imag, f_perp_multi_imag = mie.calc_ang_dist(marray_imag, xarray, angles) 274 | 275 | cross_sections_multi_real = mie.calc_cross_sections(marray_real, xarray, wavelen) 276 | cross_sections_multi_imag = mie.calc_cross_sections(marray_imag, xarray, wavelen) 277 | 278 | assert_array_almost_equal(f_par_multi_real, f_par_multi_imag) 279 | assert_array_almost_equal(f_perp_multi_real, f_perp_multi_imag) 280 | assert_array_almost_equal(cross_sections_multi_real[0].to('um^2').magnitude, cross_sections_multi_imag[0].to('um^2').magnitude) 281 | assert_array_almost_equal(cross_sections_multi_real[1].to('um^2').magnitude, cross_sections_multi_imag[1].to('um^2').magnitude) 282 | assert_array_almost_equal(cross_sections_multi_real[2].to('um^2').magnitude, cross_sections_multi_imag[2].to('um^2').magnitude) 283 | assert_array_almost_equal(cross_sections_multi_real[3].to('um^2').magnitude, cross_sections_multi_imag[3].to('um^2').magnitude) 284 | assert_array_almost_equal(cross_sections_multi_real[4].magnitude, cross_sections_multi_imag[4].magnitude) 285 | 286 | def test_cross_section_Fu(): 287 | # Test that the cross sections match the Mie cross sections when there is 288 | # no absorption in the medium 289 | wavelen = Quantity('500.0 nm') 290 | radius = Quantity('200.0 nm') 291 | n_particle = Quantity(1.59, '') 292 | 293 | # Mie cross sections 294 | n_matrix1 = Quantity(1.33, '') 295 | m1 = index_ratio(n_particle, n_matrix1) 296 | x1 = size_parameter(wavelen, n_matrix1, radius) 297 | cscat1, cext1, cabs1, _, _ = mie.calc_cross_sections(m1, x1, wavelen/n_matrix1) 298 | 299 | # Fu cross sections 300 | n_matrix2 = Quantity(1.33, '') 301 | m2 = index_ratio(n_particle, n_matrix2) 302 | x2 = size_parameter(wavelen, n_matrix2, radius) 303 | x_scat = size_parameter(wavelen, n_particle, radius) 304 | nstop = mie._nstop(x2) 305 | coeffs = mie._scatcoeffs(m2, x2, nstop) 306 | internal_coeffs = mie._internal_coeffs(m2, x2, nstop) 307 | 308 | cscat2,cabs2,cext2 = mie._cross_sections_complex_medium_fu(coeffs[0],coeffs[1], 309 | internal_coeffs[0], 310 | internal_coeffs[1], 311 | radius, n_particle, 312 | n_matrix2, x_scat, 313 | x2, wavelen) 314 | 315 | assert_almost_equal(cscat1.to('um^2').magnitude, cscat2.to('um^2').magnitude, decimal=6) 316 | assert_almost_equal(cabs1.to('um^2').magnitude, cabs2.to('um^2').magnitude, decimal=6) 317 | assert_almost_equal(cext1.to('um^2').magnitude, cext2.to('um^2').magnitude, decimal=6) 318 | 319 | # Test that the cross sections match the Mie cross sections when there is 320 | # no absorption in the medium and there is absorption in the particle 321 | n_particle2 = Quantity(1.59 + 0.01j, '') 322 | 323 | # Mie cross sections 324 | n_matrix1 = Quantity(1.33, '') 325 | m1 = index_ratio(n_particle2, n_matrix1) 326 | x1 = size_parameter(wavelen, n_matrix1, radius) 327 | cscat3, cext3, cabs3, _, _ = mie.calc_cross_sections(m1, x1, wavelen/n_matrix1) 328 | 329 | # Fu cross sections 330 | n_matrix2 = Quantity(1.33, '') 331 | m2 = index_ratio(n_particle2, n_matrix2) 332 | x2 = size_parameter(wavelen, n_matrix2, radius) 333 | x_scat = size_parameter(wavelen, n_particle2, radius) 334 | nstop = mie._nstop(x2) 335 | coeffs = mie._scatcoeffs(m2, x2, nstop) 336 | internal_coeffs = mie._internal_coeffs(m2, x2, nstop) 337 | 338 | cscat4,cabs4,cext4 = mie._cross_sections_complex_medium_fu(coeffs[0],coeffs[1], 339 | internal_coeffs[0], 340 | internal_coeffs[1], 341 | radius, n_particle2, 342 | n_matrix2, x_scat, 343 | x2, wavelen) 344 | 345 | assert_almost_equal(cscat3.to('um^2').magnitude, cscat4.to('um^2').magnitude, decimal=6) 346 | assert_almost_equal(cabs3.to('um^2').magnitude, cabs4.to('um^2').magnitude, decimal=6) 347 | assert_almost_equal(cext3.to('um^2').magnitude, cext4.to('um^2').magnitude, decimal=6) 348 | 349 | def test_cross_section_Sudiarta(): 350 | # Test that the cross sections match the Mie cross sections when there is 351 | # no absorption in the medium 352 | wavelen = Quantity('500.0 nm') 353 | radius = Quantity('200.0 nm') 354 | n_particle = Quantity(1.59, '') 355 | 356 | # Mie cross sections 357 | n_matrix1 = Quantity(1.33, '') 358 | m1 = index_ratio(n_particle, n_matrix1) 359 | x1 = size_parameter(wavelen, n_matrix1, radius) 360 | cscat1, cext1, cabs1, _, _ = mie.calc_cross_sections(m1, x1, wavelen/n_matrix1) 361 | 362 | # Sudiarta cross sections 363 | n_matrix2 = Quantity(1.33, '') 364 | m2 = index_ratio(n_particle, n_matrix2) 365 | x2 = size_parameter(wavelen, n_matrix2, radius) 366 | nstop = mie._nstop(x2) 367 | coeffs = mie._scatcoeffs(m2, x2, nstop) 368 | 369 | cscat2, cabs2, cext2 = mie._cross_sections_complex_medium_sudiarta(coeffs[0], 370 | coeffs[1], 371 | x2, radius) 372 | 373 | assert_almost_equal(cscat1.to('um^2').magnitude, cscat2.to('um^2').magnitude, decimal=6) 374 | assert_almost_equal(cabs1.to('um^2').magnitude, cabs2.to('um^2').magnitude, decimal=6) 375 | assert_almost_equal(cext1.to('um^2').magnitude, cext2.to('um^2').magnitude, decimal=6) 376 | 377 | # Test that the cross sections match the Mie cross sections when there is 378 | # no absorption in the medium and there is absorption in the particle 379 | n_particle2 = Quantity(1.59 + 0.01j, '') 380 | 381 | # Mie cross sections 382 | n_matrix1 = Quantity(1.33, '') 383 | m1 = index_ratio(n_particle2, n_matrix1) 384 | x1 = size_parameter(wavelen, n_matrix1, radius) 385 | cscat3, cext3, cabs3, _, _ = mie.calc_cross_sections(m1, x1, wavelen/n_matrix1) 386 | 387 | # Fu cross sections 388 | n_matrix2 = Quantity(1.33, '') 389 | m2 = index_ratio(n_particle2, n_matrix2) 390 | x2 = size_parameter(wavelen, n_matrix2, radius) 391 | nstop = mie._nstop(x2) 392 | coeffs = mie._scatcoeffs(m2, x2, nstop) 393 | 394 | cscat4, cabs4, cext4 = mie._cross_sections_complex_medium_sudiarta(coeffs[0], 395 | coeffs[1], 396 | x2, radius) 397 | 398 | assert_almost_equal(cscat3.to('um^2').magnitude, cscat4.to('um^2').magnitude, decimal=6) 399 | assert_almost_equal(cabs3.to('um^2').magnitude, cabs4.to('um^2').magnitude, decimal=6) 400 | assert_almost_equal(cext3.to('um^2').magnitude, cext4.to('um^2').magnitude, decimal=6) 401 | 402 | def test_pis_taus(): 403 | ''' 404 | Checks that the vectorized pis_and_taus matches the scalar pis_and_taus 405 | ''' 406 | 407 | # number of terms to keep 408 | nstop = 3 409 | 410 | # check that result for a vector input matches the result for a scalar input 411 | theta = np.pi/4 412 | pis, taus = mie._pis_and_taus(nstop,theta) 413 | pis_v, taus_v = mie._pis_and_taus(nstop, np.array(theta)) 414 | 415 | assert_almost_equal(pis, pis_v) 416 | assert_almost_equal(taus, taus_v) 417 | 418 | # check that result for a vector input matches the result for a scalar input 419 | # for a theta 1d array 420 | theta = np.array([np.pi/4, np.pi/2, np.pi/3]) 421 | pis_v, taus_v = mie._pis_and_taus(nstop, theta) 422 | pis = np.zeros((len(theta), nstop)) 423 | taus = np.zeros((len(theta), nstop)) 424 | for i in range(len(theta)): 425 | pis[i,:], taus[i,:] = mie._pis_and_taus(nstop, theta[i]) 426 | 427 | assert_almost_equal(pis, pis_v) 428 | assert_almost_equal(taus, taus_v) 429 | 430 | # check that result for a vector input matches the result for a scalar input 431 | # for a theta 2d array 432 | theta = np.array([[np.pi/4, np.pi/2, np.pi/3],[np.pi/6, np.pi/4, np.pi/2]]) 433 | pis_v, taus_v = mie._pis_and_taus(nstop, theta) 434 | pis = np.zeros((theta.shape[0], theta.shape[1], nstop)) 435 | taus = np.zeros((theta.shape[0], theta.shape[1], nstop)) 436 | for i in range(theta.shape[0]): 437 | for j in range(theta.shape[1]): 438 | pis[i,j,:], taus[i,j,:] = mie._pis_and_taus(nstop, theta[i,j]) 439 | 440 | assert_almost_equal(pis, pis_v) 441 | assert_almost_equal(taus, taus_v) 442 | 443 | 444 | def test_cross_section_complex_medium(): 445 | 446 | # test that the cross sections calculated with the Mie solutions in absorbing 447 | # medium match the far-field Mie solutions and Sudiarta and Fu's solutions 448 | # when there is no absorption in the medium 449 | 450 | # set parameters 451 | wavelen = Quantity('400.0 nm') 452 | n_particle = Quantity(1.5+0.01j,'') 453 | n_matrix = Quantity(1.0,'') 454 | radius = Quantity(150.0,'nm') 455 | theta = Quantity(np.linspace(0, np.pi, 1000), 'rad')#1000 456 | distance = Quantity(10000.0,'nm') 457 | 458 | 459 | m = index_ratio(n_particle, n_matrix) 460 | k = 2*np.pi*n_matrix/wavelen 461 | x = size_parameter(wavelen, n_matrix, radius) 462 | nstop = mie._nstop(x) 463 | coeffs = mie._scatcoeffs(m, x, nstop) 464 | 465 | # With far-field Mie solutions 466 | cscat_mie = mie.calc_cross_sections(m, x, wavelen/n_matrix)[0] 467 | 468 | # With Sudiarta 469 | cscat_sudiarta = mie._cross_sections_complex_medium_sudiarta(coeffs[0], 470 | coeffs[1], 471 | x, radius)[0] 472 | # With Fu 473 | x_scat = size_parameter(wavelen, n_particle, radius) 474 | internal_coeffs = mie._internal_coeffs(m, x, nstop) 475 | cscat_fu = mie._cross_sections_complex_medium_fu(coeffs[0], coeffs[1], 476 | internal_coeffs[0], 477 | internal_coeffs[1], 478 | radius, n_particle, 479 | n_matrix, x_scat, x, 480 | wavelen)[0] 481 | # With Mie solutions in absorbing medium 482 | rho_scat = k*distance 483 | I_par_scat, I_perp_scat = mie.diff_scat_intensity_complex_medium(m, x, theta, 484 | rho_scat) 485 | cscat_exact = mie.integrate_intensity_complex_medium(I_par_scat, I_perp_scat, 486 | distance, theta, k)[0] 487 | 488 | # check that intensity equations without the asymptotic form of the spherical 489 | # Hankel equations (because they simplify when the fields are multiplied by 490 | # their conjugates to get the intensity) matches old result before simplifying 491 | cscat_exact_old = 0.15417313385938064 492 | assert_almost_equal(cscat_exact_old, cscat_exact.to('um^2').magnitude, decimal=15) 493 | 494 | assert_almost_equal(cscat_exact.to('um^2').magnitude, cscat_mie.to('um^2').magnitude, decimal=6) 495 | assert_almost_equal(cscat_exact.to('um^2').magnitude, cscat_sudiarta.to('um^2').magnitude, decimal=6) 496 | assert_almost_equal(cscat_exact.to('um^2').magnitude, cscat_fu.to('um^2').magnitude, decimal=6) 497 | 498 | 499 | # test that the cross sections calculated with the full Mie solutions 500 | # match the near field Sudiarta and Fu's solutions when there is absorption 501 | # in the medium 502 | n_matrix = Quantity(1.0+0.001j,'') 503 | distance = Quantity(radius.magnitude,'nm') 504 | 505 | m = index_ratio(n_particle, n_matrix) 506 | k = 2*np.pi*n_matrix/wavelen 507 | x = size_parameter(wavelen, n_matrix, radius) 508 | nstop = mie._nstop(x) 509 | coeffs = mie._scatcoeffs(m, x, nstop) 510 | 511 | # With Sudiarta 512 | cscat_sudiarta2 = mie._cross_sections_complex_medium_sudiarta(coeffs[0], 513 | coeffs[1], x, 514 | radius)[0] 515 | # With Fu 516 | x_scat = size_parameter(wavelen, n_particle, radius) 517 | internal_coeffs = mie._internal_coeffs(m, x, nstop) 518 | cscat_fu2 = mie._cross_sections_complex_medium_fu(coeffs[0], coeffs[1], 519 | internal_coeffs[0], 520 | internal_coeffs[1], 521 | radius, n_particle, 522 | n_matrix, x_scat, x, 523 | wavelen)[0] 524 | # With full Mie solutions that include the near fields 525 | rho_scat = k*distance 526 | I_par_scat, I_perp_scat = mie.diff_scat_intensity_complex_medium(m, x, theta, 527 | rho_scat, near_field=True) 528 | cscat_exact2 = mie.integrate_intensity_complex_medium(I_par_scat, I_perp_scat, 529 | distance, theta, k)[0] 530 | 531 | assert_almost_equal(cscat_exact2.to('um^2').magnitude, cscat_sudiarta2.to('um^2').magnitude, decimal=5) 532 | assert_almost_equal(cscat_exact2.to('um^2').magnitude, cscat_fu2.to('um^2').magnitude, decimal=5) 533 | 534 | # test that the cross sections calculated with the full Mie solutions 535 | # match the far-field Mie solutions when the matrix absorption is close to 0 536 | n_matrix = Quantity(1.0+0.0000001j,'') 537 | m = index_ratio(n_particle, n_matrix) 538 | k = 2*np.pi*n_matrix/wavelen 539 | x = size_parameter(wavelen, n_matrix, radius) 540 | rho_scat = k*distance 541 | 542 | # With full Mie solutions 543 | I_par_scat, I_perp_scat = mie.diff_scat_intensity_complex_medium(m, x, theta, 544 | rho_scat) 545 | 546 | cscat_exact3 = mie.integrate_intensity_complex_medium(I_par_scat, I_perp_scat, 547 | distance, theta, k)[0] 548 | 549 | # With far-field Mie solutions 550 | cscat_mie3 = mie.calc_cross_sections(m, x, wavelen/n_matrix)[0] 551 | 552 | # check that intensity equations without the asymptotic form of the spherical 553 | # Hankel equations (because they simplify when the fields are multiplied by 554 | # their conjugates to get the intensity) matches old result before simplifying 555 | cscat_exact_old3 = 0.15417310571064319 556 | assert_almost_equal(cscat_exact_old3, cscat_exact3.to('um^2').magnitude, decimal=15) 557 | 558 | assert_almost_equal(cscat_exact3.to('um^2').magnitude, cscat_mie3.to('um^2').magnitude, decimal=6) 559 | 560 | 561 | def test_multilayer_complex_medium(): 562 | # test that the form factor and cross sections are the same for a real 563 | # index ratio m and a complex index ratio with a 0 imaginary component 564 | marray = [1.15, 1.2] 565 | n_sample = Quantity(1.5 + 0j, '') 566 | wavelen = Quantity('500.0 nm') 567 | multi_radius = Quantity(np.array([100, 110]),'nm') 568 | xarray = size_parameter(wavelen, n_sample, multi_radius) 569 | angles = Quantity(np.linspace(0, np.pi, 10000), 'rad') 570 | distance = Quantity(100000000.0,'nm') 571 | k = 2*np.pi*n_sample/wavelen 572 | kd = k*distance 573 | 574 | # With far-field Mie solutions 575 | cscat_real = mie.calc_cross_sections(marray, xarray, wavelen/n_sample)[0] 576 | 577 | # with imag solutions 578 | I_par_multi, I_perp_multi = mie.diff_scat_intensity_complex_medium(marray, xarray, angles, kd) 579 | cscat_imag = mie.integrate_intensity_complex_medium(I_par_multi, I_perp_multi, 580 | distance, angles, k)[0] 581 | 582 | # check that intensity equations without the asymptotic form of the spherical 583 | # Hankel equations (because they simplify when the fields are multiplied by 584 | # their conjugates to get the intensity) matches old result before simplifying 585 | cscat_imag_old = 6275.240019849266 586 | assert_almost_equal(cscat_imag_old, cscat_imag.magnitude, decimal=11) 587 | 588 | assert_array_almost_equal(cscat_real.magnitude, cscat_imag.magnitude, decimal=3) 589 | 590 | 591 | def test_vector_scattering_amplitude_2d_theta_cartesian(): 592 | ''' 593 | Test that the amplitude scattering vector assuming x-polarized incident 594 | light calculated by amp_scat_vec_2d_theta_xy() matches what we get by doing 595 | the matrix multiplication manually 596 | 597 | amp_scat_vec_2d_theta_xy() converts from the parallel/perpendicular basis 598 | by doing the change of basis matrix multiplication in the function 599 | 600 | Here, we carry out the change of basis manually and plug in the numbers 601 | to make sure the two methods match. 602 | 603 | [as_vec_x] = [cos(phi) sin(phi)] * [S2 0] * [cos(phi) sin(phi)] * [1] 604 | [as_vec_y] [sin(phi) -cos(phi)] [0 S1] [sin(phi) -cos(phi)] [0] 605 | 606 | = [S2cos(phi)^2 + S1sin(phi)^2] 607 | [S2cos(phi)sin(phi) - S1cos(phi)sin(phi)] 608 | 609 | ''' 610 | 611 | # parameters of sample and source 612 | wavelen = Quantity('658.0 nm') 613 | radius = Quantity('0.85 um') 614 | n_matrix = Quantity(1.00, '') 615 | #n_particle = Quantity(1.59 + 1e-4 * 1.0j, '') 616 | n_particle = Quantity(1.59, '') 617 | thetas = Quantity(np.linspace(np.pi/2, np.pi, 2), 'rad') 618 | phis = Quantity(np.linspace(0, 2*np.pi, 4), 'rad') 619 | thetas_2d, phis_2d = np.meshgrid(thetas, phis) # be careful with meshgrid shape. 620 | 621 | # parameters for calculating scattering 622 | m = index_ratio(n_particle, n_matrix) 623 | x = size_parameter(wavelen, n_matrix, radius) 624 | 625 | # calculate the amplitude scattering matrix in xy basis 626 | as_vec_x0, as_vec_y0 = mie.vector_scattering_amplitude(m, x, thetas_2d, 627 | coordinate_system = 'cartesian', phis = phis_2d) 628 | 629 | # calcualte the amplitude scattering matrix in par/perp basis 630 | S1_sp, S2_sp, S3_sp, S4_sp = mie.amplitude_scattering_matrix(m, x, thetas_2d) 631 | 632 | as_vec_x = S2_sp*np.cos(phis_2d)**2 + S1_sp*np.sin(phis_2d)**2 633 | as_vec_y = S2_sp*np.cos(phis_2d)*np.sin(phis_2d) - S1_sp*np.cos(phis_2d)*np.sin(phis_2d) 634 | 635 | assert_almost_equal(as_vec_x0.magnitude, as_vec_x.magnitude) 636 | assert_almost_equal(as_vec_y0.magnitude, as_vec_y.magnitude) 637 | 638 | def test_diff_scat_intensity_complex_medium_cartesian(): 639 | ''' 640 | Test that the magnitude of the differential scattered intensity is the 641 | same in the xy basis as it is in the parallel, perpendicular basis, as 642 | long as the incident light is unpolarized for both 643 | 644 | This should be true because a rotation around phi brings the par/perp basis 645 | into the x,y basis 646 | ''' 647 | # parameters of sample and source 648 | wavelen = Quantity('658.0 nm') 649 | radius = Quantity('0.85 um') 650 | n_matrix = Quantity(1.00 + 1e-4* 1.0j, '') 651 | n_particle = Quantity(1.59 + 1e-4 * 1.0j, '') 652 | thetas = Quantity(np.linspace(np.pi/2, np.pi, 4), 'rad') 653 | phis = Quantity(np.linspace(0, 2*np.pi, 3), 'rad') 654 | thetas_2d, phis_2d = np.meshgrid(thetas, phis) # be careful with meshgrid shape. 655 | # for integration, theta dimension must always come first, 656 | # which is not how it is done here 657 | thetas_2d = Quantity(thetas_2d, 'rad') 658 | 659 | # parameters for calculating scattering 660 | m = index_ratio(n_particle, n_matrix) 661 | x = size_parameter(wavelen, n_matrix, radius) 662 | kd = 2*np.pi*n_matrix/wavelen*Quantity(10000.0,'nm') 663 | 664 | # calculate differential scattered intensity in par/perp basis 665 | I_par, I_perp = mie.diff_scat_intensity_complex_medium(m, x, thetas_2d, kd, 666 | near_field=False) 667 | 668 | # calculate differential scattered intensity in xy basis 669 | # if incident vector is unpolarized (1,1), then the resulting differential 670 | # scattered intensity should be the same as I_par, I_perp 671 | I_x, I_y = mie.diff_scat_intensity_complex_medium(m, x, thetas_2d, kd, 672 | coordinate_system = 'cartesian', phis = phis_2d, 673 | near_field=False, incident_vector = (1, 1)) 674 | 675 | # assert equality of their magnitudes 676 | I_xy_mag = np.sqrt(I_x**2 + I_y**2) 677 | I_par_perp_mag = np.sqrt(I_par**2 + I_perp**2) 678 | 679 | # check that the magnitudes are equal 680 | assert_array_almost_equal(I_xy_mag.magnitude, I_par_perp_mag.magnitude, decimal=16) 681 | 682 | def test_integrate_intensity_complex_medium_cartesian(): 683 | ''' 684 | Test that when integrated over all theta and phi angles, the intensities 685 | calculated in the par/perp basis match those calculated in the x/y basis 686 | ''' 687 | # parameters of sample and source 688 | wavelen = Quantity('658.0 nm') 689 | radius = Quantity('0.85 um') 690 | n_matrix = Quantity(1.00 + 1e-4* 1.0j, '') 691 | n_particle = Quantity(1.59 + 1e-4 * 1.0j, '') 692 | thetas = Quantity(np.linspace(0, np.pi, 500), 'rad') 693 | phis = Quantity(np.linspace(0, 2*np.pi, 550), 'rad') 694 | phis_2d, thetas_2d = np.meshgrid(phis, thetas) # remember, meshgrid shape is (len(thetas), len(phis)) 695 | # and theta dimension MUST come first in these calculations 696 | # parameters for calculating scattering 697 | m = index_ratio(n_particle, n_matrix) 698 | x = size_parameter(wavelen, n_matrix, radius) 699 | k = 2*np.pi*n_matrix/wavelen 700 | distance = Quantity(10000.0,'nm') 701 | kd = k*distance 702 | 703 | # calculate the differential scattered intensities 704 | I_x, I_y = mie.diff_scat_intensity_complex_medium(m, x, thetas_2d, kd, 705 | coordinate_system = 'cartesian', phis = phis_2d, 706 | near_field=False) 707 | I_par, I_perp = mie.diff_scat_intensity_complex_medium(m, x, thetas, kd, 708 | near_field=False) 709 | 710 | # integrate the differential scattered intensities 711 | cscat_xy = mie.integrate_intensity_complex_medium(I_x, I_y, distance, thetas, k, 712 | coordinate_system = 'cartesian', phis = phis)[0] 713 | 714 | # check that intensity equations without the asymptotic form of the spherical 715 | # Hankel equations (because they simplify when the fields are multiplied by 716 | # their conjugates to get the intensity) matches old result before simplifying 717 | cscat_xy_old = 6010696.7108612377 718 | assert_almost_equal(cscat_xy_old, cscat_xy.magnitude, decimal=7) 719 | 720 | cscat_parperp = mie.integrate_intensity_complex_medium(I_par, I_perp, 721 | distance, thetas, k)[0] 722 | 723 | # check that the integrated cross sections are equal 724 | assert_almost_equal(cscat_xy.magnitude, cscat_parperp.magnitude) 725 | 726 | def test_value_errors(): 727 | ''' 728 | test the errors related to incorrect input 729 | ''' 730 | 731 | # parameters of sample and source 732 | wavelen = Quantity('658.0 nm') 733 | radius = Quantity('0.85 um') 734 | n_matrix = Quantity(1.00 + 1e-4* 1.0j, '') 735 | n_particle = Quantity(1.59 + 1e-4 * 1.0j, '') 736 | thetas = Quantity(np.linspace(np.pi/2, np.pi, 4), 'rad') 737 | phis = Quantity(np.linspace(0, 2*np.pi, 3), 'rad') 738 | thetas_2d, phis_2d = np.meshgrid(thetas, phis) 739 | thetas_2d = Quantity(thetas_2d, 'rad') 740 | 741 | # parameters for calculating scattering 742 | m = index_ratio(n_particle, n_matrix) 743 | x = size_parameter(wavelen, n_matrix, radius) 744 | k = 2*np.pi*n_matrix/wavelen 745 | distance = Quantity(10000.0,'nm') 746 | kd = k*distance 747 | 748 | with pytest.raises(ValueError): 749 | # try to calculate differential scattered intensity in weird coordinate system 750 | I_x, I_y = mie.diff_scat_intensity_complex_medium(m, x, thetas_2d, kd, 751 | coordinate_system = 'weird', phis = phis_2d, 752 | near_field=True) 753 | 754 | # try to calculate new 755 | I_x, I_y = mie.diff_scat_intensity_complex_medium(m, x, thetas_2d, kd, 756 | coordinate_system = 'cartesian', phis = phis_2d, 757 | near_field=True) 758 | # calculate the differenetial scattered intensities 759 | I_x, I_y = mie.diff_scat_intensity_complex_medium(m, x, thetas_2d, kd, 760 | coordinate_system = 'cartesian', phis = phis_2d, 761 | near_field=False) 762 | 763 | I_par, I_perp = mie.diff_scat_intensity_complex_medium(m, x, thetas, kd, 764 | near_field=True) 765 | 766 | with pytest.raises(ValueError): 767 | # integrate the differential scattered intensities 768 | cscat_xy = mie.integrate_intensity_complex_medium(I_x, I_y, distance, 769 | thetas, k, coordinate_system = 'cartesian')[0] 770 | 771 | cscat_weird = mie.integrate_intensity_complex_medium(I_x, I_y, distance, 772 | thetas, k, coordinate_system = 'weird')[0] 773 | 774 | as_vec_weird = mie.vector_scattering_amplitude(m, x, thetas_2d, 775 | coordinate_system = 'weird', phis = phis_2d) 776 | 777 | as_vec_xy = mie.vector_scattering_amplitude(m, x, thetas_2d, 778 | coordinate_system = 'cartesian') 779 | 780 | def test_dwell_time_and_energy(): 781 | #Test that the dwell time function matches example given in 782 | #Lagendijk and van Tiggelen, Physics Reports 270 (1996) 143-215 pg 169 783 | #The example given is for 440 nm titania particles in vacuum, 784 | #where: 785 | # 786 | #size parameter: x = 4.59 787 | #radius = 220 nm 788 | #c = speed of light in vacuum 789 | # 790 | #and the dwell time can be expressed as: 791 | # 792 | #td = 850*(220 nm)/ c 793 | # 794 | #so the corresponding distance travelled in a vacuum is: 795 | #distance = c*td ~ 190 um 796 | 797 | # parameters given in Lagendijk and van Tiggelen 798 | radius = Quantity('220.0 nm') 799 | n_medium = Quantity(1.0, '') 800 | n_particle = Quantity(2.73, '') 801 | c = Quantity(2.99792e8,'m/s') 802 | x = 4.59 803 | m = 2.73 804 | wavelen = 2*np.pi*radius*n_medium/x 805 | wavelen_media = 2*np.pi*radius/x 806 | 807 | dwell_time = mie.calc_dwell_time(radius, 808 | n_medium, 809 | n_particle, 810 | wavelen) 811 | 812 | 813 | nstop = mie._nstop(x) 814 | gamma_n, An = mie._time_coeffs(m, x, nstop) 815 | n = np.arange(1,nstop+1) 816 | y = m*x 817 | W_star_calc = 3/4*np.sum((2*n + 1)/y**2 *gamma_n*(1+An**2-n*(n+1)/y**2)) 818 | W_star_reported = 2500 819 | 820 | W_reported = 2500*4/3*np.pi*radius**3 821 | W_reported = W_reported.to('um^3') 822 | 823 | # calculate the energy contained in sphere 824 | W_calc = mie.calc_energy(radius, n_medium, m, x, nstop) 825 | W_calc = W_calc.to('um^3') 826 | 827 | cscat_reported = 3.9*np.pi*radius**2 828 | cscat_reported = cscat_reported.to('um^2') 829 | cscat_calc = mie.calc_cross_sections(m, x, wavelen_media)[0] 830 | cscat_calc = cscat_calc.to('um^2') 831 | 832 | distance_reported = Quantity('190.0 um') 833 | distance_calc = dwell_time*c 834 | distance_calc = distance_calc.to('um') 835 | 836 | assert_approx_equal(cscat_reported.magnitude, cscat_calc.magnitude, significant=2) 837 | assert_approx_equal(W_star_reported, np.real(W_star_calc), significant=2) 838 | assert_approx_equal(W_reported.magnitude, np.real(W_calc.magnitude), significant=2) 839 | assert_approx_equal(distance_reported.magnitude, np.real(distance_calc.magnitude), significant=2) 840 | -------------------------------------------------------------------------------- /pymie/tests/test_multilayer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Vinothan N. Manoharan 2 | # 3 | # This file is part of the python-mie 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 multilayer_sphere_lib module 19 | 20 | .. moduleauthor:: Vinothan N. Manoharan 21 | .. moduleauthor:: Victoria Hwang 22 | """ 23 | import os 24 | from .. import Quantity, size_parameter, np, mie 25 | from .. import multilayer_sphere_lib as msl 26 | from numpy.testing import assert_array_almost_equal, assert_allclose 27 | import yaml 28 | 29 | def test_scatcoeffs_multi(): 30 | # test that the scattering coefficients are the same for a non-multilayer 31 | # particle and for an equivalent multilayer particle 32 | 33 | # calculate coefficients for the non-multilayer 34 | m = 1.15 35 | n_sample = Quantity(1.5, '') 36 | wavelen = Quantity('500.0 nm') 37 | radius = Quantity('100.0 nm') 38 | x = size_parameter(wavelen, n_sample, radius) 39 | nstop = mie._nstop(x) 40 | coeffs = mie._scatcoeffs(m, x, nstop) 41 | 42 | # calculate coefficients for a multilayer particle with a core that 43 | # is the same as the non-multilayer and a shell thickness of zero 44 | marray = [1.15, 1.15] # layer index ratios, innermost first 45 | multi_radius = Quantity(np.array([100.0, 100.0]),'nm') 46 | xarray = size_parameter(wavelen, n_sample, multi_radius) 47 | coeffs_multi = msl.scatcoeffs_multi(marray, xarray) 48 | 49 | assert_array_almost_equal(coeffs, coeffs_multi) 50 | 51 | # calculate coefficients for a 3-layer particle with a core that 52 | # is the same as the non-multilayer and shell thicknesses of zero 53 | marray2 = [1.15, 1.15, 1.15] # layer index ratios, innermost first 54 | multi_radius2 = Quantity(np.array([100.0, 100.0, 100.0]),'nm') 55 | xarray2 = size_parameter(wavelen, n_sample, multi_radius2) 56 | coeffs_multi2 = msl.scatcoeffs_multi(marray2, xarray2) 57 | 58 | assert_array_almost_equal(coeffs, coeffs_multi2) 59 | 60 | def test_scatcoeffs_multi_absorbing_particle(): 61 | # test that the scattering coefficients are the same for a real index ratio 62 | # and a complex index ratio with a 0 imaginary component. 63 | marray_real = [1.15, 1.2] 64 | marray_imag = [1.15 + 0j, 1.2 + 0j] 65 | n_sample = Quantity(1.5, '') 66 | wavelen = Quantity('500.0 nm') 67 | multi_radius = Quantity(np.array([100.0, 110.0]),'nm') 68 | xarray = size_parameter(wavelen, n_sample, multi_radius) 69 | 70 | coeffs_multi_real = msl.scatcoeffs_multi(marray_real, xarray) 71 | coeffs_multi_imag = msl.scatcoeffs_multi(marray_imag, xarray) 72 | 73 | assert_array_almost_equal(coeffs_multi_real, coeffs_multi_imag) 74 | 75 | def test_sooty_particles(): 76 | ''' 77 | Test multilayered sphere scattering coefficients by comparison of 78 | radiometric quantities. 79 | 80 | We will use the data in [Yang2003]_ Table 3 on p. 1717, cases 81 | 2, 3, and 4 as our gold standard. 82 | ''' 83 | x_L = 100 84 | m_med = 1.33 85 | m_abs = 2. + 1.j 86 | f_v = 0.1 87 | 88 | def efficiencies_from_scat_units(m, x): 89 | asbs = msl.scatcoeffs_multi(m, x) 90 | qs = np.array(mie._cross_sections(*asbs)) * 2 / x_L**2 91 | # there is a factor of 2 conventional difference between 92 | # "backscattering" and "radar backscattering" efficiencies. 93 | return np.array([qs[1], qs[0], qs[2]*4*np.pi/2.]) 94 | 95 | # first case: absorbing core 96 | x_ac = np.array([f_v**(1./3.) * x_L, x_L]) 97 | m_ac = np.array([m_abs, m_med]) 98 | 99 | # second case: absorbing shell 100 | x_as = np.array([(1. - f_v)**(1./3.), 1.]) * x_L 101 | m_as = np.array([m_med, m_abs]) 102 | 103 | # third case: smooth distribution (900 layers) 104 | n_layers = 900 105 | x_sm = np.arange(1, n_layers + 1) * x_L / n_layers 106 | beta = (m_abs**2 - m_med**2) / (m_abs**2 + 2. * m_med**2) 107 | f = 4./3. * (x_sm / x_L) * f_v 108 | m_sm = m_med * np.sqrt(1. + 3. * f * beta / (1. - f * beta)) 109 | 110 | location = os.path.split(os.path.abspath(__file__))[0] 111 | gold_name = os.path.join(location, 'gold', 112 | 'gold_multilayer') 113 | gold_file = open(gold_name + '.yaml') 114 | gold = np.array(yaml.safe_load(gold_file)) 115 | gold_file.close() 116 | 117 | assert_allclose(efficiencies_from_scat_units(m_ac, x_ac), gold[0], 118 | rtol = 1e-3) 119 | assert_allclose(efficiencies_from_scat_units(m_as, x_as), gold[1], 120 | rtol = 2e-5) 121 | assert_allclose(efficiencies_from_scat_units(m_sm, x_sm), gold[2], 122 | rtol = 1e-3) 123 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # convert all warnings to errors 2 | [tool.pytest.ini_options] 3 | filterwarnings = [ 4 | "error", 5 | ] 6 | 7 | [tool.ruff] 8 | line-length = 79 9 | 10 | [tool.ruff.lint] 11 | # Rulesets for ruff to check 12 | select = [ 13 | # pyflakes rules 14 | "F", 15 | # pycodestyle (PEP8) 16 | "E", "W", 17 | ] 18 | 19 | [tool.ruff.lint.per-file-ignores] 20 | # Ignore long line warnings and unused variable warnings in test files. We 21 | # sometimes have long lines for nicely formatting gold results, and we sometimes 22 | # have unused variables just to check if function returns without an error 23 | "**/tests/*" = ["E501", "F841"] 24 | # Ignore "ambiguous variable name" when we use "l" as a variable in Mie 25 | # calculations 26 | "pymie/mie.py" = ["E741"] -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='pymie', 4 | version='0.1', 5 | description='Pure Python package for Mie scattering calculations.', 6 | url='https://github.com/manoharan-lab/python-mie', 7 | author='Manoharan Lab, Harvard University', 8 | author_email='vnm@seas.harvard.edu', 9 | packages=['pymie'], 10 | install_requires=['pint', 'numpy', 'scipy']) 11 | --------------------------------------------------------------------------------