From 43fe48a98df767d841440562ef04ce9e13370e93 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 13 Apr 2016 04:05:08 -0700 Subject: [PATCH 001/437] initial commit --- .gitignore | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 +++ 2 files changed, 63 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f7cccc --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a55fb1c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# fdfd-tools + +Tools for creating FDFD simulations \ No newline at end of file From e8c1fe612f844c48728bcdf7de277d0f796da4fd Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 13 Apr 2016 04:06:15 -0700 Subject: [PATCH 002/437] add license --- LICENSE.md | 651 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 651 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4ef32f0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,651 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 2007 Free Software Foundation, Inc. <>_ + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +## Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +## TERMS AND CONDITIONS + +### 0. Definitions + +“This License” refers to version 3 of the GNU Affero General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based +on the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that **(1)** displays an appropriate copyright notice, and **(2)** +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code + +The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other +than the work as a whole, that **(a)** is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and **(b)** serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +### 2. Basic Permissions + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +### 4. Conveying Verbatim Copies + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified +it, and giving a relevant date. +* **b)** The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. +This requirement modifies the requirement in section 4 to +“keep intact all notices”. +* **c)** You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. +* **d)** If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either **(1)** a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or **(2)** access to copy the +Corresponding Source from a network server at no charge. +* **c)** Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. +* **d)** Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A “User Product” is either **(1)** a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or **(2)** anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms + +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or +authors of the material; or +* **e)** Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +### 8. Termination + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated **(a)** +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and **(b)** permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +### 11. Patents + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either **(1)** cause the Corresponding Source to be so +available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license **(a)** in connection with copies of the covered work +conveyed by you (or copies made from those copies), or **(b)** primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16 + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source” link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<>. From 282ba3a36ecf83c839dfcfe382359ba96d8eb501 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 13 Apr 2016 04:07:10 -0700 Subject: [PATCH 003/437] elaborate in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a55fb1c..6cb3e2d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # fdfd-tools -Tools for creating FDFD simulations \ No newline at end of file +Python tools for creating finite-difference frequency-domain (FDFD) electromagnetic simulations. From 6c1ceb16709112b0038b367c8896b86c6224fa61 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 30 May 2016 22:30:45 -0700 Subject: [PATCH 004/437] initial development --- .gitignore | 2 + README.md | 34 ++- examples/test.py | 234 +++++++++++++++++++++ fdfd_tools/__init__.py | 25 +++ fdfd_tools/functional.py | 149 +++++++++++++ fdfd_tools/grid.py | 167 +++++++++++++++ fdfd_tools/operators.py | 397 +++++++++++++++++++++++++++++++++++ fdfd_tools/vectorization.py | 49 +++++ fdfd_tools/waveguide.py | 309 +++++++++++++++++++++++++++ fdfd_tools/waveguide_mode.py | 301 ++++++++++++++++++++++++++ float_raster.py | 1 + gridlock | 1 + setup.py | 18 ++ 13 files changed, 1685 insertions(+), 2 deletions(-) create mode 100644 examples/test.py create mode 100644 fdfd_tools/__init__.py create mode 100644 fdfd_tools/functional.py create mode 100644 fdfd_tools/grid.py create mode 100644 fdfd_tools/operators.py create mode 100644 fdfd_tools/vectorization.py create mode 100644 fdfd_tools/waveguide.py create mode 100644 fdfd_tools/waveguide_mode.py create mode 120000 float_raster.py create mode 120000 gridlock create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 7f7cccc..825f5ec 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ docs/_build/ # PyBuilder target/ + +.idea/ diff --git a/README.md b/README.md index 6cb3e2d..ea4de31 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,33 @@ -# fdfd-tools +# fdfd_tools -Python tools for creating finite-difference frequency-domain (FDFD) electromagnetic simulations. +**fdfd_tools** is a python package containing utilities for +creating and analyzing 2D and 3D finite-difference frequency-domain (FDFD) +electromagnetic simulations. + + +**Contents** +* Library of sparse matrices for representing the electromagnetic wave + equation in 3D, as well as auxiliary matrices for conversion between fields +* Waveguide mode solver and waveguide mode operators +* Stretched-coordinate PML boundaries (SCPML) +* Functional versions of most operators +* Anisotropic media (eps_xx, eps_yy, eps_zz, mu_xx, ...) + +This package does *not* provide a matrix solver. The waveguide mode solver +uses scipy's eigenvalue solver; I recommend a GPU-based iterative solver (eg. +those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html)). You will +need the ability to solve complex symmetric (non-Hermitian) linear systems, +ideally with double precision. + +## Installation + +**Requirements:** +* python 3 (written and tested with 3.5) +* numpy +* scipy + + +Install with pip, via git: +```bash +pip install git+https://mpxd.net/gogs/jan/fdfd_tools.git@release +``` diff --git a/examples/test.py b/examples/test.py new file mode 100644 index 0000000..23303b2 --- /dev/null +++ b/examples/test.py @@ -0,0 +1,234 @@ +import numpy +from numpy.ctypeslib import ndpointer +import ctypes + +# h5py used by (uncalled) h5_write(); not used in currently-called code + +from fdfd_tools import vec, unvec, waveguide_mode +import fdfd_tools, fdfd_tools.functional, fdfd_tools.grid +import gridlock + +from matplotlib import pyplot + +__author__ = 'Jan Petykiewicz' + + +def complex_to_alternating(x: numpy.ndarray) -> numpy.ndarray: + stacked = numpy.vstack((numpy.real(x), numpy.imag(x))) + return stacked.T.astype(numpy.float64).flatten() + + +def solve_A(A, b: numpy.ndarray) -> numpy.ndarray: + A_vals = complex_to_alternating(A.data) + b_vals = complex_to_alternating(b) + x_vals = numpy.zeros_like(b_vals) + + args = ['dummy', + '--solver', 'QMR', + '--maxiter', '40000', + '--atol', '1e-6', + '--verbose', '100'] + argc = ctypes.c_int(len(args)) + argv_arr_t = ctypes.c_char_p * len(args) + argv_arr = argv_arr_t() + argv_arr[:] = [s.encode('ascii') for s in args] + + A_dim = ctypes.c_int(A.shape[0]) + A_nnz = ctypes.c_int(A.nnz) + npdouble = ndpointer(ctypes.c_double) + npint = ndpointer(ctypes.c_int) + + lib = ctypes.cdll.LoadLibrary('/home/jan/magma_solve/zsolve_shared.so') + c_solver = lib.zsolve + c_solver.argtypes = [ctypes.c_int, argv_arr_t, + ctypes.c_int, ctypes.c_int, + npdouble, npint, npint, npdouble, npdouble] + + c_solver(argc, argv_arr, A_dim, A_nnz, A_vals, + A.indptr.astype(numpy.intc), + A.indices.astype(numpy.intc), + b_vals, x_vals) + + x = (x_vals[::2] + 1j * x_vals[1::2]).flatten() + return x + + +def write_h5(filename, A, b): + import h5py + # dtype=np.dtype([('real', 'float64'), ('imag', 'float64')]) + h5py.get_config().complex_names = ('real', 'imag') + with h5py.File(filename, 'w') as mat_file: + mat_file.create_group('/A') + mat_file['/A/ir'] = A.indices.astype(numpy.intc) + mat_file['/A/jc'] = A.indptr.astype(numpy.intc) + mat_file['/A/data'] = A.data + mat_file['/b'] = b + mat_file['/x'] = numpy.zeros_like(b) + + +def test0(): + dx = 50 # discretization (nm/cell) + pml_thickness = 10 # (number of cells) + + wl = 1550 # Excitation wavelength + omega = 2 * numpy.pi / wl + + # Device design parameters + radii = (1, 0.6) + th = 220 + center = [0, 0, 0] + + # refractive indices + n_ring = numpy.sqrt(12.6) # ~Si + n_air = 4.0 # air + + # Half-dimensions of the simulation grid + xyz_max = numpy.array([1.2, 1.2, 0.3]) * 1000 + pml_thickness * dx + + # Coordinates of the edges of the cells. + half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max] + edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords] + + # #### Create the grid, mask, and draw the device #### + grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3) + grid.draw_cylinder(surface_normal=gridlock.Direction.z, + center=center, + radius=max(radii), + thickness=th, + eps=n_ring**2, + num_points=24) + grid.draw_cylinder(surface_normal=gridlock.Direction.z, + center=center, + radius=min(radii), + thickness=th*1.1, + eps=n_air ** 2, + num_points=24) + + dx0_a = grid.dxyz + dx0_b = [grid.shifted_dxyz(which_shifts=a)[a] for a in range(3)] + dxes = [dx0_a, dx0_b] + for a in (0, 1, 2): + for p in (-1, 1): + dxes = fdfd_tools.grid.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, + thickness=pml_thickness) + + J = [numpy.zeros_like(grid.grids[0], dtype=complex) for _ in range(3)] + J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1e5 + + A = fdfd_tools.functional.e_full(omega, dxes, vec(grid.grids)).tocsr() + b = -1j * omega * vec(J) + + x = solve_A(A, b) + E = unvec(x, grid.shape) + + print('Norm of the residual is {}'.format(numpy.linalg.norm(A.dot(x) - b)/numpy.linalg.norm(b))) + + pyplot.figure() + pyplot.pcolor(numpy.real(E[1][:, :, grid.shape[2]//2]), cmap='seismic') + pyplot.axis('equal') + pyplot.show() + + +def test1(): + dx = 40 # discretization (nm/cell) + pml_thickness = 10 # (number of cells) + + wl = 1550 # Excitation wavelength + omega = 2 * numpy.pi / wl + + # Device design parameters + w = 600 + th = 220 + center = [0, 0, 0] + + # refractive indices + n_wg = numpy.sqrt(12.6) # ~Si + n_air = 1.0 # air + + # Half-dimensions of the simulation grid + xyz_max = numpy.array([0.8, 0.9, 0.6]) * 1000 + (pml_thickness + 2) * dx + + # Coordinates of the edges of the cells. + half_edge_coords = [numpy.arange(dx/2, m + dx/2, step=dx) for m in xyz_max] + edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords] + + # #### Create the grid and draw the device #### + grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3) + grid.draw_cuboid(center=center, dimensions=[8e3, w, th], eps=n_wg**2) + + dx0_a = grid.dxyz + dx0_b = [grid.shifted_dxyz(which_shifts=a)[a] for a in range(3)] + dxes = [dx0_a, dx0_b] + for a in (0, 1, 2): + for p in (-1, 1): + dxes = fdfd_tools.grid.stretch_with_scpml(dxes,omega=omega, axis=a, polarity=p, + thickness=pml_thickness) + + half_dims = numpy.array([10, 20, 15]) * dx + dims = [-half_dims, half_dims] + dims[1][0] = dims[0][0] + ind_dims = (grid.pos2ind(dims[0], which_shifts=None).astype(int), + grid.pos2ind(dims[1], which_shifts=None).astype(int)) + wg_args = { + 'omega': omega, + 'slices': [slice(i, f+1) for i, f in zip(*ind_dims)], + 'dxes': dxes, + 'axis': 0, + 'polarity': +1, + } + + wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, **wg_args, epsilon=grid.grids) + J = waveguide_mode.compute_source(**wg_args, **wg_results) + H_overlap = waveguide_mode.compute_overlap_e(**wg_args, **wg_results) + + A = fdfd_tools.operators.e_full(omega, dxes, vec(grid.grids)).tocsr() + b = -1j * omega * vec(J) + x = solve_A(A, b) + E = unvec(x, grid.shape) + + print('Norm of the residual is ', numpy.linalg.norm(A @ x - b)) + + def pcolor(v): + vmax = numpy.max(numpy.abs(v)) + pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax) + pyplot.axis('equal') + pyplot.colorbar() + + center = grid.pos2ind([0, 0, 0], None).astype(int) + pyplot.figure() + pyplot.subplot(2, 2, 1) + pcolor(numpy.real(E[1][center[0], :, :])) + pyplot.subplot(2, 2, 2) + pyplot.plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10)) + pyplot.subplot(2, 2, 3) + pcolor(numpy.real(E[1][:, :, center[2]])) + pyplot.subplot(2, 2, 4) + + def poyntings(E): + e = vec(E) + h = fdfd_tools.operators.e2h(omega, dxes) @ e + cross1 = fdfd_tools.operators.poynting_e_cross(e, dxes) @ h.conj() + cross2 = fdfd_tools.operators.poynting_h_cross(h.conj(), dxes) @ e + s1 = unvec(0.5 * numpy.real(cross1), grid.shape) + s2 = unvec(0.5 * numpy.real(-cross2), grid.shape) + return s1, s2 + + s1x, s2x = poyntings(E) + pyplot.plot(s1x[0].sum(axis=2).sum(axis=1)) + pyplot.hold(True) + pyplot.plot(s2x[0].sum(axis=2).sum(axis=1)) + pyplot.show() + + q = [] + for i in range(-5, 30): + H_rolled = [numpy.roll(h, i, axis=0) for h in H_overlap] + q += [numpy.abs(vec(E) @ vec(H_rolled))] + pyplot.figure() + pyplot.plot(q) + pyplot.title('Overlap with mode') + pyplot.show() + print('Average overlap with mode:', sum(q)/len(q)) + +if __name__ == '__main__': + # test0() + test1() diff --git a/fdfd_tools/__init__.py b/fdfd_tools/__init__.py new file mode 100644 index 0000000..4d75b2c --- /dev/null +++ b/fdfd_tools/__init__.py @@ -0,0 +1,25 @@ +""" +Electromagnetic FDFD simulation tools + +Tools for 3D and 2D Electromagnetic Finite Difference Frequency Domain (FDFD) +simulations. These tools handle conversion of fields to/from vector form, +creation of the wave operator matrix, stretched-coordinate PMLs, +field conversion operators, waveguide mode operator, and waveguide mode +solver. + +This package only contains a solver for the waveguide mode eigenproblem; +if you want to solve 3D problems you can use your favorite iterative sparse +matrix solver (so long as it can handle complex symmetric [non-Hermitian] +matrices, ideally with double precision). + + +Dependencies: +- numpy +- scipy + +""" + +from .vectorization import vec, unvec, field_t, vfield_t +from .grid import dx_lists_t + +__author__ = 'Jan Petykiewicz' \ No newline at end of file diff --git a/fdfd_tools/functional.py b/fdfd_tools/functional.py new file mode 100644 index 0000000..4d573db --- /dev/null +++ b/fdfd_tools/functional.py @@ -0,0 +1,149 @@ +""" +Functional versions of many FDFD operators. These can be useful for performing + FDFD calculations without needing to construct large matrices in memory. + +The functions generated here expect inputs in the form E = [E_x, E_y, E_z], where each + component E_* is an ndarray of equal shape. +""" +from typing import List, Callable +import numpy + +from . import dx_lists_t, field_t + +__author__ = 'Jan Petykiewicz' + + +functional_matrix = Callable[[List[numpy.ndarray]], List[numpy.ndarray]] + + +def curl_h(dxes: dx_lists_t) -> functional_matrix: + """ + Curl operator for use with the H field. + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Function for taking the discretized curl of the H-field, F(H) -> curlH + """ + dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') + + def dH(f, ax): + return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] + + def ch_fun(H: List[numpy.ndarray]) -> List[numpy.ndarray]: + E = [dH(H[2], 1) - dH(H[1], 2), + dH(H[0], 2) - dH(H[2], 0), + dH(H[1], 0) - dH(H[0], 1)] + return E + + return ch_fun + + +def curl_e(dxes: dx_lists_t) -> functional_matrix: + """ + Curl operator for use with the E field. + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Function for taking the discretized curl of the E-field, F(E) -> curlE + """ + dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') + + def dE(f, ax): + return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] + + def ce_fun(E: List[numpy.ndarray]) -> List[numpy.ndarray]: + H = [dE(E[2], 1) - dE(E[1], 2), + dE(E[0], 2) - dE(E[2], 0), + dE(E[1], 0) - dE(E[0], 1)] + return H + + return ce_fun + + +def e_full(omega: complex, + dxes: dx_lists_t, + epsilon: field_t, + mu: field_t = None + ) -> functional_matrix: + """ + Wave operator del x (1/mu * del x) - omega**2 * epsilon, for use with E-field, + with wave equation + (del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J + + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param epsilon: Dielectric constant + :param mu: Magnetic permeability (default 1 everywhere) + :return: Function implementing the wave operator A(E) -> E + """ + ch = curl_h(dxes) + ce = curl_e(dxes) + + def op_1(E): + curls = ch(ce(E)) + return [c - omega ** 2 * e * x for c, e, x in zip(curls, epsilon, E)] + + def op_mu(E): + curls = ch([m * y for m, y in zip(mu, ce(E))]) + return [c - omega ** 2 * e * x for c, e, x in zip(curls, epsilon, E)] + + if numpy.any(numpy.equal(mu, None)): + return op_1 + else: + return op_mu + + +def eh_full(omega: complex, + dxes: dx_lists_t, + epsilon: field_t, + mu: field_t = None + ) -> functional_matrix: + """ + Wave operator for full (both E and H) field representation. + + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param epsilon: Dielectric constant + :param mu: Magnetic permeability (default 1 everywhere) + :return: Function implementing the wave operator A(E, H) -> (E, H) + """ + ch = curl_h(dxes) + ce = curl_e(dxes) + + def op_1(E, H): + return ([c - 1j * omega * e * x for c, e, x in zip(ch(H), epsilon, E)], + [c + 1j * omega * y for c, y in zip(ce(E), H)]) + + def op_mu(E, H): + return ([c - 1j * omega * e * x for c, e, x in zip(ch(H), epsilon, E)], + [c + 1j * omega * m * y for c, m, y in zip(ce(E), mu, H)]) + + if numpy.any(numpy.equal(mu, None)): + return op_1 + else: + return op_mu + + +def e2h(omega: complex, + dxes: dx_lists_t, + mu: field_t = None, + ) -> functional_matrix: + """ + Utility operator for converting the E field into the H field. + For use with e_full -- assumes that there is no magnetic current M. + + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param mu: Magnetic permeability (default 1 everywhere) + :return: Function for converting E to H + """ + A2 = curl_e(dxes) + + def e2h_1_1(E): + return [y / (-1j * omega) for y in A2(E)] + + def e2h_mu(E): + return [y / (-1j * omega * m) for y, m in zip(A2(E), mu)] + + if numpy.any(numpy.equal(mu, None)): + return e2h_1_1 + else: + return e2h_mu diff --git a/fdfd_tools/grid.py b/fdfd_tools/grid.py new file mode 100644 index 0000000..00b4bdc --- /dev/null +++ b/fdfd_tools/grid.py @@ -0,0 +1,167 @@ +""" +Functions for creating stretched coordinate PMLs. +""" + +from typing import List, Tuple, Callable +import numpy + +__author__ = 'Jan Petykiewicz' + + +dx_lists_t = List[List[numpy.ndarray]] +s_function_type = Callable[[float], float] + + +def prepare_s_function(ln_R: float = -16, + m: float = 4 + ) -> s_function_type: + """ + Create an s_function to pass to the SCPML functions. This is used when you would like to + customize the PML parameters. + + :param ln_R: Natural logarithm of the desired reflectance + :param m: Polynomial order for the PML (imaginary part increases as distance ** m) + :return: An s_function, which takes an ndarray (distances) and returns an ndarray (complex part of + the cell width; needs to be divided by sqrt(epilon_effective) * real(omega)) before + use. + """ + def s_factor(distance: numpy.ndarray) -> numpy.ndarray: + s_max = (m + 1) * ln_R / 2 # / 2 because we have assume boundaries + return s_max * (distance ** m) + return s_factor + + +def uniform_grid_scpml(shape: numpy.ndarray or List[int], + thicknesses: numpy.ndarray or List[int], + omega: float, + epsilon_effective: float = 1.0, + s_function: s_function_type = None, + ) -> dx_lists_t: + """ + Create dx arrays for a uniform grid with a cell width of 1 and a pml. + + If you want something more fine-grained, check out stretch_with_scpml(...). + + :param shape: Shape of the grid, including the PMLs (which are 2*thicknesses thick) + :param thicknesses: [th_x, th_y, th_z] Thickness of the PML in each direction. Both polarities are added. + Each th_ of pml is applied twice, once on each edge of the grid along the given axis. + th_* may be zero, in which case no pml is added. + :param omega: Angular frequency for the simulation + :param epsilon_effective: Effective epsilon of the PML. Match this to the material at the edge of your grid. + Default 1. + :param s_function: s_function created by prepare_s_function(...), allowing customization of pml parameters. + Default uses prepare_s_function() with no parameters. + :return: Complex cell widths (dx_lists) + """ + if s_function is None: + s_function = prepare_s_function() + + # Normalized distance to nearest boundary + def l(u, n, t): + return ((t - u).clip(0) + (u - (n - t)).clip(0)) / t + + dx_a = [numpy.array(numpy.inf)] * 3 + dx_b = [numpy.array(numpy.inf)] * 3 + + # divide by this to adjust for epsilon_effective and omega + s_correction = numpy.sqrt(epsilon_effective) * numpy.real(omega) + + for k, th in enumerate(thicknesses): + s = shape[k] + if th > 0: + sr = numpy.arange(s) + dx_a[k] = 1 + 1j * s_function(l(sr, s, th)) / s_correction + dx_b[k] = 1 + 1j * s_function(l(sr+0.5, s, th)) / s_correction + else: + dx_a[k] = numpy.ones((s,)) + dx_b[k] = numpy.ones((s,)) + return [dx_a, dx_b] + + +def stretch_with_scpml(dxes: dx_lists_t, + axis: int, + polarity: int, + omega: float, + epsilon_effective: float = 1.0, + thickness: int = 10, + s_function: s_function_type = None, + ) -> dx_lists_t: + """ + Stretch dxes to contain a stretched-coordinate PML (SCPML) in one direction along one axis. + + :param dxes: dx_tuple with coordinates to stretch + :param axis: axis to stretch (0=x, 1=y, 2=z) + :param polarity: direction to stretch (-1 for -ve, +1 for +ve) + :param omega: Angular frequency for the simulation + :param epsilon_effective: Effective epsilon of the PML. Match this to the material at the edge of your grid. + Default 1. + :param thickness: number of cells to use for pml (default 10) + :param s_function: s_function created by prepare_s_function(...), allowing customization of pml parameters. + Default uses prepare_s_function() with no parameters. + :return: Complex cell widths + """ + if s_function is None: + s_function = prepare_s_function() + + dx_ai = dxes[0][axis].astype(complex) + dx_bi = dxes[1][axis].astype(complex) + + pos = numpy.hstack((0, dx_ai.cumsum())) + pos_a = (pos[:-1] + pos[1:]) / 2 + pos_b = pos[:-1] + + # divide by this to adjust for epsilon_effective and omega + s_correction = numpy.sqrt(epsilon_effective) * numpy.real(omega) + + if polarity > 0: + # front pml + bound = pos[thickness] + d = bound - pos[0] + + def l_d(x): + return (bound - x) / (bound - pos[0]) + + slc = slice(thickness) + + else: + # back pml + bound = pos[-thickness - 1] + d = pos[-1] - bound + + def l_d(x): + return (x - bound) / (pos[-1] - bound) + + if thickness == 0: + slc = slice(None) + else: + slc = slice(-thickness, None) + + dx_ai[slc] *= 1 + 1j * s_function(l_d(pos_a[slc])) / d / s_correction + dx_bi[slc] *= 1 + 1j * s_function(l_d(pos_b[slc])) / d / s_correction + + dxes[0][axis] = dx_ai + dxes[1][axis] = dx_bi + + return dxes + + +def generate_dx(pos: List[numpy.ndarray]) -> dx_lists_t: + """ + Given a list of 3 ndarrays cell centers, creates the cell width parameters. + + :param pos: List of 3 ndarrays of cell centers + :return: (dx_a, dx_b) cell widths (no pml) + """ + if len(pos) != 3: + raise Exception('Must have len(pos) == 3') + + dx_a = [numpy.array(numpy.inf)] * 3 + dx_b = [numpy.array(numpy.inf)] * 3 + + for i, p_orig in enumerate(pos): + p = numpy.array(p_orig, dtype=float) + if p.size != 1: + p_shifted = numpy.hstack((p[1:], p[-1] + (p[1] - p[0]))) + dx_a[i] = numpy.diff(p) + dx_b[i] = numpy.diff((p + p_shifted) / 2) + return dx_a, dx_b diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py new file mode 100644 index 0000000..7a72d8e --- /dev/null +++ b/fdfd_tools/operators.py @@ -0,0 +1,397 @@ +""" +Sparse matrix operators for use with electromagnetic wave equations. + +These functions return sparse-matrix (scipy.sparse.spmatrix) representations of + a variety of operators, intended for use with E and H fields vectorized using the + fdfd_tools.vec() and .unvec() functions (column-major/Fortran ordering). + +E- and H-field values are defined on a Yee cell; epsilon values should be calculated for + cells centered at each E component (mu at each H component). + +Many of these functions require a 'dxes' parameter, of type fdfd_tools.dx_lists_type, + which contains grid cell width information in the following format: + [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]], + [[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]] + where dx_e_0 is the x-width of the x=0 cells, as used when calculating dE/dx, + and dy_h_0 is the y-width of the y=0 cells, as used when calculating dH/dy, etc. + + +The following operators are included: +- E-only wave operator +- H-only wave operator +- EH wave operator +- Curl for use with E, H fields +- E to H conversion +- M to J conversion +- Poynting cross products + +Also available: +- Circular shifts +- Discrete derivatives +- Averaging operators +- Cross product matrices +""" + +from typing import List, Tuple +import numpy +import scipy.sparse as sparse + +from . import vec, dx_lists_t, vfield_t + + +__author__ = 'Jan Petykiewicz' + + +def e_full(omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Wave operator del x (1/mu * del x) - omega**2 * epsilon, for use with E-field, + with wave equation + (del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J + + To make this matrix symmetric, use the preconditions from e_full_preconditioners(). + + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param epsilon: Vectorized dielectric constant + :param mu: Vectorized magnetic permeability (default 1 everywhere).. + :return: Sparse matrix containing the wave operator + """ + ce = curl_e(dxes) + ch = curl_h(dxes) + + e = sparse.diags(epsilon) + if numpy.any(numpy.equal(mu, None)): + m_div = sparse.eye(epsilon.size) + else: + m_div = sparse.diags(1 / mu) + + op = ch @ m_div @ ce - omega**2 * e + return op + + +def e_full_preconditioners(dxes: dx_lists_t + ) -> Tuple[sparse.spmatrix, sparse.spmatrix]: + """ + Left and right preconditioners (Pl, Pr) for symmetrizing the e_full wave operator. + + The preconditioned matrix A_symm = (Pl @ A @ Pr) is complex-symmetric + (non-Hermitian unless there is no loss or PMLs). + + The preconditioner matrices are diagonal and complex, with Pr = 1 / Pl + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Preconditioner matrices (Pl, Pr) + """ + p_squared = [dxes[0][0][:, None, None] * dxes[1][1][None, :, None] * dxes[1][2][None, None, :], + dxes[1][0][:, None, None] * dxes[0][1][None, :, None] * dxes[1][2][None, None, :], + dxes[1][0][:, None, None] * dxes[1][1][None, :, None] * dxes[0][2][None, None, :]] + + p_vector = numpy.sqrt(vec(p_squared)) + P_left = sparse.diags(p_vector) + P_right = sparse.diags(1 / p_vector) + return P_left, P_right + + +def h_full(omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Wave operator del x (1/epsilon * del x) - omega**2 * mu, for use with H-field, + with wave equation + (del x (1/epsilon * del x) - omega**2 * mu) H = i * omega * M + + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param epsilon: Vectorized dielectric constant + :param mu: Vectorized magnetic permeability (default 1 everywhere) + :return: Sparse matrix containing the wave operator + """ + ec = curl_e(dxes) + hc = curl_h(dxes) + + e_div = sparse.diags(1 / epsilon) + if numpy.any(numpy.equal(mu, None)): + m = sparse.eye(epsilon.size) + else: + m = sparse.diags(mu) + + A = ec @ e_div @ hc - omega**2 * m + return A + + +def eh_full(omega, dxes, epsilon, mu=None): + """ + Wave operator for [E, H] field representation. This operator implements Maxwell's + equations without cancelling out either E or H. The operator is + [[-i * omega * epsilon, del x], + [del x, i * omega * mu]] + + for use with a field vector of the form hstack(vec(E), vec(H)). + + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param epsilon: Vectorized dielectric constant + :param mu: Vectorized magnetic permeability (default 1 everywhere) + :return: Sparse matrix containing the wave operator + """ + A2 = curl_e(dxes) + A1 = curl_h(dxes) + + iwe = 1j * omega * sparse.diags(epsilon) + iwm = 1j * omega + if not numpy.any(numpy.equal(mu, None)): + iwm *= sparse.diags(mu) + + A = sparse.bmat([[-iwe, A1], + [A2, +iwm]]) + return A + + +def curl_h(dxes: dx_lists_t) -> sparse.spmatrix: + """ + Curl operator for use with the H field. + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Sparse matrix for taking the discretized curl of the H-field + """ + return cross(deriv_back(dxes[1])) + + +def curl_e(dxes: dx_lists_t) -> sparse.spmatrix: + """ + Curl operator for use with the E field. + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Sparse matrix for taking the discretized curl of the E-field + """ + return cross(deriv_forward(dxes[0])) + + +def e2h(omega: complex, + dxes: dx_lists_t, + mu: vfield_t = None, + ) -> sparse.spmatrix: + """ + Utility operator for converting the E field into the H field. + For use with e_full -- assumes that there is no magnetic current M. + + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param mu: Vectorized magnetic permeability (default 1 everywhere) + :return: Sparse matrix for converting E to H + """ + op = curl_e(dxes) / (-1j * omega) + + if not numpy.any(numpy.equal(mu, None)): + op = sparse.diags(1 / mu) @ op + + return op + + +def m2j(omega: complex, + dxes: dx_lists_t, + mu: vfield_t = None): + """ + Utility operator for converting M field into J. + Converts a magnetic current M into an electric current J. + For use with eg. e_full. + + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param mu: Vectorized magnetic permeability (default 1 everywhere) + :return: Sparse matrix for converting E to H + """ + op = curl_h(dxes) / (1j * omega) + + if not numpy.any(numpy.equal(mu, None)): + op = op @ sparse.diags(1 / mu) + + return op + + +def rotation(axis: int, shape: List[int]) -> sparse.spmatrix: + """ + Utility operator for performing a circular shift along a specified axis by 1 element. + + :param axis: Axis to shift along. x=0, y=1, z=2 + :param shape: Shape of the grid being shifted + :return: Sparse matrix for performing the circular shift + """ + if len(shape) not in (2, 3): + raise Exception('Invalid shape: {}'.format(shape)) + if axis not in range(len(shape)): + raise Exception('Invalid direction: {}, shape is {}'.format(axis, shape)) + + n = numpy.prod(shape) + + shifts = [1 if k == axis else 0 for k in range(3)] + shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts)] + ijk = numpy.meshgrid(*shifted_diags, indexing='ij') + + i_ind = numpy.arange(n) + j_ind = ijk[0] + ijk[1] * shape[0] + if len(shape) == 3: + j_ind += ijk[2] * shape[0] * shape[1] + + vij = (numpy.ones(n), (i_ind, j_ind.flatten(order='F'))) + + D = sparse.csr_matrix(vij, shape=(n, n)) + return D + + +def deriv_forward(dx_e: List[numpy.ndarray]) -> List[sparse.spmatrix]: + """ + Utility operators for taking discretized derivatives (forward variant). + + :param dx_e: Lists of cell sizes for all axes [[dx_0, dx_1, ...], ...]. + :return: List of operators for taking forward derivatives along each axis. + """ + shape = [s.size for s in dx_e] + n = numpy.prod(shape) + + dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij') + + def deriv(axis): + return rotation(axis, shape) - sparse.eye(n) + + Ds = [sparse.diags(+1 / dx.flatten(order='F')) @ deriv(a) + for a, dx in enumerate(dx_e_expanded)] + + return Ds + + +def deriv_back(dx_h: List[numpy.ndarray]) -> List[sparse.spmatrix]: + """ + Utility operators for taking discretized derivatives (backward variant). + + :param dx_h: Lists of cell sizes for all axes [[dx_0, dx_1, ...], ...]. + :return: List of operators for taking forward derivatives along each axis. + """ + shape = [s.size for s in dx_h] + n = numpy.prod(shape) + + dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij') + + def deriv(axis): + return rotation(axis, shape) - sparse.eye(n) + + Ds = [sparse.diags(-1 / dx.flatten(order='F')) @ deriv(a).T + for a, dx in enumerate(dx_h_expanded)] + + return Ds + + +def cross(B: List[sparse.spmatrix]) -> sparse.spmatrix: + """ + Cross product operator + + :param B: List [Bx, By, Bz] of sparse matrices corresponding to the x, y, z + portions of the operator on the left side of the cross product. + :return: Sparse matrix corresponding to (B x), where x is the cross product + """ + n = B[0].shape[0] + zero = sparse.csr_matrix((n, n)) + return sparse.bmat([[zero, -B[2], B[1]], + [B[2], zero, -B[0]], + [-B[1], B[0], zero]]) + + +def vec_cross(b: vfield_t) -> sparse.spmatrix: + """ + Vector cross product operator + + :param b: Vector on the left side of the cross product + :return: Sparse matrix corresponding to (b x), where x is the cross product + """ + B = [sparse.diags(c) for c in numpy.split(b, 3)] + return cross(B) + + +def avgf(axis: int, shape: List[int]) -> sparse.spmatrix: + """ + Forward average operator (x4 = (x4 + x5) / 2) + + :param axis: Axis to average along (x=0, y=1, z=2) + :param shape: Shape of the grid to average + :return: Sparse matrix for forward average operation + """ + if len(shape) not in (2, 3): + raise Exception('Invalid shape: {}'.format(shape)) + + n = numpy.prod(shape) + return 0.5 * (sparse.eye(n) + rotation(axis, shape)) + + +def avgb(axis: int, shape: List[int]) -> sparse.spmatrix: + """ + Backward average operator (x4 = (x4 + x3) / 2) + + :param axis: Axis to average along (x=0, y=1, z=2) + :param shape: Shape of the grid to average + :return: Sparse matrix for backward average operation + """ + return avgf(axis, shape).T + + +def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: + """ + Operator for computing the Poynting vector, contining the (E x) portion of the Poynting vector. + + :param e: Vectorized E-field for the ExH cross product + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Sparse matrix containing (E x) portion of Poynting cross product + """ + shape = [len(dx) for dx in dxes[0]] + + fx, fy, fz = [avgf(i, shape) for i in range(3)] + bx, by, bz = [avgb(i, shape) for i in range(3)] + + dxag = [dx.flatten(order='F') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] + dbgx, dbgy, dbgz = [sparse.diags(dx.flatten(order='F')) + for dx in numpy.meshgrid(*dxes[1], indexing='ij')] + + Ex, Ey, Ez = [sparse.diags(ei * da) for ei, da in zip(numpy.split(e, 3), dxag)] + + n = numpy.prod(shape) + zero = sparse.csr_matrix((n, n)) + + P = sparse.bmat( + [[ zero, -fx @ Ez @ bz @ dbgy, fx @ Ey @ by @ dbgz], + [ fy @ Ez @ bz @ dbgx, zero, -fy @ Ex @ bx @ dbgz], + [-fz @ Ey @ by @ dbgx, fz @ Ex @ bx @ dbgy, zero]]) + return P + + +def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: + """ + Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector. + + :param h: Vectorized H-field for the HxE cross product + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Sparse matrix containing (H x) portion of Poynting cross product + """ + shape = [len(dx) for dx in dxes[0]] + + fx, fy, fz = [avgf(i, shape) for i in range(3)] + bx, by, bz = [avgb(i, shape) for i in range(3)] + + dxbg = [dx.flatten(order='F') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] + dagx, dagy, dagz = [sparse.diags(dx.flatten(order='F')) + for dx in numpy.meshgrid(*dxes[0], indexing='ij')] + + Hx, Hy, Hz = [sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg)] + + n = numpy.prod(shape) + zero = sparse.csr_matrix((n, n)) + + P = sparse.bmat( + [[ zero, -by @ Hz @ fx @ dagy, bz @ Hy @ fx @ dagz], + [ bx @ Hz @ fy @ dagx, zero, -bz @ Hx @ fy @ dagz], + [-bx @ Hy @ fz @ dagx, by @ Hx @ fz @ dagy, zero]]) + return P diff --git a/fdfd_tools/vectorization.py b/fdfd_tools/vectorization.py new file mode 100644 index 0000000..dfa48f1 --- /dev/null +++ b/fdfd_tools/vectorization.py @@ -0,0 +1,49 @@ +""" +Functions for moving between a vector field (list of 3 ndarrays, [f_x, f_y, f_z]) +and a 1D array representation of that field [f_x0, f_x1, f_x2,... f_y0,... f_z0,...]. +Vectorized versions of the field use column-major (ie., Fortran, Matlab) ordering. +""" + + +from typing import List +import numpy + +__author__ = 'Jan Petykiewicz' + +# Types +field_t = List[numpy.ndarray] # vector field (eg. [E_x, E_y, E_z] +vfield_t = numpy.ndarray # linearized vector field + + +def vec(f: field_t) -> vfield_t: + """ + Create a 1D ndarray from a 3D vector field which spans a 1-3D region. + + Returns None if called with f=None. + + :param f: A vector field, [f_x, f_y, f_z] where each f_ component is a 1 to + 3D ndarray (f_* should all be the same size). Doesn't fail with f=None. + :return: A 1D ndarray containing the linearized field (or None) + """ + if numpy.any(numpy.equal(f, None)): + return None + return numpy.hstack(tuple((fi.flatten(order='F') for fi in f))) + + +def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: + """ + Perform the inverse of vec(): take a 1D ndarray and output a 3D field + of form [f_x, f_y, f_z] where each of f_* is a len(shape)-dimensional + ndarray. + + Returns None if called with v=None. + + :param v: 1D ndarray representing a 3D vector field of shape shape (or None) + :param shape: shape of the vector field + :return: [f_x, f_y, f_z] where each f_ is a len(shape) dimensional ndarray + (or None) + """ + if numpy.any(numpy.equal(v, None)): + return None + return [vi.reshape(shape, order='F') for vi in numpy.split(v, 3)] + diff --git a/fdfd_tools/waveguide.py b/fdfd_tools/waveguide.py new file mode 100644 index 0000000..a8ae1f2 --- /dev/null +++ b/fdfd_tools/waveguide.py @@ -0,0 +1,309 @@ +""" +Various operators and helper functions for solving for waveguide modes. + +Assuming a z-dependence of the from exp(-i * wavenumber * z), we can simplify Maxwell's + equations in the absence of sources to the form + +A @ [H_x, H_y] = wavenumber**2 * [H_x, H_y] + +with A = +omega**2 * epsilon * mu + +epsilon * [[-Dy], [Dx]] / epsilon * [-Dy, Dx] + +[[Dx], [Dy]] / mu * [Dx, Dy] * mu + +which is the form used in this file. + +As the z-dependence is known, all the functions in this file assume a 2D grid + (ie. dxes = [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dx_h_0, ...], [dy_h_0, ...]]]) + with propagation along the z axis. +""" + +from typing import List, Tuple +import numpy +from numpy.linalg import norm +import scipy.sparse as sparse + +from . import unvec, dx_lists_t, field_t, vfield_t +from . import operators + + +__author__ = 'Jan Petykiewicz' + + +def operator(omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + ) -> sparse.spmatrix: + """ + Waveguide operator of the form + + omega**2 * epsilon * mu + + epsilon * [[-Dy], [Dx]] / epsilon * [-Dy, Dx] + + [[Dx], [Dy]] / mu * [Dx, Dy] * mu + + for use with a field vector of the form [H_x, H_y]. + + This operator can be used to form an eigenvalue problem of the form + A @ [H_x, H_y] = wavenumber**2 * [H_x, H_y] + + which can then be solved for the eigenmodes of the system (an exp(-i * wavenumber * z) + z-dependence is assumed for the fields). + + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Sparse matrix representation of the operator + """ + if numpy.any(numpy.equal(mu, None)): + mu = numpy.ones_like(epsilon) + + Dfx, Dfy = operators.deriv_forward(dxes[0]) + Dbx, Dby = operators.deriv_back(dxes[1]) + + eps_parts = numpy.split(epsilon, 3) + eps_yx = sparse.diags(numpy.hstack((eps_parts[1], eps_parts[0]))) + eps_z_inv = sparse.diags(1 / eps_parts[2]) + + mu_parts = numpy.split(mu, 3) + mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) + mu_z_inv = sparse.diags(1 / mu_parts[2]) + + op = omega ** 2 * eps_yx @ mu_xy + \ + eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx)) + \ + sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy + + return op + + +def normalized_fields(v: numpy.ndarray, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> Tuple[vfield_t, vfield_t]: + """ + Given a vector v containing the vectorized H_x and H_y fields, + returns normalized, vectorized E and H fields for the system. + + :param v: Vector containing H_x and H_y fields + :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Normalized, vectorized (e, h) containing all vector components. + """ + e = v2e(v, wavenumber, omega, dxes, epsilon, mu=mu) + h = v2h(v, wavenumber, dxes, mu=mu) + + shape = [s.size for s in dxes[0]] + dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)] + + E = unvec(e, shape) + H = unvec(h, shape) + + S1 = E[0] * numpy.roll(numpy.conj(H[1]), 1, axis=0) * dxes_real[0][1] * dxes_real[1][0] + S2 = E[1] * numpy.roll(numpy.conj(H[0]), 1, axis=1) * dxes_real[0][0] * dxes_real[1][1] + S = 0.25 * ((S1 + numpy.roll(S1, 1, axis=0)) - + (S2 + numpy.roll(S2, 1, axis=1))) + P = 0.5 * numpy.real(S.sum()) + assert P > 0, 'Found a mode propagating in the wrong direction! P={}'.format(P) + + norm_amplitude = 1 / numpy.sqrt(P) + norm_angle = -numpy.angle(e[e.size//2]) + norm_factor = norm_amplitude * numpy.exp(1j * norm_angle) + + e *= norm_factor + h *= norm_factor + + return e, h + + +def v2h(v: numpy.ndarray, + wavenumber: complex, + dxes: dx_lists_t, + mu: vfield_t = None + ) -> vfield_t: + """ + Given a vector v containing the vectorized H_x and H_y fields, + returns a vectorized H including all three H components. + + :param v: Vector containing H_x and H_y fields + :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Vectorized H field with all vector components + """ + Dfx, Dfy = operators.deriv_forward(dxes[0]) + op = sparse.hstack((Dfx, Dfy)) + + if not numpy.any(numpy.equal(mu, None)): + mu_parts = numpy.split(mu, 3) + mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) + mu_z_inv = sparse.diags(1 / mu_parts[2]) + + op = mu_z_inv @ op @ mu_xy + + w = op @ v / (1j * wavenumber) + return numpy.hstack((v, w)).flatten() + + +def v2e(v: numpy.ndarray, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> vfield_t: + """ + Given a vector v containing the vectorized H_x and H_y fields, + returns a vectorized E containing all three E components + + :param v: Vector containing H_x and H_y fields + :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Vectorized E field with all vector components. + """ + h2eop = h2e(wavenumber, omega, dxes, epsilon) + return h2eop @ v2h(v, wavenumber, dxes, mu) + + +def e2h(wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Returns an operator which, when applied to a vectorized E eigenfield, produces + the vectorized H eigenfield. + + :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Sparse matrix representation of the operator + """ + op = curl_e(wavenumber, dxes) / (-1j * omega) + if not numpy.any(numpy.equal(mu, None)): + op = sparse.diags(1 / mu) @ op + return op + + +def h2e(wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t + ) -> sparse.spmatrix: + """ + Returns an operator which, when applied to a vectorized H eigenfield, produces + the vectorized E eigenfield. + + :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param epsilon: Vectorized dielectric constant grid + :return: Sparse matrix representation of the operator + """ + op = sparse.diags(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes) + return op + + +def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: + """ + Discretized curl operator for use with the waveguide E field. + + :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :return: Sparse matrix representation of the operator + """ + n = 1 + for d in dxes[0]: + n *= len(d) + + Bz = -1j * wavenumber * sparse.eye(n) + Dfx, Dfy = operators.deriv_forward(dxes[0]) + return operators.cross([Dfx, Dfy, Bz]) + + +def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: + """ + Discretized curl operator for use with the waveguide H field. + + :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :return: Sparse matrix representation of the operator + """ + n = 1 + for d in dxes[1]: + n *= len(d) + + Bz = -1j * wavenumber * sparse.eye(n) + Dbx, Dby = operators.deriv_back(dxes[1]) + return operators.cross([Dbx, Dby, Bz]) + + +def h_err(h: vfield_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> float: + """ + Calculates the relative error in the H field + + :param h: Vectorized H field + :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Relative error norm(OP @ h) / norm(h) + """ + ce = curl_e(wavenumber, dxes) + ch = curl_h(wavenumber, dxes) + + eps_inv = sparse.diags(1 / epsilon) + + if numpy.any(numpy.equal(mu, None)): + op = ce @ eps_inv @ ch @ h - omega ** 2 * h + else: + op = ce @ eps_inv @ ch @ h - omega ** 2 * (mu * h) + + return norm(op) / norm(h) + + +def e_err(e: vfield_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> float: + """ + Calculates the relative error in the E field + + :param e: Vectorized E field + :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Relative error norm(OP @ e) / norm(e) + """ + ce = curl_e(wavenumber, dxes) + ch = curl_h(wavenumber, dxes) + + if numpy.any(numpy.equal(mu, None)): + op = ch @ ce @ e - omega ** 2 * (epsilon * e) + else: + mu_inv = sparse.diags(1 / mu) + op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e) + + return norm(op) / norm(e) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py new file mode 100644 index 0000000..df62143 --- /dev/null +++ b/fdfd_tools/waveguide_mode.py @@ -0,0 +1,301 @@ +from typing import Dict, List +import numpy +import scipy.sparse as sparse +import scipy.sparse.linalg as spalg + +from . import vec, unvec, dx_lists_t, vfield_t, field_t +from . import operators, waveguide, functional + + +def solve_waveguide_mode_2d(mode_number: int, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + wavenumber_correction: bool = True + ) -> Dict[str, complex or field_t]: + """ + Given a 2d region, attempts to solve for the eigenmode with the specified mode number. + + :param mode_number: Number of the mode, 0-indexed + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param epsilon: Dielectric constant + :param mu: Magnetic permeability (default 1 everywhere) + :param wavenumber_correction: Whether to correct the wavenumber to + account for numerical dispersion (default True) + :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} + """ + + ''' + Solve for the largest-magnitude eigenvalue of the real operator + by using power iteration. + ''' + dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] + + A_r = waveguide.operator(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) + + # Use power iteration for 20 steps to estimate the dominant eigenvector + v = numpy.random.rand(A_r.shape[0]) + for _ in range(20): + v = A_r @ v + v /= numpy.linalg.norm(v) + + lm_eigval = v @ A_r @ v + + ''' + Shift by the absolute value of the largest eigenvalue, then find a few of the + largest (shifted) eigenvalues. The shift ensures that we find the largest + _positive_ eigenvalues, since any negative eigenvalues will be shifted to the range + 0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval) + ''' + shifted_A_r = A_r + abs(lm_eigval) * sparse.eye(A_r.shape[0]) + eigvals, eigvecs = spalg.eigs(shifted_A_r, which='LM', k=mode_number + 3, ncv=50) + + # Pick the eigenvalue we want from the few we found + k = eigvals.argsort()[-(mode_number+1)] + v = eigvecs[:, k] + + ''' + Now solve for the eigenvector of the full operator, using the real operator's + eigenvector as an initial guess for Rayleigh quotient iteration. + ''' + A = waveguide.operator(omega, dxes, epsilon, mu) + + eigval = None + for _ in range(40): + eigval = v @ A @ v + if numpy.linalg.norm(A @ v - eigval * v) < 1e-13: + break + w = spalg.spsolve(A - eigval * sparse.eye(A.shape[0]), v) + v = w / numpy.linalg.norm(w) + + # Calculate the wave-vector (force the real part to be positive) + wavenumber = numpy.sqrt(eigval) + wavenumber *= numpy.sign(numpy.real(wavenumber)) + + e, h = waveguide.normalized_fields(v, wavenumber, omega, dxes, epsilon, mu) + + ''' + Perform correction on wavenumber to account for numerical dispersion. + + See Numerical Dispersion in Taflove's FDTD book. + This correction term reduces the error in emitted power, but additional + error is introduced into the E_err and H_err terms. This effect becomes + more pronounced as beta increases. + ''' + if wavenumber_correction: + wavenumber -= 2 * numpy.sin(numpy.real(wavenumber / 2)) - numpy.real(wavenumber) + + shape = [d.size for d in dxes[0]] + fields = { + 'wavenumber': wavenumber, + 'E': unvec(e, shape), + 'H': unvec(h, shape), + } + + return fields + + +def solve_waveguide_mode(mode_number: int, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + epsilon: field_t, + mu: field_t = None, + wavenumber_correction: bool = True + ) -> Dict[str, complex or numpy.ndarray]: + """ + Given a 3D grid, selects a slice from the grid and attempts to + solve for an eigenmode propagating through that slice. + + :param mode_number: Number of the mode, 0-indexed + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param axis: Propagation axis (0=x, 1=y, 2=z) + :param polarity: Propagation direction (+1 for +ve, -1 for -ve) + :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] should select only one + :param epsilon: Dielectric constant + :param mu: Magnetic permeability (default 1 everywhere) + :param wavenumber_correction: Whether to correct the wavenumber to + account for numerical dispersion (default True) + :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} + """ + if mu is None: + mu = [numpy.ones_like(epsilon[0])] * 3 + + ''' + Solve the 2D problem in the specified plane + ''' + # Define rotation to set z as propagation direction + order = numpy.roll(range(3), 2 - axis) + reverse_order = numpy.roll(range(3), axis - 2) + + # Reduce to 2D and solve the 2D problem + args_2d = { + 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], + 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), + 'mu': vec([mu[i][slices].transpose(order) for i in order]), + 'wavenumber_correction': wavenumber_correction, + } + fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d) + + ''' + Apply corrections and expand to 3D + ''' + # Scale based on dx in propagation direction + dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes]) + + # Adjust for propagation direction + fields_2d['E'][2] *= polarity + fields_2d['H'][2] *= polarity + + # Apply phase shift to H-field + d_prop = 0.5 * sum(dxab_forward) + for a in range(3): + fields_2d['H'][a] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * d_prop) + + # Expand E, H to full epsilon space we were given + E = [None]*3 + H = [None]*3 + for a, o in enumerate(reverse_order): + E[a] = numpy.zeros_like(epsilon[0], dtype=complex) + H[a] = numpy.zeros_like(epsilon[0], dtype=complex) + + E[a][slices] = fields_2d['E'][o][:, :, None].transpose(reverse_order) + H[a][slices] = fields_2d['H'][o][:, :, None].transpose(reverse_order) + + results = { + 'wavenumber': fields_2d['wavenumber'], + 'H': H, + 'E': E, + } + + return results + + +def compute_source(E: field_t, + H: field_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + mu: field_t = None, + ) -> field_t: + """ + Given an eigenmode obtained by solve_waveguide_mode, returns the current source distribution + necessary to position a unidirectional source at the slice location. + + :param E: E-field of the mode + :param H: H-field of the mode (advanced by half of a Yee cell from E) + :param wavenumber: Wavenumber of the mode + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param axis: Propagation axis (0=x, 1=y, 2=z) + :param polarity: Propagation direction (+1 for +ve, -1 for -ve) + :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] should select only one + :param mu: Magnetic permeability (default 1 everywhere) + :return: J distribution for the unidirectional source + """ + if mu is None: + mu = [1] * 3 + + J = [None]*3 + M = [None]*3 + + src_order = numpy.roll(range(3), axis) + exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) + J[src_order[0]] = numpy.zeros_like(E[0]) + J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity + J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity + + M[src_order[0]] = numpy.zeros_like(E[0]) + M[src_order[1]] = +numpy.roll(E[src_order[2]], -1, axis=axis) + M[src_order[2]] = -numpy.roll(E[src_order[1]], -1, axis=axis) + + A1f = functional.curl_h(dxes) + + Jm_iw = A1f([M[k] / mu[k] for k in range(3)]) + for k in range(3): + J[k] += Jm_iw[k] / (-1j * omega) + + return J + + +def compute_overlap_e(E: field_t, + H: field_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + mu: field_t = None, + ) -> field_t: + """ + Given an eigenmode obtained by solve_waveguide_mode, calculates overlap_e for the + mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) + [assumes reflection symmetry]. + + overlap_e makes use of the e2h operator to collapse the above expression into + (vec(E) @ vec(overlap_e)), allowing for simple calculation of the mode overlap. + + :param E: E-field of the mode + :param H: H-field of the mode (advanced by half of a Yee cell from E) + :param wavenumber: Wavenumber of the mode + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param axis: Propagation axis (0=x, 1=y, 2=z) + :param polarity: Propagation direction (+1 for +ve, -1 for -ve) + :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] should select only one + :param mu: Magnetic permeability (default 1 everywhere) + :return: overlap_e for calculating the mode overlap + """ + cross_plane = [slice(None)] * 3 + cross_plane[axis] = slices[axis] + + # Determine phase factors for parallel slices + a_shape = numpy.roll([-1, 1, 1], axis) + a_E = numpy.real(dxes[0][axis]).cumsum() + a_H = numpy.real(dxes[1][axis]).cumsum() + iphi = -polarity * 1j * wavenumber + phase_E = numpy.exp(iphi * (a_E - a_E[slices[axis]])).reshape(a_shape) + phase_H = numpy.exp(iphi * (a_H - a_H[slices[axis]])).reshape(a_shape) + + # Expand our slice to the entire grid using the calculated phase factors + Ee = [None]*3 + He = [None]*3 + for k in range(3): + Ee[k] = phase_E * E[k][tuple(cross_plane)] + He[k] = phase_H * H[k][tuple(cross_plane)] + + + # Write out the operator product for the mode orthogonality integral + domain = numpy.zeros_like(E[0], dtype=int) + domain[slices] = 1 + + npts = E[0].size + dn = numpy.zeros(npts * 3, dtype=int) + dn[0:npts] = 1 + dn = numpy.roll(dn, npts * axis) + + e2h = operators.e2h(omega, dxes, mu) + ds = sparse.diags(vec([domain]*3)) + h_cross_ = operators.poynting_h_cross(vec(He), dxes) + e_cross_ = operators.poynting_e_cross(vec(Ee), dxes) + + overlap_e = dn @ ds @ (-h_cross_ + e_cross_ @ e2h) + + # Normalize + dx_forward = dxes[0][axis][slices[axis]] + norm_factor = numpy.abs(overlap_e @ vec(Ee)) + overlap_e /= norm_factor * dx_forward + + return unvec(overlap_e, E[0].shape) diff --git a/float_raster.py b/float_raster.py new file mode 120000 index 0000000..9ee7c7f --- /dev/null +++ b/float_raster.py @@ -0,0 +1 @@ +../float_raster/float_raster.py \ No newline at end of file diff --git a/gridlock b/gridlock new file mode 120000 index 0000000..a39870d --- /dev/null +++ b/gridlock @@ -0,0 +1 @@ +../gridlock/gridlock/ \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9294a9d --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +setup(name='fdfd_tools', + version='0.1', + description='FDFD Electromagnetic simulation tools', + author='Jan Petykiewicz', + author_email='anewusername@gmail.com', + url='https://mpxd.net/gogs/jan/fdfd_tools', + packages=find_packages(), + install_requires=[ + 'numpy', + 'scipy', + ], + extras_require={ + }, + ) From 35555cf4b3dc39cd3fcf75a5ed0d6f9422307538 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 30 May 2016 23:01:44 -0700 Subject: [PATCH 005/437] remove accidental symlinks --- float_raster.py | 1 - gridlock | 1 - 2 files changed, 2 deletions(-) delete mode 120000 float_raster.py delete mode 120000 gridlock diff --git a/float_raster.py b/float_raster.py deleted file mode 120000 index 9ee7c7f..0000000 --- a/float_raster.py +++ /dev/null @@ -1 +0,0 @@ -../float_raster/float_raster.py \ No newline at end of file diff --git a/gridlock b/gridlock deleted file mode 120000 index a39870d..0000000 --- a/gridlock +++ /dev/null @@ -1 +0,0 @@ -../gridlock/gridlock/ \ No newline at end of file From 8f202fd0610e19625a8611a290c46fa863de4ebe Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 17 Jun 2016 16:49:39 -0700 Subject: [PATCH 006/437] Add shift_with_mirror, and add shift_distance argument to rotation() --- fdfd_tools/operators.py | 62 +++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index 7a72d8e..2fb215c 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -215,12 +215,13 @@ def m2j(omega: complex, return op -def rotation(axis: int, shape: List[int]) -> sparse.spmatrix: +def rotation(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmatrix: """ Utility operator for performing a circular shift along a specified axis by 1 element. :param axis: Axis to shift along. x=0, y=1, z=2 :param shape: Shape of the grid being shifted + :param shift_distance: Number of cells to shift by. May be negative. Default 1. :return: Sparse matrix for performing the circular shift """ if len(shape) not in (2, 3): @@ -228,12 +229,11 @@ def rotation(axis: int, shape: List[int]) -> sparse.spmatrix: if axis not in range(len(shape)): raise Exception('Invalid direction: {}, shape is {}'.format(axis, shape)) - n = numpy.prod(shape) - - shifts = [1 if k == axis else 0 for k in range(3)] + shifts = [abs(shift_distance) if a == axis else 0 for a in range(3)] shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts)] ijk = numpy.meshgrid(*shifted_diags, indexing='ij') + n = numpy.prod(shape) i_ind = numpy.arange(n) j_ind = ijk[0] + ijk[1] * shape[0] if len(shape) == 3: @@ -241,8 +241,52 @@ def rotation(axis: int, shape: List[int]) -> sparse.spmatrix: vij = (numpy.ones(n), (i_ind, j_ind.flatten(order='F'))) - D = sparse.csr_matrix(vij, shape=(n, n)) - return D + d = sparse.csr_matrix(vij, shape=(n, n)) + + if shift_distance < 0: + d = d.T + + return d + + +def shift_with_mirror(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmatrix: + """ + Utility operator for performing an n-element shift along a specified axis, with mirror + boundary conditions applied to the cells beyond the receding edge. + + :param axis: Axis to shift along. x=0, y=1, z=2 + :param shape: Shape of the grid being shifted + :param shift_distance: Number of cells to shift by. May be negative. Default 1. + :return: Sparse matrix for performing the circular shift + """ + if len(shape) not in (2, 3): + raise Exception('Invalid shape: {}'.format(shape)) + if axis not in range(len(shape)): + raise Exception('Invalid direction: {}, shape is {}'.format(axis, shape)) + if shift_distance >= shape[axis]: + raise Exception('Shift ({}) is too large for axis {} of size {}'.format( + shift_distance, axis, shape[axis])) + + def mirrored_range(n, s): + v = numpy.arange(n) + s + v = numpy.where(v >= n, 2 * n - v - 1, v) + v = numpy.where(v < 0, - 1 - v, v) + return v + + shifts = [shift_distance if a == axis else 0 for a in range(3)] + shifted_diags = [mirrored_range(n, s) for n, s in zip(shape, shifts)] + ijk = numpy.meshgrid(*shifted_diags, indexing='ij') + + n = numpy.prod(shape) + i_ind = numpy.arange(n) + j_ind = ijk[0] + ijk[1] * shape[0] + if len(shape) == 3: + j_ind += ijk[2] * shape[0] * shape[1] + + vij = (numpy.ones(n), (i_ind, j_ind.flatten(order='F'))) + + d = sparse.csr_matrix(vij, shape=(n, n)) + return d def deriv_forward(dx_e: List[numpy.ndarray]) -> List[sparse.spmatrix]: @@ -258,7 +302,7 @@ def deriv_forward(dx_e: List[numpy.ndarray]) -> List[sparse.spmatrix]: dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij') def deriv(axis): - return rotation(axis, shape) - sparse.eye(n) + return rotation(axis, shape, 1) - sparse.eye(n) Ds = [sparse.diags(+1 / dx.flatten(order='F')) @ deriv(a) for a, dx in enumerate(dx_e_expanded)] @@ -279,9 +323,9 @@ def deriv_back(dx_h: List[numpy.ndarray]) -> List[sparse.spmatrix]: dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij') def deriv(axis): - return rotation(axis, shape) - sparse.eye(n) + return rotation(axis, shape, -1) - sparse.eye(n) - Ds = [sparse.diags(-1 / dx.flatten(order='F')) @ deriv(a).T + Ds = [sparse.diags(-1 / dx.flatten(order='F')) @ deriv(a) for a, dx in enumerate(dx_h_expanded)] return Ds From 5bf902212e258806d965b25646ed4249d8ec009b Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 01:20:30 -0700 Subject: [PATCH 007/437] Comment cleanup --- fdfd_tools/grid.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/grid.py b/fdfd_tools/grid.py index 00b4bdc..12f0d9e 100644 --- a/fdfd_tools/grid.py +++ b/fdfd_tools/grid.py @@ -26,7 +26,7 @@ def prepare_s_function(ln_R: float = -16, use. """ def s_factor(distance: numpy.ndarray) -> numpy.ndarray: - s_max = (m + 1) * ln_R / 2 # / 2 because we have assume boundaries + s_max = (m + 1) * ln_R / 2 # / 2 because we assume periodic boundaries return s_max * (distance ** m) return s_factor @@ -43,13 +43,16 @@ def uniform_grid_scpml(shape: numpy.ndarray or List[int], If you want something more fine-grained, check out stretch_with_scpml(...). :param shape: Shape of the grid, including the PMLs (which are 2*thicknesses thick) - :param thicknesses: [th_x, th_y, th_z] Thickness of the PML in each direction. Both polarities are added. + :param thicknesses: [th_x, th_y, th_z] Thickness of the PML in each direction. + Both polarities are added. Each th_ of pml is applied twice, once on each edge of the grid along the given axis. th_* may be zero, in which case no pml is added. :param omega: Angular frequency for the simulation - :param epsilon_effective: Effective epsilon of the PML. Match this to the material at the edge of your grid. + :param epsilon_effective: Effective epsilon of the PML. Match this to the material + at the edge of your grid. Default 1. - :param s_function: s_function created by prepare_s_function(...), allowing customization of pml parameters. + :param s_function: s_function created by prepare_s_function(...), allowing + customization of pml parameters. Default uses prepare_s_function() with no parameters. :return: Complex cell widths (dx_lists) """ @@ -145,9 +148,9 @@ def stretch_with_scpml(dxes: dx_lists_t, return dxes -def generate_dx(pos: List[numpy.ndarray]) -> dx_lists_t: +def generate_periodic_dx(pos: List[numpy.ndarray]) -> DXList: """ - Given a list of 3 ndarrays cell centers, creates the cell width parameters. + Given a list of 3 ndarrays cell centers, creates the cell width parameters for a periodic grid. :param pos: List of 3 ndarrays of cell centers :return: (dx_a, dx_b) cell widths (no pml) From 05d2557f6fa2f554def2586b4c21f09b2d9e0e33 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 01:20:51 -0700 Subject: [PATCH 008/437] Style fixes --- fdfd_tools/functional.py | 54 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/fdfd_tools/functional.py b/fdfd_tools/functional.py index 4d573db..1922964 100644 --- a/fdfd_tools/functional.py +++ b/fdfd_tools/functional.py @@ -13,7 +13,7 @@ from . import dx_lists_t, field_t __author__ = 'Jan Petykiewicz' -functional_matrix = Callable[[List[numpy.ndarray]], List[numpy.ndarray]] +functional_matrix = Callable[[field_t], field_t] def curl_h(dxes: dx_lists_t) -> functional_matrix: @@ -28,11 +28,11 @@ def curl_h(dxes: dx_lists_t) -> functional_matrix: def dH(f, ax): return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] - def ch_fun(H: List[numpy.ndarray]) -> List[numpy.ndarray]: - E = [dH(H[2], 1) - dH(H[1], 2), - dH(H[0], 2) - dH(H[2], 0), - dH(H[1], 0) - dH(H[0], 1)] - return E + def ch_fun(h: field_t) -> field_t: + e = [dh(h[2], 1) - dh(h[1], 2), + dh(h[0], 2) - dh(h[2], 0), + dh(h[1], 0) - dh(h[0], 1)] + return e return ch_fun @@ -49,11 +49,11 @@ def curl_e(dxes: dx_lists_t) -> functional_matrix: def dE(f, ax): return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] - def ce_fun(E: List[numpy.ndarray]) -> List[numpy.ndarray]: - H = [dE(E[2], 1) - dE(E[1], 2), - dE(E[0], 2) - dE(E[2], 0), - dE(E[1], 0) - dE(E[0], 1)] - return H + def ce_fun(e: field_t) -> field_t: + h = [de(e[2], 1) - de(e[1], 2), + de(e[0], 2) - de(e[2], 0), + de(e[1], 0) - de(e[0], 1)] + return h return ce_fun @@ -77,13 +77,13 @@ def e_full(omega: complex, ch = curl_h(dxes) ce = curl_e(dxes) - def op_1(E): - curls = ch(ce(E)) - return [c - omega ** 2 * e * x for c, e, x in zip(curls, epsilon, E)] + def op_1(e): + curls = ch(ce(e)) + return [c - omega ** 2 * e * x for c, e, x in zip(curls, epsilon, e)] - def op_mu(E): - curls = ch([m * y for m, y in zip(mu, ce(E))]) - return [c - omega ** 2 * e * x for c, e, x in zip(curls, epsilon, E)] + def op_mu(e): + curls = ch([m * y for m, y in zip(mu, ce(e))]) + return [c - omega ** 2 * p * x for c, p, x in zip(curls, epsilon, e)] if numpy.any(numpy.equal(mu, None)): return op_1 @@ -108,13 +108,13 @@ def eh_full(omega: complex, ch = curl_h(dxes) ce = curl_e(dxes) - def op_1(E, H): - return ([c - 1j * omega * e * x for c, e, x in zip(ch(H), epsilon, E)], - [c + 1j * omega * y for c, y in zip(ce(E), H)]) + def op_1(e, h): + return ([c - 1j * omega * p * x for c, p, x in zip(ch(h), epsilon, e)], + [c + 1j * omega * y for c, y in zip(ce(e), h)]) - def op_mu(E, H): - return ([c - 1j * omega * e * x for c, e, x in zip(ch(H), epsilon, E)], - [c + 1j * omega * m * y for c, m, y in zip(ce(E), mu, H)]) + def op_mu(e, h): + return ([c - 1j * omega * p * x for c, p, x in zip(ch(h), epsilon, e)], + [c + 1j * omega * m * y for c, m, y in zip(ce(e), mu, h)]) if numpy.any(numpy.equal(mu, None)): return op_1 @@ -137,11 +137,11 @@ def e2h(omega: complex, """ A2 = curl_e(dxes) - def e2h_1_1(E): - return [y / (-1j * omega) for y in A2(E)] + def e2h_1_1(e): + return [y / (-1j * omega) for y in A2(e)] - def e2h_mu(E): - return [y / (-1j * omega * m) for y, m in zip(A2(E), mu)] + def e2h_mu(e): + return [y / (-1j * omega * m) for y, m in zip(A2(e), mu)] if numpy.any(numpy.equal(mu, None)): return e2h_1_1 From 2cac441717c34c3de9081083ef9816e0ff555fad Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 02:57:04 -0700 Subject: [PATCH 009/437] Add PEC, PMC options for E, H wave operators --- fdfd_tools/operators.py | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index 2fb215c..f3d501a 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -45,7 +45,8 @@ __author__ = 'Jan Petykiewicz' def e_full(omega: complex, dxes: dx_lists_t, epsilon: vfield_t, - mu: vfield_t = None + mu: vfield_t = None, + pec: vfield_t = None, ) -> sparse.spmatrix: """ Wave operator del x (1/mu * del x) - omega**2 * epsilon, for use with E-field, @@ -57,19 +58,28 @@ def e_full(omega: complex, :param omega: Angular frequency of the simulation :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param epsilon: Vectorized dielectric constant - :param mu: Vectorized magnetic permeability (default 1 everywhere).. + :param mu: Vectorized magnetic permeability (default 1 everywhere). + :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted + as containing a perfect electrical conductor (PEC). :return: Sparse matrix containing the wave operator """ ce = curl_e(dxes) ch = curl_h(dxes) - e = sparse.diags(epsilon) + ev = epsilon + if numpy.any(numpy.equal(pec, None)): + pm = sparse.eye(epsilon.size) + else: + pm = sparse.diags(numpy.where(pec, 0, 1)) # Set pm to (not PEC) + ev = numpy.where(pec, 1.0, ev) # Set epsilon to 1 at PEC + + e = sparse.diags(ev) if numpy.any(numpy.equal(mu, None)): m_div = sparse.eye(epsilon.size) else: m_div = sparse.diags(1 / mu) - op = ch @ m_div @ ce - omega**2 * e + op = pm @ ch @ m_div @ ce @ pm - omega**2 * e return op @@ -99,7 +109,8 @@ def e_full_preconditioners(dxes: dx_lists_t def h_full(omega: complex, dxes: dx_lists_t, epsilon: vfield_t, - mu: vfield_t = None + mu: vfield_t = None, + pmc: vfield_t = None, ) -> sparse.spmatrix: """ Wave operator del x (1/epsilon * del x) - omega**2 * mu, for use with H-field, @@ -110,18 +121,28 @@ def h_full(omega: complex, :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param epsilon: Vectorized dielectric constant :param mu: Vectorized magnetic permeability (default 1 everywhere) + :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted + as containing a perfect magnetic conductor (PMC). :return: Sparse matrix containing the wave operator """ ec = curl_e(dxes) hc = curl_h(dxes) - e_div = sparse.diags(1 / epsilon) - if numpy.any(numpy.equal(mu, None)): - m = sparse.eye(epsilon.size) + if mu is None: + mv = numpy.ones_like(epsilon) else: - m = sparse.diags(mu) + mv = mu - A = ec @ e_div @ hc - omega**2 * m + if numpy.any(numpy.equal(pmc, None)): + pe = sparse.eye(epsilon.size) + else: + pe = sparse.diags(numpy.where(pmc, 0, 1)) # Set pe to (not PMC) + mv = numpy.where(pmc, 1.0, mv) # Set mu to 1 at PMC + + e_div = sparse.diags(1 / epsilon) + m = sparse.diags(mv) + + A = pe @ ec @ e_div @ hc @ pe - omega**2 * m return A From ec825945b6d0d901141c1bd102a461e6f5e14ff9 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 03:00:13 -0700 Subject: [PATCH 010/437] Fix annotation --- fdfd_tools/grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/grid.py b/fdfd_tools/grid.py index 12f0d9e..a4accb8 100644 --- a/fdfd_tools/grid.py +++ b/fdfd_tools/grid.py @@ -148,7 +148,7 @@ def stretch_with_scpml(dxes: dx_lists_t, return dxes -def generate_periodic_dx(pos: List[numpy.ndarray]) -> DXList: +def generate_periodic_dx(pos: List[numpy.ndarray]) -> dx_lists_t: """ Given a list of 3 ndarrays cell centers, creates the cell width parameters for a periodic grid. From 02ec6d67d601acc54c3305b22491008611f4f94e Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 03:01:01 -0700 Subject: [PATCH 011/437] Fix function case --- fdfd_tools/functional.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/functional.py b/fdfd_tools/functional.py index 1922964..f184223 100644 --- a/fdfd_tools/functional.py +++ b/fdfd_tools/functional.py @@ -25,7 +25,7 @@ def curl_h(dxes: dx_lists_t) -> functional_matrix: """ dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') - def dH(f, ax): + def dh(f, ax): return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] def ch_fun(h: field_t) -> field_t: @@ -46,7 +46,7 @@ def curl_e(dxes: dx_lists_t) -> functional_matrix: """ dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') - def dE(f, ax): + def de(f, ax): return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] def ce_fun(e: field_t) -> field_t: From 16bd864e90473533b6a8afd1bc0a0ae3b8405812 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 14:22:39 -0700 Subject: [PATCH 012/437] Remove unused import --- fdfd_tools/grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/grid.py b/fdfd_tools/grid.py index a4accb8..2faf928 100644 --- a/fdfd_tools/grid.py +++ b/fdfd_tools/grid.py @@ -2,7 +2,7 @@ Functions for creating stretched coordinate PMLs. """ -from typing import List, Tuple, Callable +from typing import List, Callable import numpy __author__ = 'Jan Petykiewicz' From 28f85712ce9ba21e5efbe60247dffe3be6686969 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 14:23:24 -0700 Subject: [PATCH 013/437] Use autoshifted_dxyz --- examples/test.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/test.py b/examples/test.py index 23303b2..ff7037e 100644 --- a/examples/test.py +++ b/examples/test.py @@ -104,9 +104,7 @@ def test0(): eps=n_air ** 2, num_points=24) - dx0_a = grid.dxyz - dx0_b = [grid.shifted_dxyz(which_shifts=a)[a] for a in range(3)] - dxes = [dx0_a, dx0_b] + dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (0, 1, 2): for p in (-1, 1): dxes = fdfd_tools.grid.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, @@ -156,9 +154,7 @@ def test1(): grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3) grid.draw_cuboid(center=center, dimensions=[8e3, w, th], eps=n_wg**2) - dx0_a = grid.dxyz - dx0_b = [grid.shifted_dxyz(which_shifts=a)[a] for a in range(3)] - dxes = [dx0_a, dx0_b] + dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (0, 1, 2): for p in (-1, 1): dxes = fdfd_tools.grid.stretch_with_scpml(dxes,omega=omega, axis=a, polarity=p, From a512a889304235bf5d7aaa970d27f91898914590 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 14:23:51 -0700 Subject: [PATCH 014/437] Add test code for PEC --- examples/test.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/test.py b/examples/test.py index ff7037e..69b726c 100644 --- a/examples/test.py +++ b/examples/test.py @@ -177,7 +177,12 @@ def test1(): J = waveguide_mode.compute_source(**wg_args, **wg_results) H_overlap = waveguide_mode.compute_overlap_e(**wg_args, **wg_results) - A = fdfd_tools.operators.e_full(omega, dxes, vec(grid.grids)).tocsr() + pecg = gridlock.Grid(edge_coords, initial=0, num_grids=3) + # pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) + pecg.grids = [numpy.sign(r) for r in pecg.grids] + # pecg.visualize_isosurface() + + A = fdfd_tools.operators.e_full(omega, dxes, vec(grid.grids), pec=vec(pecg.grids)).tocsr() b = -1j * omega * vec(J) x = solve_A(A, b) E = unvec(x, grid.shape) From e288e590211380dc937760038dcf7ed89f71068f Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 16:45:38 -0700 Subject: [PATCH 015/437] PEC and PMC for all wave operators --- examples/test.py | 12 ++++++--- fdfd_tools/operators.py | 54 ++++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/examples/test.py b/examples/test.py index 69b726c..b41438a 100644 --- a/examples/test.py +++ b/examples/test.py @@ -177,12 +177,18 @@ def test1(): J = waveguide_mode.compute_source(**wg_args, **wg_results) H_overlap = waveguide_mode.compute_overlap_e(**wg_args, **wg_results) - pecg = gridlock.Grid(edge_coords, initial=0, num_grids=3) + pecg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) # pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) - pecg.grids = [numpy.sign(r) for r in pecg.grids] # pecg.visualize_isosurface() - A = fdfd_tools.operators.e_full(omega, dxes, vec(grid.grids), pec=vec(pecg.grids)).tocsr() + pmcg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) + # pmcg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) + # pmcg.visualize_isosurface() + + A = fdfd_tools.operators.e_full(omega, dxes, + epsilon=vec(grid.grids), + pec=vec(pecg.grids), + pmc=vec(pmcg.grids)).tocsr() b = -1j * omega * vec(J) x = solve_A(A, b) E = unvec(x, grid.shape) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index f3d501a..8045aaa 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -47,6 +47,7 @@ def e_full(omega: complex, epsilon: vfield_t, mu: vfield_t = None, pec: vfield_t = None, + pmc: vfield_t = None, ) -> sparse.spmatrix: """ Wave operator del x (1/mu * del x) - omega**2 * epsilon, for use with E-field, @@ -61,6 +62,8 @@ def e_full(omega: complex, :param mu: Vectorized magnetic permeability (default 1 everywhere). :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted as containing a perfect electrical conductor (PEC). + :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted + as containing a perfect magnetic conductor (PMC). :return: Sparse matrix containing the wave operator """ ce = curl_e(dxes) @@ -68,10 +71,15 @@ def e_full(omega: complex, ev = epsilon if numpy.any(numpy.equal(pec, None)): + pe = sparse.eye(epsilon.size) + else: + pe = sparse.diags(numpy.where(pec, 0, 1)) # Set pe to (not PEC) + ev = numpy.where(pec, 1.0, ev) # Set epsilon to 1 at PEC + + if numpy.any(numpy.equal(pmc, None)): pm = sparse.eye(epsilon.size) else: - pm = sparse.diags(numpy.where(pec, 0, 1)) # Set pm to (not PEC) - ev = numpy.where(pec, 1.0, ev) # Set epsilon to 1 at PEC + pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC) e = sparse.diags(ev) if numpy.any(numpy.equal(mu, None)): @@ -79,7 +87,7 @@ def e_full(omega: complex, else: m_div = sparse.diags(1 / mu) - op = pm @ ch @ m_div @ ce @ pm - omega**2 * e + op = pe @ ch @ pm @ m_div @ ce @ pe - omega**2 * e return op @@ -110,6 +118,7 @@ def h_full(omega: complex, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, + pec: vfield_t = None, pmc: vfield_t = None, ) -> sparse.spmatrix: """ @@ -121,6 +130,8 @@ def h_full(omega: complex, :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param epsilon: Vectorized dielectric constant :param mu: Vectorized magnetic permeability (default 1 everywhere) + :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted + as containing a perfect electrical conductor (PEC). :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted as containing a perfect magnetic conductor (PMC). :return: Sparse matrix containing the wave operator @@ -134,19 +145,24 @@ def h_full(omega: complex, mv = mu if numpy.any(numpy.equal(pmc, None)): + pm = sparse.eye(epsilon.size) + else: + pm = sparse.diags(numpy.where(pmc, 0, 1)) # Set pe to (not PMC) + mv = numpy.where(pmc, 1.0, mv) # Set mu to 1 at PMC + + if numpy.any(numpy.equal(pec, None)): pe = sparse.eye(epsilon.size) else: - pe = sparse.diags(numpy.where(pmc, 0, 1)) # Set pe to (not PMC) - mv = numpy.where(pmc, 1.0, mv) # Set mu to 1 at PMC + pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC) e_div = sparse.diags(1 / epsilon) m = sparse.diags(mv) - A = pe @ ec @ e_div @ hc @ pe - omega**2 * m + A = pm @ ec @ pe @ e_div @ hc @ pm - omega**2 * m return A -def eh_full(omega, dxes, epsilon, mu=None): +def eh_full(omega, dxes, epsilon, mu=None, pec=None, pmc=None): """ Wave operator for [E, H] field representation. This operator implements Maxwell's equations without cancelling out either E or H. The operator is @@ -159,18 +175,32 @@ def eh_full(omega, dxes, epsilon, mu=None): :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param epsilon: Vectorized dielectric constant :param mu: Vectorized magnetic permeability (default 1 everywhere) + :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted + as containing a perfect electrical conductor (PEC). + :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted + as containing a perfect magnetic conductor (PMC). :return: Sparse matrix containing the wave operator """ - A2 = curl_e(dxes) - A1 = curl_h(dxes) + if numpy.any(numpy.equal(pec, None)): + pe = sparse.eye(epsilon.size) + else: + pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC) - iwe = 1j * omega * sparse.diags(epsilon) - iwm = 1j * omega + if numpy.any(numpy.equal(pmc, None)): + pm = sparse.eye(epsilon.size) + else: + pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC) + + iwe = pe @ (1j * omega * sparse.diags(epsilon)) + iwm = pm * 1j * omega if not numpy.any(numpy.equal(mu, None)): iwm *= sparse.diags(mu) + A1 = pe @ curl_h(dxes) @ pm + A2 = pm @ curl_e(dxes) @ pe + A = sparse.bmat([[-iwe, A1], - [A2, +iwm]]) + [A2, iwm]]) return A From a3dac5c8f81a974d21a4e33cd7d7081962802aa6 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 16:55:51 -0700 Subject: [PATCH 016/437] Add e2h PMC arg, and clarify comments --- fdfd_tools/operators.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index 8045aaa..886338a 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -62,8 +62,10 @@ def e_full(omega: complex, :param mu: Vectorized magnetic permeability (default 1 everywhere). :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted as containing a perfect electrical conductor (PEC). + The PEC is applied per-field-component (ie, pec.size == epsilon.size) :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (ie, pmc.size == epsilon.size) :return: Sparse matrix containing the wave operator """ ce = curl_e(dxes) @@ -132,8 +134,10 @@ def h_full(omega: complex, :param mu: Vectorized magnetic permeability (default 1 everywhere) :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted as containing a perfect electrical conductor (PEC). + The PEC is applied per-field-component (ie, pec.size == epsilon.size) :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (ie, pmc.size == epsilon.size) :return: Sparse matrix containing the wave operator """ ec = curl_e(dxes) @@ -177,8 +181,10 @@ def eh_full(omega, dxes, epsilon, mu=None, pec=None, pmc=None): :param mu: Vectorized magnetic permeability (default 1 everywhere) :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted as containing a perfect electrical conductor (PEC). + The PEC is applied per-field-component (ie, pec.size == epsilon.size) :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (ie, pmc.size == epsilon.size) :return: Sparse matrix containing the wave operator """ if numpy.any(numpy.equal(pec, None)): @@ -227,6 +233,7 @@ def curl_e(dxes: dx_lists_t) -> sparse.spmatrix: def e2h(omega: complex, dxes: dx_lists_t, mu: vfield_t = None, + pmc: vfield_t = None, ) -> sparse.spmatrix: """ Utility operator for converting the E field into the H field. @@ -235,6 +242,9 @@ def e2h(omega: complex, :param omega: Angular frequency of the simulation :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param mu: Vectorized magnetic permeability (default 1 everywhere) + :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted + as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (ie, pmc.size == epsilon.size) :return: Sparse matrix for converting E to H """ op = curl_e(dxes) / (-1j * omega) @@ -242,6 +252,9 @@ def e2h(omega: complex, if not numpy.any(numpy.equal(mu, None)): op = sparse.diags(1 / mu) @ op + if not numpy.any(numpy.equal(pmc, None)): + op = sparse.diags(numpy.where(pmc, 0, 1)) @ op + return op From 8daab636ea1af85eb633551a72d5e6ae944c1471 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 3 Jul 2016 23:56:54 -0700 Subject: [PATCH 017/437] Cleaner conductor implementation --- examples/test.py | 2 +- fdfd_tools/operators.py | 38 +++++++++++++++++--------------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/examples/test.py b/examples/test.py index b41438a..098f797 100644 --- a/examples/test.py +++ b/examples/test.py @@ -178,7 +178,7 @@ def test1(): H_overlap = waveguide_mode.compute_overlap_e(**wg_args, **wg_results) pecg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) - # pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) + pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) # pecg.visualize_isosurface() pmcg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index 886338a..42d48de 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -71,25 +71,23 @@ def e_full(omega: complex, ce = curl_e(dxes) ch = curl_h(dxes) - ev = epsilon if numpy.any(numpy.equal(pec, None)): pe = sparse.eye(epsilon.size) else: pe = sparse.diags(numpy.where(pec, 0, 1)) # Set pe to (not PEC) - ev = numpy.where(pec, 1.0, ev) # Set epsilon to 1 at PEC if numpy.any(numpy.equal(pmc, None)): pm = sparse.eye(epsilon.size) else: - pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC) + pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC) - e = sparse.diags(ev) + e = sparse.diags(epsilon) if numpy.any(numpy.equal(mu, None)): m_div = sparse.eye(epsilon.size) else: m_div = sparse.diags(1 / mu) - op = pe @ ch @ pm @ m_div @ ce @ pe - omega**2 * e + op = pe @ (ch @ pm @ m_div @ ce - omega**2 * e) @ pe return op @@ -143,26 +141,23 @@ def h_full(omega: complex, ec = curl_e(dxes) hc = curl_h(dxes) - if mu is None: - mv = numpy.ones_like(epsilon) - else: - mv = mu - - if numpy.any(numpy.equal(pmc, None)): - pm = sparse.eye(epsilon.size) - else: - pm = sparse.diags(numpy.where(pmc, 0, 1)) # Set pe to (not PMC) - mv = numpy.where(pmc, 1.0, mv) # Set mu to 1 at PMC - if numpy.any(numpy.equal(pec, None)): pe = sparse.eye(epsilon.size) else: pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC) - e_div = sparse.diags(1 / epsilon) - m = sparse.diags(mv) + if numpy.any(numpy.equal(pmc, None)): + pm = sparse.eye(epsilon.size) + else: + pm = sparse.diags(numpy.where(pmc, 0, 1)) # Set pe to (not PMC) - A = pm @ ec @ pe @ e_div @ hc @ pm - omega**2 * m + e_div = sparse.diags(1 / epsilon) + if mu is None: + m = sparse.eye(epsilon.size) + else: + m = sparse.diags(mu) + + A = pm @ (ec @ pe @ e_div @ hc - omega**2 * m) @ pm return A @@ -197,10 +192,11 @@ def eh_full(omega, dxes, epsilon, mu=None, pec=None, pmc=None): else: pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC) - iwe = pe @ (1j * omega * sparse.diags(epsilon)) - iwm = pm * 1j * omega + iwe = pe @ (1j * omega * sparse.diags(epsilon)) @ pe + iwm = 1j * omega if not numpy.any(numpy.equal(mu, None)): iwm *= sparse.diags(mu) + iwm = pm @ iwm @ pm A1 = pe @ curl_h(dxes) @ pm A2 = pm @ curl_e(dxes) @ pe From eb4d9be6cfb57d2206bcaa5d80680aa0b813d316 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 4 Jul 2016 16:35:28 -0700 Subject: [PATCH 018/437] use opencl solver (for testing) --- examples/test.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/test.py b/examples/test.py index 098f797..c7b3db3 100644 --- a/examples/test.py +++ b/examples/test.py @@ -10,6 +10,8 @@ import gridlock from matplotlib import pyplot +from opencl_fdfd import cg_solver + __author__ = 'Jan Petykiewicz' @@ -185,16 +187,25 @@ def test1(): # pmcg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) # pmcg.visualize_isosurface() + ''' + Solve! + ''' A = fdfd_tools.operators.e_full(omega, dxes, epsilon=vec(grid.grids), pec=vec(pecg.grids), pmc=vec(pmcg.grids)).tocsr() b = -1j * omega * vec(J) - x = solve_A(A, b) + # x = solve_A(A, b) + + x = cg_solver(omega, dxes, J=vec(J), epsilon=vec(grid.grids), + pec=vec(pecg.grids), pmc=vec(pmcg.grids)) E = unvec(x, grid.shape) print('Norm of the residual is ', numpy.linalg.norm(A @ x - b)) + ''' + Plot results + ''' def pcolor(v): vmax = numpy.max(numpy.abs(v)) pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax) From 56df805e2496c093f6c9253f862ccf4c52593290 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 4 Jul 2016 23:53:54 -0700 Subject: [PATCH 019/437] add possibility to use csr opengl solver --- examples/test.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/examples/test.py b/examples/test.py index c7b3db3..eff2dd1 100644 --- a/examples/test.py +++ b/examples/test.py @@ -10,7 +10,7 @@ import gridlock from matplotlib import pyplot -from opencl_fdfd import cg_solver +from opencl_fdfd import cg_solver, csr __author__ = 'Jan Petykiewicz' @@ -180,7 +180,7 @@ def test1(): H_overlap = waveguide_mode.compute_overlap_e(**wg_args, **wg_results) pecg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) - pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) + # pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) # pecg.visualize_isosurface() pmcg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) @@ -190,15 +190,21 @@ def test1(): ''' Solve! ''' - A = fdfd_tools.operators.e_full(omega, dxes, - epsilon=vec(grid.grids), - pec=vec(pecg.grids), - pmc=vec(pmcg.grids)).tocsr() - b = -1j * omega * vec(J) - # x = solve_A(A, b) + sim_args = { + 'omega': omega, + 'dxes': dxes, + 'epsilon': vec(grid.grids), + 'pec': vec(pecg.grids), + 'pmc': vec(pmcg.grids), + } + + b = -1j * omega * vec(J) + A = fdfd_tools.operators.e_full(**sim_args).tocsr() +# x = solve_A(A, b) + +# x = csr.cg_solver(J=vec(J), **sim_args) + x = cg_solver(J=vec(J), **sim_args) - x = cg_solver(omega, dxes, J=vec(J), epsilon=vec(grid.grids), - pec=vec(pecg.grids), pmc=vec(pmcg.grids)) E = unvec(x, grid.shape) print('Norm of the residual is ', numpy.linalg.norm(A @ x - b)) @@ -211,7 +217,7 @@ def test1(): pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax) pyplot.axis('equal') pyplot.colorbar() - + center = grid.pos2ind([0, 0, 0], None).astype(int) pyplot.figure() pyplot.subplot(2, 2, 1) From c2d43b01df141d18ca25bdc091ab77f2ea6e7188 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 11 Jul 2016 14:54:18 -0700 Subject: [PATCH 020/437] move magma solver into different package --- examples/test.py | 60 ++---------------------------------------------- 1 file changed, 2 insertions(+), 58 deletions(-) diff --git a/examples/test.py b/examples/test.py index eff2dd1..09f425a 100644 --- a/examples/test.py +++ b/examples/test.py @@ -1,8 +1,4 @@ import numpy -from numpy.ctypeslib import ndpointer -import ctypes - -# h5py used by (uncalled) h5_write(); not used in currently-called code from fdfd_tools import vec, unvec, waveguide_mode import fdfd_tools, fdfd_tools.functional, fdfd_tools.grid @@ -10,64 +6,12 @@ import gridlock from matplotlib import pyplot +#import magma_fdfd from opencl_fdfd import cg_solver, csr __author__ = 'Jan Petykiewicz' -def complex_to_alternating(x: numpy.ndarray) -> numpy.ndarray: - stacked = numpy.vstack((numpy.real(x), numpy.imag(x))) - return stacked.T.astype(numpy.float64).flatten() - - -def solve_A(A, b: numpy.ndarray) -> numpy.ndarray: - A_vals = complex_to_alternating(A.data) - b_vals = complex_to_alternating(b) - x_vals = numpy.zeros_like(b_vals) - - args = ['dummy', - '--solver', 'QMR', - '--maxiter', '40000', - '--atol', '1e-6', - '--verbose', '100'] - argc = ctypes.c_int(len(args)) - argv_arr_t = ctypes.c_char_p * len(args) - argv_arr = argv_arr_t() - argv_arr[:] = [s.encode('ascii') for s in args] - - A_dim = ctypes.c_int(A.shape[0]) - A_nnz = ctypes.c_int(A.nnz) - npdouble = ndpointer(ctypes.c_double) - npint = ndpointer(ctypes.c_int) - - lib = ctypes.cdll.LoadLibrary('/home/jan/magma_solve/zsolve_shared.so') - c_solver = lib.zsolve - c_solver.argtypes = [ctypes.c_int, argv_arr_t, - ctypes.c_int, ctypes.c_int, - npdouble, npint, npint, npdouble, npdouble] - - c_solver(argc, argv_arr, A_dim, A_nnz, A_vals, - A.indptr.astype(numpy.intc), - A.indices.astype(numpy.intc), - b_vals, x_vals) - - x = (x_vals[::2] + 1j * x_vals[1::2]).flatten() - return x - - -def write_h5(filename, A, b): - import h5py - # dtype=np.dtype([('real', 'float64'), ('imag', 'float64')]) - h5py.get_config().complex_names = ('real', 'imag') - with h5py.File(filename, 'w') as mat_file: - mat_file.create_group('/A') - mat_file['/A/ir'] = A.indices.astype(numpy.intc) - mat_file['/A/jc'] = A.indptr.astype(numpy.intc) - mat_file['/A/data'] = A.data - mat_file['/b'] = b - mat_file['/x'] = numpy.zeros_like(b) - - def test0(): dx = 50 # discretization (nm/cell) pml_thickness = 10 # (number of cells) @@ -200,7 +144,7 @@ def test1(): b = -1j * omega * vec(J) A = fdfd_tools.operators.e_full(**sim_args).tocsr() -# x = solve_A(A, b) +# x = magma_fdfd.solve_A(A, b) # x = csr.cg_solver(J=vec(J), **sim_args) x = cg_solver(J=vec(J), **sim_args) From 85880c859e0bb9018149ea1e3cb1a950b5d20226 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 18 Jul 2016 21:10:02 -0700 Subject: [PATCH 021/437] Comment cleanup --- fdfd_tools/grid.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fdfd_tools/grid.py b/fdfd_tools/grid.py index 2faf928..8ecb44f 100644 --- a/fdfd_tools/grid.py +++ b/fdfd_tools/grid.py @@ -21,9 +21,9 @@ def prepare_s_function(ln_R: float = -16, :param ln_R: Natural logarithm of the desired reflectance :param m: Polynomial order for the PML (imaginary part increases as distance ** m) - :return: An s_function, which takes an ndarray (distances) and returns an ndarray (complex part of - the cell width; needs to be divided by sqrt(epilon_effective) * real(omega)) before - use. + :return: An s_function, which takes an ndarray (distances) and returns an ndarray (complex part + of the cell width; needs to be divided by sqrt(epilon_effective) * real(omega)) + before use. """ def s_factor(distance: numpy.ndarray) -> numpy.ndarray: s_max = (m + 1) * ln_R / 2 # / 2 because we assume periodic boundaries @@ -96,11 +96,11 @@ def stretch_with_scpml(dxes: dx_lists_t, :param axis: axis to stretch (0=x, 1=y, 2=z) :param polarity: direction to stretch (-1 for -ve, +1 for +ve) :param omega: Angular frequency for the simulation - :param epsilon_effective: Effective epsilon of the PML. Match this to the material at the edge of your grid. - Default 1. + :param epsilon_effective: Effective epsilon of the PML. Match this to the material at the + edge of your grid. Default 1. :param thickness: number of cells to use for pml (default 10) - :param s_function: s_function created by prepare_s_function(...), allowing customization of pml parameters. - Default uses prepare_s_function() with no parameters. + :param s_function: s_function created by prepare_s_function(...), allowing customization + of pml parameters. Default uses prepare_s_function() with no parameters. :return: Complex cell widths """ if s_function is None: From ec674fe3f4bd99a4f7f26e23456286b69fa0063b Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 4 Aug 2016 22:46:02 -0700 Subject: [PATCH 022/437] Add solvers submodule and clean up examples. Solvers submodule includes a generic solver in case you already have a sparse matrix solver, or in case you have no solver at all. Example file now uses alternate solvers if available, and has a nicer way of picking which solver gets used. --- examples/test.py | 53 +++++++++++++------ fdfd_tools/__init__.py | 2 +- fdfd_tools/solvers.py | 114 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 fdfd_tools/solvers.py diff --git a/examples/test.py b/examples/test.py index 09f425a..b71e859 100644 --- a/examples/test.py +++ b/examples/test.py @@ -1,18 +1,22 @@ +import importlib import numpy +from numpy.linalg import norm from fdfd_tools import vec, unvec, waveguide_mode -import fdfd_tools, fdfd_tools.functional, fdfd_tools.grid +import fdfd_tools +import fdfd_tools.functional +import fdfd_tools.grid +from fdfd_tools.solvers import generic as generic_solver + import gridlock from matplotlib import pyplot -#import magma_fdfd -from opencl_fdfd import cg_solver, csr __author__ = 'Jan Petykiewicz' -def test0(): +def test0(solver=generic_solver): dx = 50 # discretization (nm/cell) pml_thickness = 10 # (number of cells) @@ -59,21 +63,27 @@ def test0(): J = [numpy.zeros_like(grid.grids[0], dtype=complex) for _ in range(3)] J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1e5 + ''' + Solve! + ''' + x = solver(J=vec(J), **sim_args) + A = fdfd_tools.functional.e_full(omega, dxes, vec(grid.grids)).tocsr() b = -1j * omega * vec(J) + print('Norm of the residual is ', norm(A @ x - b)) - x = solve_A(A, b) E = unvec(x, grid.shape) - print('Norm of the residual is {}'.format(numpy.linalg.norm(A.dot(x) - b)/numpy.linalg.norm(b))) - + ''' + Plot results + ''' pyplot.figure() pyplot.pcolor(numpy.real(E[1][:, :, grid.shape[2]//2]), cmap='seismic') pyplot.axis('equal') pyplot.show() -def test1(): +def test1(solver=generic_solver): dx = 40 # discretization (nm/cell) pml_thickness = 10 # (number of cells) @@ -142,17 +152,14 @@ def test1(): 'pmc': vec(pmcg.grids), } + x = solver(J=vec(J), **sim_args) + b = -1j * omega * vec(J) A = fdfd_tools.operators.e_full(**sim_args).tocsr() -# x = magma_fdfd.solve_A(A, b) - -# x = csr.cg_solver(J=vec(J), **sim_args) - x = cg_solver(J=vec(J), **sim_args) + print('Norm of the residual is ', norm(A @ x - b)) E = unvec(x, grid.shape) - print('Norm of the residual is ', numpy.linalg.norm(A @ x - b)) - ''' Plot results ''' @@ -197,6 +204,22 @@ def test1(): pyplot.show() print('Average overlap with mode:', sum(q)/len(q)) + +def module_available(name): + return importlib.util.find_spec(name) is not None + + if __name__ == '__main__': # test0() - test1() + + if module_available('opencl_fdfd'): + from opencl_fdfd import cg_solver as opencl_solver + test1(opencl_solver) + # from opencl_fdfd.csr import fdfd_cg_solver as opencl_csr_solver + # test1(opencl_csr_solver) + # elif module_available('magma_fdfd'): + # from magma_fdfd import solver as magma_solver + # test1(magma_solver) + else: + test1() + diff --git a/fdfd_tools/__init__.py b/fdfd_tools/__init__.py index 4d75b2c..d19a4b9 100644 --- a/fdfd_tools/__init__.py +++ b/fdfd_tools/__init__.py @@ -22,4 +22,4 @@ Dependencies: from .vectorization import vec, unvec, field_t, vfield_t from .grid import dx_lists_t -__author__ = 'Jan Petykiewicz' \ No newline at end of file +__author__ = 'Jan Petykiewicz' diff --git a/fdfd_tools/solvers.py b/fdfd_tools/solvers.py new file mode 100644 index 0000000..3ee4080 --- /dev/null +++ b/fdfd_tools/solvers.py @@ -0,0 +1,114 @@ +""" +Solvers for FDFD problems. +""" + +from typing import List, Callable, Dict, Any + +import numpy +from numpy.linalg import norm +import scipy.sparse.linalg + +from . import operators + + +def _scipy_qmr(A: scipy.sparse.csr_matrix, + b: numpy.ndarray, + **kwargs + ) -> numpy.ndarray: + """ + Wrapper for scipy.sparse.linalg.qmr + + :param A: Sparse matrix + :param b: Right-hand-side vector + :param kwargs: Passed as **kwargs to the wrapped function + :return: Guess for solution (returned even if didn't converge) + """ + + ''' + Report on our progress + ''' + iter = 0 + + def print_residual(xk): + nonlocal iter + iter += 1 + if iter % 100 == 0: + print('Solver residual at iteration', iter, ':', norm(A @ xk - b)) + + if 'callback' in kwargs: + def augmented_callback(xk): + print_residual(xk) + kwargs['callback'](xk) + + kwargs['callback'] = augmented_callback + else: + kwargs['callback'] = print_residual + + ''' + Run the actual solve + ''' + + x, _ = scipy.sparse.linalg.qmr(A, b, **kwargs) + return x + + +def generic(omega: complex, + dxes: List[List[numpy.ndarray]], + J: numpy.ndarray, + epsilon: numpy.ndarray, + mu: numpy.ndarray = None, + pec: numpy.ndarray = None, + pmc: numpy.ndarray = None, + adjoint: bool = False, + matrix_solver: Callable[..., numpy.ndarray] = _scipy_qmr, + matrix_solver_opts: Dict[str, Any] = None, + ) -> numpy.ndarray: + """ + Conjugate gradient FDFD solver using CSR sparse matrices. + + All ndarray arguments should be 1D array, as returned by fdfd_tools.vec(). + + :param omega: Complex frequency to solve at. + :param dxes: [[dx_e, dy_e, dz_e], [dx_h, dy_h, dz_h]] (complex cell sizes) + :param J: Electric current distribution (at E-field locations) + :param epsilon: Dielectric constant distribution (at E-field locations) + :param mu: Magnetic permeability distribution (at H-field locations) + :param pec: Perfect electric conductor distribution + (at E-field locations; non-zero value indicates PEC is present) + :param pmc: Perfect magnetic conductor distribution + (at H-field locations; non-zero value indicates PMC is present) + :param adjoint: If true, solves the adjoint problem. + :param matrix_solver: Called as matrix_solver(A, b, **matrix_solver_opts) -> x + Where A: scipy.sparse.csr_matrix + b: numpy.ndarray + x: numpy.ndarray + Default is a wrapped version of scipy.sparse.linalg.qmr() + which doesn't return convergence info and prints the residual + every 100 iterations. + :param matrix_solver_opts: Passed as kwargs to matrix_solver(...) + :return: E-field which solves the system. + """ + + if matrix_solver_opts is None: + matrix_solver_opts = dict() + + b0 = -1j * omega * J + A0 = operators.e_full(omega, dxes, epsilon=epsilon, mu=mu, pec=pec, pmc=pmc) + + Pl, Pr = operators.e_full_preconditioners(dxes) + + if adjoint: + A = (Pl @ A0 @ Pr).H + b = Pr.H @ b0 + else: + A = Pl @ A0 @ Pr + b = Pl @ b0 + + x = matrix_solver(A.tocsr(), b, **matrix_solver_opts) + + if adjoint: + x0 = Pl.H @ x + else: + x0 = Pr @ x + + return x0 From e3a0846a148fd73d8e98a60f014c85f3512c1d01 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 4 Aug 2016 22:53:27 -0700 Subject: [PATCH 023/437] Remove extra space --- examples/test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/test.py b/examples/test.py index b71e859..a40309f 100644 --- a/examples/test.py +++ b/examples/test.py @@ -222,4 +222,3 @@ if __name__ == '__main__': # test1(magma_solver) else: test1() - From 068e0c7c935af2dc72eaf64a9a788be603c2ec17 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 4 Aug 2016 22:55:24 -0700 Subject: [PATCH 024/437] Update README with solver and example information. --- README.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ea4de31..1399887 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,17 @@ electromagnetic simulations. * Functional versions of most operators * Anisotropic media (eps_xx, eps_yy, eps_zz, mu_xx, ...) -This package does *not* provide a matrix solver. The waveguide mode solver -uses scipy's eigenvalue solver; I recommend a GPU-based iterative solver (eg. -those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html)). You will -need the ability to solve complex symmetric (non-Hermitian) linear systems, -ideally with double precision. +This package does *not* provide a fast matrix solver, though by default +```fdfd_tools.solvers.generic(...)``` will call +```scipy.sparse.linalg.qmr(...)``` to perform a solve. +For 2D problems this should be fine; likewise, the waveguide mode +solver uses scipy's eigenvalue solver, with reasonable results. + +For solving large (or 3D) problems, I recommend a GPU-based iterative +solver, such as [opencl_fdfd](https://mpxd.net/gogs/jan/opencl_fdfd) or +those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html)). Your +solver will need the ability to solve complex symmetric (non-Hermitian) +linear systems, ideally with double precision. ## Installation @@ -31,3 +37,9 @@ Install with pip, via git: ```bash pip install git+https://mpxd.net/gogs/jan/fdfd_tools.git@release ``` + +## Use + +See examples/test.py for some simple examples; you may need additional +packages such as [gridlock](https://mpxd.net/gogs/jan/gridlock) +to run the examples. From b8e9ec2b071d65e0a5e0a3b93016aaf3001e3e94 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 4 Aug 2016 22:55:46 -0700 Subject: [PATCH 025/437] Bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9294a9d..3da94ed 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup(name='fdfd_tools', - version='0.1', + version='0.2', description='FDFD Electromagnetic simulation tools', author='Jan Petykiewicz', author_email='anewusername@gmail.com', From 685de70af0686ff63dee99e3d5619ecb5b68193f Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 4 Aug 2016 23:05:11 -0700 Subject: [PATCH 026/437] Add PECs/PMCs to feature list --- README.md | 1 + fdfd_tools/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1399887..5a3f49c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ electromagnetic simulations. * Stretched-coordinate PML boundaries (SCPML) * Functional versions of most operators * Anisotropic media (eps_xx, eps_yy, eps_zz, mu_xx, ...) +* Arbitrary distributions of perfect electric and magnetic conductors (PEC / PMC) This package does *not* provide a fast matrix solver, though by default ```fdfd_tools.solvers.generic(...)``` will call diff --git a/fdfd_tools/__init__.py b/fdfd_tools/__init__.py index d19a4b9..a4efa89 100644 --- a/fdfd_tools/__init__.py +++ b/fdfd_tools/__init__.py @@ -3,7 +3,7 @@ Electromagnetic FDFD simulation tools Tools for 3D and 2D Electromagnetic Finite Difference Frequency Domain (FDFD) simulations. These tools handle conversion of fields to/from vector form, -creation of the wave operator matrix, stretched-coordinate PMLs, +creation of the wave operator matrix, stretched-coordinate PMLs, PECs and PMCs, field conversion operators, waveguide mode operator, and waveguide mode solver. From 6a56b081e4622b4d0359627d7f682ba0f25246f8 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 31 Oct 2016 18:42:51 -0700 Subject: [PATCH 027/437] add some missing type annotations --- fdfd_tools/operators.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index 42d48de..9f94e72 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -161,7 +161,13 @@ def h_full(omega: complex, return A -def eh_full(omega, dxes, epsilon, mu=None, pec=None, pmc=None): +def eh_full(omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + pec: vfield_t = None, + pmc: vfield_t = None + ) -> sparse.spmatrix: """ Wave operator for [E, H] field representation. This operator implements Maxwell's equations without cancelling out either E or H. The operator is @@ -256,7 +262,8 @@ def e2h(omega: complex, def m2j(omega: complex, dxes: dx_lists_t, - mu: vfield_t = None): + mu: vfield_t = None + ) -> sparse.spmatrix: """ Utility operator for converting M field into J. Converts a magnetic current M into an electric current J. From bb53ba44e03dd07afee2885bdc9a8468b300a549 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 31 Oct 2016 18:43:01 -0700 Subject: [PATCH 028/437] fix spacing --- fdfd_tools/solvers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fdfd_tools/solvers.py b/fdfd_tools/solvers.py index 3ee4080..bb230f2 100644 --- a/fdfd_tools/solvers.py +++ b/fdfd_tools/solvers.py @@ -12,9 +12,9 @@ from . import operators def _scipy_qmr(A: scipy.sparse.csr_matrix, - b: numpy.ndarray, - **kwargs - ) -> numpy.ndarray: + b: numpy.ndarray, + **kwargs + ) -> numpy.ndarray: """ Wrapper for scipy.sparse.linalg.qmr From 1e80a66b50eaaf7f92f44d56f58c0f7bf76d8f6e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 5 Mar 2017 17:20:38 -0800 Subject: [PATCH 029/437] add fdtd and test --- examples/test_fdtd.py | 175 +++++++++++++++++++++++++++++++ fdfd_tools/fdtd.py | 239 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 414 insertions(+) create mode 100644 examples/test_fdtd.py create mode 100644 fdfd_tools/fdtd.py diff --git a/examples/test_fdtd.py b/examples/test_fdtd.py new file mode 100644 index 0000000..1a25be4 --- /dev/null +++ b/examples/test_fdtd.py @@ -0,0 +1,175 @@ +""" +Example code for running an OpenCL FDTD simulation + +See main() for simulation setup. +""" + +import sys +import time + +import numpy +import h5py + +from fdfd_tools import fdtd +from masque import Pattern, shapes +import gridlock +import pcgen + + +def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern: + """ + Generate a masque.Pattern object containing a perturbed L3 cavity. + + :param a: Lattice constant. + :param radius: Hole radius, in units of a (lattice constant). + :param kwargs: Keyword arguments: + hole_dose, trench_dose, hole_layer, trench_layer: Shape properties for Pattern. + Defaults *_dose=1, hole_layer=0, trench_layer=1. + shifts_a, shifts_r: passed to pcgen.l3_shift; specifies lattice constant (1 - + multiplicative factor) and radius (multiplicative factor) for shifting + holes adjacent to the defect (same row). Defaults are 0.15 shift for + first hole, 0.075 shift for third hole, and no radius change. + xy_size: [x, y] number of mirror periods in each direction; total size is + 2 * n + 1 holes in each direction. Default [10, 10]. + perturbed_radius: radius of holes perturbed to form an upwards-driected beam + (multiplicative factor). Default 1.1. + trench width: Width of the undercut trenches. Default 1.2e3. + :return: masque.Pattern object containing the L3 design + """ + + default_args = {'hole_dose': 1, + 'trench_dose': 1, + 'hole_layer': 0, + 'trench_layer': 1, + 'shifts_a': (0.15, 0, 0.075), + 'shifts_r': (1.0, 1.0, 1.0), + 'xy_size': (10, 10), + 'perturbed_radius': 1.1, + 'trench_width': 1.2e3, + } + kwargs = {**default_args, **kwargs} + + xyr = pcgen.l3_shift_perturbed_defect(mirror_dims=kwargs['xy_size'], + perturbed_radius=kwargs['perturbed_radius'], + shifts_a=kwargs['shifts_a'], + shifts_r=kwargs['shifts_r']) + xyr *= a + xyr[:, 2] *= radius + + pat = Pattern() + pat.name = 'L3p-a{:g}r{:g}rp{:g}'.format(a, radius, kwargs['perturbed_radius']) + pat.shapes += [shapes.Circle(radius=r, offset=(x, y), + dose=kwargs['hole_dose'], + layer=kwargs['hole_layer']) + for x, y, r in xyr] + + maxes = numpy.max(numpy.fabs(xyr), axis=0) + pat.shapes += [shapes.Polygon.rectangle( + lx=(2 * maxes[0]), ly=kwargs['trench_width'], + offset=(0, s * (maxes[1] + a + kwargs['trench_width'] / 2)), + dose=kwargs['trench_dose'], layer=kwargs['trench_layer']) + for s in (-1, 1)] + return pat + + +def main(): + dtype = numpy.float32 + max_t = 8000 # number of timesteps + + dx = 40 # discretization (nm/cell) + pml_thickness = 8 # (number of cells) + + wl = 1550 # Excitation wavelength and fwhm + dwl = 200 + + # Device design parameters + xy_size = numpy.array([10, 10]) + a = 430 + r = 0.285 + th = 170 + + # refractive indices + n_slab = 3.408 # InGaAsP(80, 50) @ 1550nm + n_air = 1.0 # air + + # Half-dimensions of the simulation grid + xy_max = (xy_size + 1) * a * [1, numpy.sqrt(3)/2] + z_max = 1.6 * a + xyz_max = numpy.hstack((xy_max, z_max)) + pml_thickness * dx + + # Coordinates of the edges of the cells. The fdtd package can only do square grids at the moment. + half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max] + edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords] + + # #### Create the grid, mask, and draw the device #### + grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3) + grid.draw_slab(surface_normal=gridlock.Direction.z, + center=[0, 0, 0], + thickness=th, + eps=n_slab**2) + mask = perturbed_l3(a, r) + + grid.draw_polygons(surface_normal=gridlock.Direction.z, + center=[0, 0, 0], + thickness=2 * th, + eps=n_air**2, + polygons=mask.as_polygons()) + + print(grid.shape) + # #### Create the simulation grid #### + epsilon = [eps.astype(dtype) for eps in grid.grids] + + dt = .99/numpy.sqrt(3) + e = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)] + h = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)] + + update_e = fdtd.maxwell_e(dt) + update_h = fdtd.maxwell_h(dt) + + # PMLs in every direction + pml_e_funcs = [] + pml_h_funcs = [] + pml_fields = {} + for d in (0, 1, 2): + for p in (-1, 1): + ef, hf, psis = fdtd.cpml(direction=d, polarity=p, dt=dt, epsilon=epsilon, epsilon_eff=n_slab**2, dtype=dtype) + pml_e_funcs.append(ef) + pml_h_funcs.append(hf) + pml_fields.update(psis) + + # Source parameters and function + w = 2 * numpy.pi * dx / wl + fwhm = dwl * w * w / (2 * numpy.pi * dx) + alpha = (fwhm ** 2) / 8 * numpy.log(2) + delay = 7/numpy.sqrt(2 * alpha) + + def field_source(i): + t0 = i * dt - delay + return numpy.sin(w * t0) * numpy.exp(-alpha * t0**2) + + # #### Run a bunch of iterations #### + output_file = h5py.File('simulation_output.h5', 'w') + start = time.perf_counter() + for t in range(max_t): + [f(e, h, epsilon) for f in pml_e_funcs] + update_e(e, h, epsilon) + + e[1][tuple(grid.shape//2)] += field_source(t) + [f(e, h) for f in pml_h_funcs] + update_h(e, h) + + print('iteration {}: average {} iterations per sec'.format(t, (t+1)/(time.perf_counter()-start))) + sys.stdout.flush() + + if t % 20 == 0: + r = sum([(f * f * e).sum() for f, e in zip(e, epsilon)]) + print('E sum', r) + + # Save field slices + if (t % 20 == 0 and (max_t - t <= 1000 or t <= 2000)) or t == max_t-1: + print('saving E-field') + for j, f in enumerate(e): + output_file['/E{}_t{}'.format('xyz'[j], t)] = f[:, :, round(f.shape[2]/2)] + +if __name__ == '__main__': + main() diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py new file mode 100644 index 0000000..c4aa897 --- /dev/null +++ b/fdfd_tools/fdtd.py @@ -0,0 +1,239 @@ +from typing import List, Callable, Tuple, Dict +import numpy + +from . import dx_lists_t, field_t + +__author__ = 'Jan Petykiewicz' + + +functional_matrix = Callable[[field_t], field_t] + + +def curl_h(dxes: dx_lists_t = None) -> functional_matrix: + """ + Curl operator for use with the H field. + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Function for taking the discretized curl of the H-field, F(H) -> curlH + """ + if dxes: + dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') + + def dh(f, ax): + return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] + else: + def dh(f, ax): + return f - numpy.roll(f, 1, axis=ax) + + def ch_fun(h: field_t) -> field_t: + e = [dh(h[2], 1) - dh(h[1], 2), + dh(h[0], 2) - dh(h[2], 0), + dh(h[1], 0) - dh(h[0], 1)] + return e + + return ch_fun + + +def curl_e(dxes: dx_lists_t = None) -> functional_matrix: + """ + Curl operator for use with the E field. + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Function for taking the discretized curl of the E-field, F(E) -> curlE + """ + if dxes is not None: + dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') + + def de(f, ax): + return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] + else: + def de(f, ax): + return numpy.roll(f, -1, axis=ax) - f + + def ce_fun(e: field_t) -> field_t: + h = [de(e[2], 1) - de(e[1], 2), + de(e[0], 2) - de(e[2], 0), + de(e[1], 0) - de(e[0], 1)] + return h + + return ce_fun + + +def maxwell_e(dt: float, dxes: dx_lists_t = None) -> functional_matrix: + curl_h_fun = curl_h(dxes) + + def me_fun(e: field_t, h: field_t, epsilon: field_t): + ch = curl_h_fun(h) + for ei, ci, epsi in zip(e, ch, epsilon): + ei += dt * ci / epsi + return e + + return me_fun + + +def maxwell_h(dt: float, dxes: dx_lists_t = None) -> functional_matrix: + curl_e_fun = curl_e(dxes) + + def mh_fun(e: field_t, h: field_t): + ce = curl_e_fun(e) + for hi, ci in zip(h, ce): + hi -= dt * ci + return h + + return mh_fun + + +def conducting_boundary(direction: int, + polarity: int + ) -> Tuple[functional_matrix, functional_matrix]: + dirs = [0, 1, 2] + if direction not in dirs: + raise Exception('Invalid direction: {}'.format(direction)) + dirs.remove(direction) + u, v = dirs + + if polarity < 0: + boundary_slice = [slice(None)] * 3 + shifted1_slice = [slice(None)] * 3 + boundary_slice[direction] = 0 + shifted1_slice[direction] = 1 + + def en(e: field_t): + e[direction][boundary_slice] = 0 + e[u][boundary_slice] = e[u][shifted1_slice] + e[v][boundary_slice] = e[v][shifted1_slice] + return e + + def hn(h: field_t): + h[direction][boundary_slice] = h[direction][shifted1_slice] + h[u][boundary_slice] = 0 + h[v][boundary_slice] = 0 + return h + + return en, hn + + elif polarity > 0: + boundary_slice = [slice(None)] * 3 + shifted1_slice = [slice(None)] * 3 + shifted2_slice = [slice(None)] * 3 + boundary_slice[direction] = -1 + shifted1_slice[direction] = -2 + shifted2_slice[direction] = -3 + + def ep(e: field_t): + e[direction][boundary_slice] = -e[direction][shifted2_slice] + e[direction][shifted1_slice] = 0 + e[u][boundary_slice] = e[u][shifted1_slice] + e[v][boundary_slice] = e[v][shifted1_slice] + return e + + def hp(h: field_t): + h[direction][boundary_slice] = h[direction][shifted1_slice] + h[u][boundary_slice] = -h[u][shifted2_slice] + h[u][shifted1_slice] = 0 + h[v][boundary_slice] = -h[v][shifted2_slice] + h[v][shifted1_slice] = 0 + return h + + return ep, hp + + else: + raise Exception('Bad polarity: {}'.format(polarity)) + + +def cpml(direction:int, + polarity: int, + dt: float, + epsilon: field_t, + thickness: int = 8, + epsilon_eff: float = 1, + dtype: numpy.dtype = numpy.float32, + ) -> Tuple[Callable, Callable, Dict[str, field_t]]: + + if direction not in range(3): + raise Exception('Invalid direction: {}'.format(direction)) + + if polarity not in (-1, 1): + raise Exception('Invalid polarity: {}'.format(polarity)) + + if thickness <= 2: + raise Exception('It would be wise to have a pml with 4+ cells of thickness') + + if epsilon_eff <= 0: + raise Exception('epsilon_eff must be positive') + + m = (3.5, 1) + sigma_max = 0.8 * (m[0] + 1) / numpy.sqrt(epsilon_eff) + alpha_max = 0 # TODO: Decide what to do about non-zero alpha + transverse = numpy.delete(range(3), direction) + u, v = transverse + + xe = numpy.arange(1, thickness+1, dtype=float) + xh = numpy.arange(1, thickness+1, dtype=float) + if polarity > 0: + xe -= 0.5 + elif polarity < 0: + xh -= 0.5 + xe = xe[::-1] + xh = xh[::-1] + else: + raise Exception('Bad polarity!') + + expand_slice = [None] * 3 + expand_slice[direction] = slice(None) + + def par(x): + sigma = ((x / thickness) ** m[0]) * sigma_max + alpha = ((1 - x / thickness) ** m[1]) * alpha_max + p0 = numpy.exp(-(sigma + alpha) * dt) + p1 = sigma / (sigma + alpha) * (p0 - 1) + return p0[expand_slice], p1[expand_slice] + + p0e, p1e = par(xe) + p0h, p1h = par(xh) + + region = [slice(None)] * 3 + if polarity < 0: + region[direction] = slice(None, thickness) + elif polarity > 0: + region[direction] = slice(-thickness, None) + else: + raise Exception('Bad polarity!') + + if direction == 1: + se = 1 + else: + se = -1 + + # TODO check if epsilon is uniform? + shape = list(epsilon[0].shape) + shape[direction] = thickness + psi_e = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] + psi_h = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] + + fields = { + 'psi_e_u': psi_e[0], + 'psi_e_v': psi_e[1], + 'psi_h_u': psi_h[0], + 'psi_h_v': psi_h[1], + } + + def pml_e(e: field_t, h: field_t, epsilon: field_t) -> Tuple[field_t, field_t]: + psi_e[0] *= p0e + psi_e[0] += p1e * (h[v][region] - numpy.roll(h[v], 1, axis=direction)[region]) + psi_e[1] *= p0e + psi_e[1] += p1e * (h[u][region] - numpy.roll(h[u], 1, axis=direction)[region]) + e[u][region] += se * dt * psi_e[0] / epsilon[u][region] + e[v][region] -= se * dt * psi_e[1] / epsilon[v][region] + return e, h + + def pml_h(e: field_t, h: field_t) -> Tuple[field_t, field_t]: + psi_h[0] *= p0h + psi_h[0] += p1h * (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region]) + psi_h[1] *= p0h + psi_h[1] += p1h * (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region]) + h[u][region] -= se * dt * psi_h[0] + h[v][region] += se * dt * psi_h[1] + return e, h + + return pml_e, pml_h, fields From 48ddd9f512db405d677995f94ea3128ddd2d9016 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 26 Mar 2017 18:22:12 -0700 Subject: [PATCH 030/437] Switch to C-ordered arrays --- fdfd_tools/operators.py | 20 +++++++++----------- fdfd_tools/vectorization.py | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index 9f94e72..1691f36 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -302,11 +302,9 @@ def rotation(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmat n = numpy.prod(shape) i_ind = numpy.arange(n) - j_ind = ijk[0] + ijk[1] * shape[0] - if len(shape) == 3: - j_ind += ijk[2] * shape[0] * shape[1] + j_ind = numpy.ravel_multi_index(ijk, shape, order='C') - vij = (numpy.ones(n), (i_ind, j_ind.flatten(order='F'))) + vij = (numpy.ones(n), (i_ind, j_ind.flatten(order='C'))) d = sparse.csr_matrix(vij, shape=(n, n)) @@ -350,7 +348,7 @@ def shift_with_mirror(axis: int, shape: List[int], shift_distance: int=1) -> spa if len(shape) == 3: j_ind += ijk[2] * shape[0] * shape[1] - vij = (numpy.ones(n), (i_ind, j_ind.flatten(order='F'))) + vij = (numpy.ones(n), (i_ind, j_ind.flatten(order='C'))) d = sparse.csr_matrix(vij, shape=(n, n)) return d @@ -371,7 +369,7 @@ def deriv_forward(dx_e: List[numpy.ndarray]) -> List[sparse.spmatrix]: def deriv(axis): return rotation(axis, shape, 1) - sparse.eye(n) - Ds = [sparse.diags(+1 / dx.flatten(order='F')) @ deriv(a) + Ds = [sparse.diags(+1 / dx.flatten(order='C')) @ deriv(a) for a, dx in enumerate(dx_e_expanded)] return Ds @@ -392,7 +390,7 @@ def deriv_back(dx_h: List[numpy.ndarray]) -> List[sparse.spmatrix]: def deriv(axis): return rotation(axis, shape, -1) - sparse.eye(n) - Ds = [sparse.diags(-1 / dx.flatten(order='F')) @ deriv(a) + Ds = [sparse.diags(-1 / dx.flatten(order='C')) @ deriv(a) for a, dx in enumerate(dx_h_expanded)] return Ds @@ -463,8 +461,8 @@ def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: fx, fy, fz = [avgf(i, shape) for i in range(3)] bx, by, bz = [avgb(i, shape) for i in range(3)] - dxag = [dx.flatten(order='F') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] - dbgx, dbgy, dbgz = [sparse.diags(dx.flatten(order='F')) + dxag = [dx.flatten(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] + dbgx, dbgy, dbgz = [sparse.diags(dx.flatten(order='C')) for dx in numpy.meshgrid(*dxes[1], indexing='ij')] Ex, Ey, Ez = [sparse.diags(ei * da) for ei, da in zip(numpy.split(e, 3), dxag)] @@ -492,8 +490,8 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: fx, fy, fz = [avgf(i, shape) for i in range(3)] bx, by, bz = [avgb(i, shape) for i in range(3)] - dxbg = [dx.flatten(order='F') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] - dagx, dagy, dagz = [sparse.diags(dx.flatten(order='F')) + dxbg = [dx.flatten(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] + dagx, dagy, dagz = [sparse.diags(dx.flatten(order='C')) for dx in numpy.meshgrid(*dxes[0], indexing='ij')] Hx, Hy, Hz = [sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg)] diff --git a/fdfd_tools/vectorization.py b/fdfd_tools/vectorization.py index dfa48f1..2377d39 100644 --- a/fdfd_tools/vectorization.py +++ b/fdfd_tools/vectorization.py @@ -27,7 +27,7 @@ def vec(f: field_t) -> vfield_t: """ if numpy.any(numpy.equal(f, None)): return None - return numpy.hstack(tuple((fi.flatten(order='F') for fi in f))) + return numpy.hstack(tuple((fi.flatten(order='C') for fi in f))) def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: @@ -45,5 +45,5 @@ def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: """ if numpy.any(numpy.equal(v, None)): return None - return [vi.reshape(shape, order='F') for vi in numpy.split(v, 3)] + return [vi.reshape(shape, order='C') for vi in numpy.split(v, 3)] From 7cbbaedcdb6ceea0352bf5be3a0267119810b899 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 19 Apr 2017 23:18:09 -0700 Subject: [PATCH 031/437] bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3da94ed..4a3441a 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup(name='fdfd_tools', - version='0.2', + version='0.3', description='FDFD Electromagnetic simulation tools', author='Jan Petykiewicz', author_email='anewusername@gmail.com', From 43d14642582151e726665d4f8d36dba276e4add9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 May 2017 21:21:50 -0700 Subject: [PATCH 032/437] Remote pyplot.hold It's deprecated now --- examples/test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/test.py b/examples/test.py index a40309f..a7e1746 100644 --- a/examples/test.py +++ b/examples/test.py @@ -190,7 +190,6 @@ def test1(solver=generic_solver): s1x, s2x = poyntings(E) pyplot.plot(s1x[0].sum(axis=2).sum(axis=1)) - pyplot.hold(True) pyplot.plot(s2x[0].sum(axis=2).sum(axis=1)) pyplot.show() From 50334723429e94660a7835884aa97d923f430a5d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 May 2017 21:22:28 -0700 Subject: [PATCH 033/437] Use ravel instead of flatten where possible --- fdfd_tools/operators.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index 1691f36..c7fc578 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -304,7 +304,7 @@ def rotation(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmat i_ind = numpy.arange(n) j_ind = numpy.ravel_multi_index(ijk, shape, order='C') - vij = (numpy.ones(n), (i_ind, j_ind.flatten(order='C'))) + vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) d = sparse.csr_matrix(vij, shape=(n, n)) @@ -348,7 +348,7 @@ def shift_with_mirror(axis: int, shape: List[int], shift_distance: int=1) -> spa if len(shape) == 3: j_ind += ijk[2] * shape[0] * shape[1] - vij = (numpy.ones(n), (i_ind, j_ind.flatten(order='C'))) + vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) d = sparse.csr_matrix(vij, shape=(n, n)) return d @@ -369,7 +369,7 @@ def deriv_forward(dx_e: List[numpy.ndarray]) -> List[sparse.spmatrix]: def deriv(axis): return rotation(axis, shape, 1) - sparse.eye(n) - Ds = [sparse.diags(+1 / dx.flatten(order='C')) @ deriv(a) + Ds = [sparse.diags(+1 / dx.ravel(order='C')) @ deriv(a) for a, dx in enumerate(dx_e_expanded)] return Ds @@ -390,7 +390,7 @@ def deriv_back(dx_h: List[numpy.ndarray]) -> List[sparse.spmatrix]: def deriv(axis): return rotation(axis, shape, -1) - sparse.eye(n) - Ds = [sparse.diags(-1 / dx.flatten(order='C')) @ deriv(a) + Ds = [sparse.diags(-1 / dx.ravel(order='C')) @ deriv(a) for a, dx in enumerate(dx_h_expanded)] return Ds @@ -461,8 +461,8 @@ def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: fx, fy, fz = [avgf(i, shape) for i in range(3)] bx, by, bz = [avgb(i, shape) for i in range(3)] - dxag = [dx.flatten(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] - dbgx, dbgy, dbgz = [sparse.diags(dx.flatten(order='C')) + dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] + dbgx, dbgy, dbgz = [sparse.diags(dx.ravel(order='C')) for dx in numpy.meshgrid(*dxes[1], indexing='ij')] Ex, Ey, Ez = [sparse.diags(ei * da) for ei, da in zip(numpy.split(e, 3), dxag)] @@ -490,8 +490,8 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: fx, fy, fz = [avgf(i, shape) for i in range(3)] bx, by, bz = [avgb(i, shape) for i in range(3)] - dxbg = [dx.flatten(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] - dagx, dagy, dagz = [sparse.diags(dx.flatten(order='C')) + dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] + dagx, dagy, dagz = [sparse.diags(dx.ravel(order='C')) for dx in numpy.meshgrid(*dxes[0], indexing='ij')] Hx, Hy, Hz = [sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg)] From 9d337444276cd7b2367fc313b39e572bfbdd5b28 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 May 2017 21:22:43 -0700 Subject: [PATCH 034/437] Fix docstring for rotation --- fdfd_tools/operators.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index c7fc578..bfcfcdc 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -284,7 +284,8 @@ def m2j(omega: complex, def rotation(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmatrix: """ - Utility operator for performing a circular shift along a specified axis by 1 element. + Utility operator for performing a circular shift along a specified axis by a + specified number of elements. :param axis: Axis to shift along. x=0, y=1, z=2 :param shape: Shape of the grid being shifted From 6748181f8ff8d43e9792b33287b670bee07786cb Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 May 2017 21:23:18 -0700 Subject: [PATCH 035/437] use logging module for progress reports --- fdfd_tools/solvers.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/fdfd_tools/solvers.py b/fdfd_tools/solvers.py index bb230f2..066725c 100644 --- a/fdfd_tools/solvers.py +++ b/fdfd_tools/solvers.py @@ -3,6 +3,7 @@ Solvers for FDFD problems. """ from typing import List, Callable, Dict, Any +import logging import numpy from numpy.linalg import norm @@ -11,6 +12,9 @@ import scipy.sparse.linalg from . import operators +logger = logging.getLogger(__name__) + + def _scipy_qmr(A: scipy.sparse.csr_matrix, b: numpy.ndarray, **kwargs @@ -29,20 +33,20 @@ def _scipy_qmr(A: scipy.sparse.csr_matrix, ''' iter = 0 - def print_residual(xk): + def log_residual(xk): nonlocal iter iter += 1 if iter % 100 == 0: - print('Solver residual at iteration', iter, ':', norm(A @ xk - b)) + logger.info('Solver residual at iteration {} : {}'.format(iter, norm(A @ xk - b))) if 'callback' in kwargs: def augmented_callback(xk): - print_residual(xk) + log_residual(xk) kwargs['callback'](xk) kwargs['callback'] = augmented_callback else: - kwargs['callback'] = print_residual + kwargs['callback'] = log_residual ''' Run the actual solve @@ -83,7 +87,7 @@ def generic(omega: complex, b: numpy.ndarray x: numpy.ndarray Default is a wrapped version of scipy.sparse.linalg.qmr() - which doesn't return convergence info and prints the residual + which doesn't return convergence info and logs the residual every 100 iterations. :param matrix_solver_opts: Passed as kwargs to matrix_solver(...) :return: E-field which solves the system. From c14298484db30b20a02e9aaca50b2f0767ce9c17 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 24 Sep 2017 19:11:56 -0700 Subject: [PATCH 036/437] Fix eigenvalue solver for complex matrices --- fdfd_tools/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index df62143..51a6266 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -64,7 +64,7 @@ def solve_waveguide_mode_2d(mode_number: int, eigval = None for _ in range(40): - eigval = v @ A @ v + eigval = v.conj() @ A @ v if numpy.linalg.norm(A @ v - eigval * v) < 1e-13: break w = spalg.spsolve(A - eigval * sparse.eye(A.shape[0]), v) From d3c22006bdd4f7bb51a06d0782ea1ac4513c8833 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 24 Sep 2017 19:12:48 -0700 Subject: [PATCH 037/437] ie -> i.e. (docs) --- fdfd_tools/operators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index bfcfcdc..02c1197 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -182,10 +182,10 @@ def eh_full(omega: complex, :param mu: Vectorized magnetic permeability (default 1 everywhere) :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted as containing a perfect electrical conductor (PEC). - The PEC is applied per-field-component (ie, pec.size == epsilon.size) + The PEC is applied per-field-component (i.e., pec.size == epsilon.size) :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted as containing a perfect magnetic conductor (PMC). - The PMC is applied per-field-component (ie, pmc.size == epsilon.size) + The PMC is applied per-field-component (i.e., pmc.size == epsilon.size) :return: Sparse matrix containing the wave operator """ if numpy.any(numpy.equal(pec, None)): From 7342c8efd7b246ae6c3182b5970dcddb0e86417f Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 24 Sep 2017 19:13:10 -0700 Subject: [PATCH 038/437] Use ravel instead of flatten for vec() --- fdfd_tools/vectorization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/vectorization.py b/fdfd_tools/vectorization.py index 2377d39..1c2134a 100644 --- a/fdfd_tools/vectorization.py +++ b/fdfd_tools/vectorization.py @@ -27,7 +27,7 @@ def vec(f: field_t) -> vfield_t: """ if numpy.any(numpy.equal(f, None)): return None - return numpy.hstack(tuple((fi.flatten(order='C') for fi in f))) + return numpy.hstack(tuple((fi.ravel(order='C') for fi in f))) def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: From 17fa2aa3d370635a1bb8b9c713b7b93979c46ba3 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 24 Sep 2017 19:13:37 -0700 Subject: [PATCH 039/437] In-place normalization during eigensolve --- fdfd_tools/waveguide_mode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 51a6266..fac43e2 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -67,8 +67,8 @@ def solve_waveguide_mode_2d(mode_number: int, eigval = v.conj() @ A @ v if numpy.linalg.norm(A @ v - eigval * v) < 1e-13: break - w = spalg.spsolve(A - eigval * sparse.eye(A.shape[0]), v) - v = w / numpy.linalg.norm(w) + v = spalg.spsolve(A - eigval * sparse.eye(A.shape[0]), v) + v /= numpy.linalg.norm(v) # Calculate the wave-vector (force the real part to be positive) wavenumber = numpy.sqrt(eigval) From 001bf1e2eff28702084aa439b9b9a9fd648ef524 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 24 Sep 2017 19:14:30 -0700 Subject: [PATCH 040/437] Clarify eigensolver documentation --- fdfd_tools/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index fac43e2..c6b9900 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -45,7 +45,7 @@ def solve_waveguide_mode_2d(mode_number: int, ''' Shift by the absolute value of the largest eigenvalue, then find a few of the - largest (shifted) eigenvalues. The shift ensures that we find the largest + largest-magnitude (shifted) eigenvalues. The shift ensures that we find the largest _positive_ eigenvalues, since any negative eigenvalues will be shifted to the range 0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval) ''' From bacc6fea3f570f87046de80e3003908c52fd4183 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 24 Sep 2017 22:28:08 -0700 Subject: [PATCH 041/437] Move eigensolver code out to separate module --- fdfd_tools/eigensolvers.py | 95 ++++++++++++++++++++++++++++++++++++ fdfd_tools/waveguide.py | 2 +- fdfd_tools/waveguide_mode.py | 39 +++------------ 3 files changed, 102 insertions(+), 34 deletions(-) create mode 100644 fdfd_tools/eigensolvers.py diff --git a/fdfd_tools/eigensolvers.py b/fdfd_tools/eigensolvers.py new file mode 100644 index 0000000..6c06296 --- /dev/null +++ b/fdfd_tools/eigensolvers.py @@ -0,0 +1,95 @@ +""" +Solvers for eigenvalue / eigenvector problems +""" +from typing import Tuple, List +import numpy +from numpy.linalg import norm +from scipy import sparse +import scipy.sparse.linalg as spalg + + +def power_iteration(operator: sparse.spmatrix, + guess_vector: numpy.ndarray = None, + iterations: int = 20, + ) -> Tuple[complex, numpy.ndarray]: + """ + Use power iteration to estimate the dominant eigenvector of a matrix. + + :param operator: Matrix to analyze. + :param guess_vector: Starting point for the eigenvector. Default is a randomly chosen vector. + :param iterations: Number of iterations to perform. Default 20. + :return: (Largest-magnitude eigenvalue, Corresponding eigenvector estimate) + """ + if numpy.any(numpy.equal(guess_vector, None)): + v = numpy.random.rand(operator.shape[0]) + else: + v = guess_vector + + for _ in range(iterations): + v = operator @ v + v /= norm(v) + + lm_eigval = v.conj() @ operator @ v + return lm_eigval, v + + +def rayleigh_quotient_iteration(operator: sparse.spmatrix, + guess_vector: numpy.ndarray, + iterations: int = 40, + tolerance: float = 1e-13, + ) -> Tuple[complex, numpy.ndarray]: + """ + Use Rayleigh quotient iteration to refine an eigenvector guess. + + :param operator: Matrix to analyze. + :param guess_vector: Eigenvector to refine. + :param iterations: Maximum number of iterations to perform. Default 40. + :param tolerance: Stop iteration if (A - I*eigenvalue) @ v < tolerance. + Default 1e-13. + :return: (eigenvalue, eigenvector) + """ + v = guess_vector + for _ in range(iterations): + eigval = v.conj() @ operator @ v + if norm(operator @ v - eigval * v) < tolerance: + break + v = spalg.spsolve(operator - eigval * sparse.eye(operator.shape[0]), v) + v /= norm(v) + + return eigval, v + + +def signed_eigensolve(operator: sparse.spmatrix, + how_many: int, + negative: bool = False, + ) -> Tuple[numpy.ndarray, numpy.ndarray]: + """ + Find the largest-magnitude positive-only (or negative-only) eigenvalues and + eigenvectors of the provided matrix. + + :param operator: Matrix to analyze. + :param how_many: How many eigenvalues to find. + :param negative: Whether to find negative-only eigenvalues. + Default False (positive only). + :return: (sorted list of eigenvalues, 2D ndarray of corresponding eigenvectors) + eigenvectors[:, k] corresponds to the k-th eigenvalue + """ + # Use power iteration to estimate the dominant eigenvector + lm_eigval, _ = power_iteration(operator) + + ''' + Shift by the absolute value of the largest eigenvalue, then find a few of the + largest-magnitude (shifted) eigenvalues. A positive shift ensures that we find the + largest _positive_ eigenvalues, since any negative eigenvalues will be shifted to the + range 0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval) + ''' + if negative: + shifted_operator = operator - abs(lm_eigval) * sparse.eye(operator.shape[0]) + else: + shifted_operator = operator + abs(lm_eigval) * sparse.eye(operator.shape[0]) + + eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=50) + + k = eigenvalues.argsort() + return eigenvalues[k], eigenvectors[:, k] + diff --git a/fdfd_tools/waveguide.py b/fdfd_tools/waveguide.py index a8ae1f2..0725bac 100644 --- a/fdfd_tools/waveguide.py +++ b/fdfd_tools/waveguide.py @@ -23,7 +23,7 @@ import numpy from numpy.linalg import norm import scipy.sparse as sparse -from . import unvec, dx_lists_t, field_t, vfield_t +from . import vec, unvec, dx_lists_t, field_t, vfield_t from . import operators diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index c6b9900..1670991 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -1,10 +1,10 @@ from typing import Dict, List import numpy import scipy.sparse as sparse -import scipy.sparse.linalg as spalg from . import vec, unvec, dx_lists_t, vfield_t, field_t from . import operators, waveguide, functional +from .eigensolvers import signed_eigensolve, rayleigh_quotient_iteration def solve_waveguide_mode_2d(mode_number: int, @@ -12,12 +12,12 @@ def solve_waveguide_mode_2d(mode_number: int, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, - wavenumber_correction: bool = True + wavenumber_correction: bool = True, ) -> Dict[str, complex or field_t]: """ Given a 2d region, attempts to solve for the eigenmode with the specified mode number. - :param mode_number: Number of the mode, 0-indexed + :param mode_number: Number of the mode, 0-indexed. :param omega: Angular frequency of the simulation :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param epsilon: Dielectric constant @@ -29,46 +29,19 @@ def solve_waveguide_mode_2d(mode_number: int, ''' Solve for the largest-magnitude eigenvalue of the real operator - by using power iteration. ''' dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] - A_r = waveguide.operator(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) - # Use power iteration for 20 steps to estimate the dominant eigenvector - v = numpy.random.rand(A_r.shape[0]) - for _ in range(20): - v = A_r @ v - v /= numpy.linalg.norm(v) - - lm_eigval = v @ A_r @ v - - ''' - Shift by the absolute value of the largest eigenvalue, then find a few of the - largest-magnitude (shifted) eigenvalues. The shift ensures that we find the largest - _positive_ eigenvalues, since any negative eigenvalues will be shifted to the range - 0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval) - ''' - shifted_A_r = A_r + abs(lm_eigval) * sparse.eye(A_r.shape[0]) - eigvals, eigvecs = spalg.eigs(shifted_A_r, which='LM', k=mode_number + 3, ncv=50) - - # Pick the eigenvalue we want from the few we found - k = eigvals.argsort()[-(mode_number+1)] - v = eigvecs[:, k] + eigvals, eigvecs = signed_eigensolve(A_r, mode_number+3) + v = eigvecs[:, -(mode_number + 1)] ''' Now solve for the eigenvector of the full operator, using the real operator's eigenvector as an initial guess for Rayleigh quotient iteration. ''' A = waveguide.operator(omega, dxes, epsilon, mu) - - eigval = None - for _ in range(40): - eigval = v.conj() @ A @ v - if numpy.linalg.norm(A @ v - eigval * v) < 1e-13: - break - v = spalg.spsolve(A - eigval * sparse.eye(A.shape[0]), v) - v /= numpy.linalg.norm(v) + v, eigval = rayleigh_quotient_iteration(A, v) # Calculate the wave-vector (force the real part to be positive) wavenumber = numpy.sqrt(eigval) From a4616982ca5d986a8a12e00a86f7e87e686a20ee Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 24 Sep 2017 22:28:39 -0700 Subject: [PATCH 042/437] Add cylindrical coordinate 2D modesolver code --- fdfd_tools/waveguide.py | 59 ++++++++++++++++++++++++++++++++ fdfd_tools/waveguide_mode.py | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/fdfd_tools/waveguide.py b/fdfd_tools/waveguide.py index 0725bac..85e96bf 100644 --- a/fdfd_tools/waveguide.py +++ b/fdfd_tools/waveguide.py @@ -307,3 +307,62 @@ def e_err(e: vfield_t, op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e) return norm(op) / norm(e) + + +def cylindrical_operator(omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + r0: float, + ) -> sparse.spmatrix: + """ + Cylindrical coordinate waveguide operator of the form + + TODO + + for use with a field vector of the form [E_r, E_y]. + + This operator can be used to form an eigenvalue problem of the form + A @ [E_r, E_y] = wavenumber**2 * [E_r, E_y] + + which can then be solved for the eigenmodes of the system (an exp(-i * wavenumber * theta) + theta-dependence is assumed for the fields). + + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param epsilon: Vectorized dielectric constant grid + :param r0: Radius of curvature for the simulation. This should be the minimum value of + r within the simulation domain. + :return: Sparse matrix representation of the operator + """ + + Dfx, Dfy = operators.deriv_forward(dxes[0]) + Dbx, Dby = operators.deriv_back(dxes[1]) + + rx = r0 + numpy.cumsum(dxes[0][0]) + ry = r0 + dxes[0][0]/2.0 + numpy.cumsum(dxes[1][0]) + tx = 1 + rx/r0 + ty = 1 + ry/r0 + + Tx = sparse.diags(vec(tx[:, None].repeat(dxes[0][1].size, axis=1))) + Ty = sparse.diags(vec(ty[:, None].repeat(dxes[1][1].size, axis=1))) + + eps_parts = numpy.split(epsilon, 3) + eps_x = sparse.diags(eps_parts[0]) + eps_y = sparse.diags(eps_parts[1]) + eps_z_inv = sparse.diags(1 / eps_parts[2]) + + pa = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dbx, Dby)) + pb = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dby, Dbx)) + a0 = Ty @ eps_x + omega**-2 * Dby @ Ty @ Dfy + a1 = Tx @ eps_y + omega**-2 * Dbx @ Ty @ Dfx + b0 = Dbx @ Ty @ Dfy + b1 = Dby @ Ty @ Dfx + + diag = sparse.block_diag + op = (omega**2 * diag((Tx, Ty)) + pa) @ diag((a0, a1)) + \ + - (sparse.bmat(((None, Ty), (Tx, None))) + omega**-2 * pb) @ diag((b0, b1)) + + return op + + + diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 1670991..7c7bc5c 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -272,3 +272,69 @@ def compute_overlap_e(E: field_t, overlap_e /= norm_factor * dx_forward return unvec(overlap_e, E[0].shape) + + +def solve_waveguide_mode_cylindrical(mode_number: int, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + r0: float, + wavenumber_correction: bool = True, + ) -> Dict[str, complex or field_t]: + """ + Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode + of the bent waveguide with the specified mode number. + + :param mode_number: Number of the mode, 0-indexed + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header. + The first coordinate is assumed to be r, the second is y. + :param epsilon: Dielectric constant + :param r0: Radius of curvature for the simulation. This should be the minimum value of + r within the simulation domain. + :param wavenumber_correction: Whether to correct the wavenumber to + account for numerical dispersion (default True) + :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} + """ + + ''' + Solve for the largest-magnitude eigenvalue of the real operator + ''' + dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] + + A_r = waveguide.cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0) + eigvals, eigvecs = signed_eigensolve(A_r, mode_number + 3) + v = eigvecs[:, -(mode_number+1)] + + ''' + Now solve for the eigenvector of the full operator, using the real operator's + eigenvector as an initial guess for Rayleigh quotient iteration. + ''' + A = waveguide.cylindrical_operator(omega, dxes, epsilon, r0) + eigval, v = rayleigh_quotient_iteration(A, v) + + # Calculate the wave-vector (force the real part to be positive) + wavenumber = numpy.sqrt(eigval) + wavenumber *= numpy.sign(numpy.real(wavenumber)) + + ''' + Perform correction on wavenumber to account for numerical dispersion. + + See Numerical Dispersion in Taflove's FDTD book. + This correction term reduces the error in emitted power, but additional + error is introduced into the E_err and H_err terms. This effect becomes + more pronounced as beta increases. + ''' + if wavenumber_correction: + wavenumber -= 2 * numpy.sin(numpy.real(wavenumber / 2)) - numpy.real(wavenumber) + + shape = [d.size for d in dxes[0]] + v = numpy.hstack((v, numpy.zeros(shape[0] * shape[1]))) + fields = { + 'wavenumber': wavenumber, + 'E': unvec(v, shape), +# 'E': unvec(e, shape), +# 'H': unvec(h, shape), + } + + return fields From ea04fc42bec11a30430b7ade52bbbba90103fdd7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 25 Sep 2017 00:10:01 -0700 Subject: [PATCH 043/437] Fix switched args --- fdfd_tools/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 7c7bc5c..cffc916 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -41,7 +41,7 @@ def solve_waveguide_mode_2d(mode_number: int, eigenvector as an initial guess for Rayleigh quotient iteration. ''' A = waveguide.operator(omega, dxes, epsilon, mu) - v, eigval = rayleigh_quotient_iteration(A, v) + eigval, v = rayleigh_quotient_iteration(A, v) # Calculate the wave-vector (force the real part to be positive) wavenumber = numpy.sqrt(eigval) From a4cc96395397748b5ff047227693efc505d55e1e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 17 Oct 2017 12:58:15 -0700 Subject: [PATCH 044/437] bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4a3441a..ef1df08 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup(name='fdfd_tools', - version='0.3', + version='0.4', description='FDFD Electromagnetic simulation tools', author='Jan Petykiewicz', author_email='anewusername@gmail.com', From 73e3fa18b11c962dffcbed80a03d522bd38e3fb2 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 17 Oct 2017 13:00:46 -0700 Subject: [PATCH 045/437] fix cylindrical operator --- fdfd_tools/waveguide.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/waveguide.py b/fdfd_tools/waveguide.py index 85e96bf..1566a74 100644 --- a/fdfd_tools/waveguide.py +++ b/fdfd_tools/waveguide.py @@ -340,8 +340,8 @@ def cylindrical_operator(omega: complex, rx = r0 + numpy.cumsum(dxes[0][0]) ry = r0 + dxes[0][0]/2.0 + numpy.cumsum(dxes[1][0]) - tx = 1 + rx/r0 - ty = 1 + ry/r0 + tx = rx/r0 + ty = ry/r0 Tx = sparse.diags(vec(tx[:, None].repeat(dxes[0][1].size, axis=1))) Ty = sparse.diags(vec(ty[:, None].repeat(dxes[1][1].size, axis=1))) From 4bf862761100bdb103a7e23034b02a0b5b47249d Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 5 Nov 2017 14:50:30 -0800 Subject: [PATCH 046/437] clarify beta -> wavenumber --- fdfd_tools/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index cffc916..e50cc6a 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -323,7 +323,7 @@ def solve_waveguide_mode_cylindrical(mode_number: int, See Numerical Dispersion in Taflove's FDTD book. This correction term reduces the error in emitted power, but additional error is introduced into the E_err and H_err terms. This effect becomes - more pronounced as beta increases. + more pronounced as the wavenumber increases. ''' if wavenumber_correction: wavenumber -= 2 * numpy.sin(numpy.real(wavenumber / 2)) - numpy.real(wavenumber) From 6503b488ced14a023c2b5a8345efcf73458ce36f Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 5 Nov 2017 16:33:19 -0800 Subject: [PATCH 047/437] add farfield.py --- fdfd_tools/farfield.py | 220 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 fdfd_tools/farfield.py diff --git a/fdfd_tools/farfield.py b/fdfd_tools/farfield.py new file mode 100644 index 0000000..84a04ba --- /dev/null +++ b/fdfd_tools/farfield.py @@ -0,0 +1,220 @@ +""" +Functions for performing near-to-farfield transformation (and the reverse). +""" +from typing import Dict, List +import numpy +from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift +from numpy import pi + + +def near_to_farfield(E_near: List[numpy.ndarray], + H_near: List[numpy.ndarray], + dx: float, + dy: float, + padded_size: List[int] = None + ) -> Dict[str]: + """ + Compute the farfield, i.e. the distribution of the fields after propagation + through several wavelengths of uniform medium. + + The input fields should be complex phasors. + + :param E_near: List of 2 ndarrays containing the 2D phasor field slices for the transverse + E fields (e.g. [Ex, Ey] for calculating the farfield toward the z-direction). + :param H_near: List of 2 ndarrays containing the 2D phasor field slices for the transverse + H fields (e.g. [Hx, hy] for calculating the farfield towrad the z-direction). + :param dx: Cell size along x-dimension, in units of wavelength. + :param dy: Cell size along y-dimension, in units of wavelength. + :param padded_size: Shape of the output. A single integer `n` will be expanded to `(n, n)`. + Powers of 2 are most efficient for FFT computation. + Default is the smallest power of 2 larger than the input, for each axis. + :returns: Dict with keys + 'E_far': Normalized E-field farfield; multiply by + (i k exp(-i k r) / (4 pi r)) to get the actual field value. + 'H_far': Normalized H-field farfield; multiply by + (i k exp(-i k r) / (4 pi r)) to get the actual field value. + 'kx', 'ky': Wavevector values corresponding to the x- and y- axes in E_far and H_far, + normalized to wavelength (dimensionless). + 'dkx', 'dky': step size for kx and ky, normalized to wavelength. + 'theta': arctan2(ky, kx) corresponding to each (kx, ky). + This is the angle in the x-y plane, counterclockwise from above, starting from +x. + 'phi': arccos(kz / k) corresponding to each (kx, ky). + This is the angle away from +z. + """ + + if not len(E_near) == 2: + raise Exception('E_near must be a length-2 list of ndarrays') + if not len(H_near) == 2: + raise Exception('H_near must be a length-2 list of ndarrays') + + s = E_near[0].shape + if not all(s == f.shape for f in E_near + H_near): + raise Exception('All fields must be the same shape!') + + if padded_size is None: + padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int) + if not hasattr(padded_size, '__len__'): + padded_size = (padded_size, padded_size) + + En_fft = [fftshift(fft2(fftshift(Eni), s=padded_size)) for Eni in E_near] + Hn_fft = [fftshift(fft2(fftshift(Hni), s=padded_size)) for Hni in H_near] + + # Propagation vectors kx, ky + k = 2 * pi + kxx = 2 * pi * fftshift(fftfreq(padded_size[0], dx)) + kyy = 2 * pi * fftshift(fftfreq(padded_size[1], dy)) + + kx, ky = numpy.meshgrid(kxx, kyy, indexing='ij') + kxy2 = kx * kx + ky * ky + kxy = numpy.sqrt(kxy2) + kz = numpy.sqrt(k * k - kxy2) + + sin_th = ky / kxy + cos_th = kx / kxy + cos_phi = kz / k + + sin_th[numpy.logical_and(kx == 0, ky == 0)] = 0 + cos_th[numpy.logical_and(kx == 0, ky == 0)] = 1 + + # Normalized vector potentials N, L + N = [-Hn_fft[1] * cos_phi * cos_th + Hn_fft[0] * cos_phi * sin_th, + Hn_fft[1] * sin_th + Hn_fft[0] * cos_th] + L = [ En_fft[1] * cos_phi * cos_th - En_fft[0] * cos_phi * sin_th, + -En_fft[1] * sin_th - En_fft[0] * cos_th] + + E_far = [-L[1] - N[0], + L[0] - N[1]] + H_far = [-E_far[1], + E_far[0]] + + theta = numpy.arctan2(ky, kx) + phi = numpy.arccos(cos_phi) + theta[numpy.logical_and(kx == 0, ky == 0)] = 0 + phi[numpy.logical_and(kx == 0, ky == 0)] = 0 + + # Zero fields beyond valid (phi, theta) + invalid_ind = kxy2 >= k * k + theta[invalid_ind] = 0 + phi[invalid_ind] = 0 + for i in range(2): + E_far[i][invalid_ind] = 0 + H_far[i][invalid_ind] = 0 + + outputs = { + 'E': E_far, + 'H': H_far, + 'dkx': kx[1]-kx[0], + 'dky': ky[1]-ky[0], + 'kx': kx, + 'ky': ky, + 'theta': theta, + 'phi': phi, + } + + return outputs + + + +def far_to_nearfield(E_far: List[numpy.ndarray], + H_far: List[numpy.ndarray], + dkx: float, + dky: float, + padded_size: List[int] = None + ) -> Dict[str]: + """ + Compute the farfield, i.e. the distribution of the fields after propagation + through several wavelengths of uniform medium. + + The input fields should be complex phasors. + + :param E_far: List of 2 ndarrays containing the 2D phasor field slices for the transverse + E fields (e.g. [Ex, Ey] for calculating the nearfield toward the z-direction). + Fields should be normalized so that + E_far = E_far_actual / (i k exp(-i k r) / (4 pi r)) + :param H_far: List of 2 ndarrays containing the 2D phasor field slices for the transverse + H fields (e.g. [Hx, hy] for calculating the nearfield toward the z-direction). + Fields should be normalized so that + H_far = H_far_actual / (i k exp(-i k r) / (4 pi r)) + :param dkx: kx discretization, in units of wavelength. + :param dky: ky discretization, in units of wavelength. + :param padded_size: Shape of the output. A single integer `n` will be expanded to `(n, n)`. + Powers of 2 are most efficient for FFT computation. + Default is the smallest power of 2 larger than the input, for each axis. + :returns: Dict with keys + 'E': E-field nearfield + 'H': H-field nearfield + 'dx', 'dy': spatial discretization, normalized to wavelength (dimensionless) + """ + + if not len(E_far) == 2: + raise Exception('E_far must be a length-2 list of ndarrays') + if not len(H_far) == 2: + raise Exception('H_far must be a length-2 list of ndarrays') + + s = E_far[0].shape + if not all(s == f.shape for f in E_far + H_far): + raise Exception('All fields must be the same shape!') + + if padded_size is None: + padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int) + if not hasattr(padded_size, '__len__'): + padded_size = (padded_size, padded_size) + + + k = 2 * pi + kxs = fftshift(fftfreq(s[0], 1/(s[0] * dkx))) + kys = fftshift(fftfreq(s[0], 1/(s[1] * dky))) + + kx, ky = numpy.meshgrid(kxs, kys, indexing='ij') + kxy2 = kx * kx + ky * ky + kxy = numpy.sqrt(kxy2) + + kz = numpy.sqrt(k * k - kxy2) + + sin_th = ky / kxy + cos_th = kx / kxy + cos_phi = kz / k + + sin_th[numpy.logical_and(kx == 0, ky == 0)] = 0 + cos_th[numpy.logical_and(kx == 0, ky == 0)] = 1 + + # Zero fields beyond valid (phi, theta) + invalid_ind = kxy2 >= k * k + theta[invalid_ind] = 0 + phi[invalid_ind] = 0 + for i in range(2): + E_far[i][invalid_ind] = 0 + H_far[i][invalid_ind] = 0 + + + # Normalized vector potentials N, L + L = [0.5 * E_far[1], + -0.5 * E_far[0]] + N = [L[1], + -L[0]] + + En_fft = [-( L[0] * sin_th + L[1] * cos_phi * cos_th)/cos_phi, + -(-L[0] * cos_th + L[1] * cos_phi * sin_th)/cos_phi] + + Hn_fft = [( N[0] * sin_th + N[1] * cos_phi * cos_th)/cos_phi, + (-N[0] * cos_th + N[1] * cos_phi * sin_th)/cos_phi] + + for i in range(2): + En_fft[i][cos_phi == 0] = 0 + Hn_fft[i][cos_phi == 0] = 0 + + E_near = [ifftshift(ifft2(ifftshift(Ei), s=padded_size)) for Ei in En_fft] + H_near = [ifftshift(ifft2(ifftshift(Hi), s=padded_size)) for Hi in Hn_fft] + + dx = 2 * pi / (s[0] * dkx) + dy = 2 * pi / (s[0] * dky) + + outputs = { + 'E': E_near, + 'H': H_near, + 'dx': dx, + 'dy': dy, + } + + return outputs + From 4aa2d07cef0e8793bbe48bfcf4ac246b193d467f Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 9 Dec 2017 18:21:37 -0800 Subject: [PATCH 048/437] Add Bloch eigenproblem --- fdfd_tools/bloch.py | 406 ++++++++++++++++++++++++++++++++++++ fdfd_tools/eigensolvers.py | 19 +- fdfd_tools/vectorization.py | 2 +- 3 files changed, 420 insertions(+), 7 deletions(-) create mode 100644 fdfd_tools/bloch.py diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py new file mode 100644 index 0000000..d7a2ffa --- /dev/null +++ b/fdfd_tools/bloch.py @@ -0,0 +1,406 @@ +''' +Bloch eigenmode solver/operators + +This module contains functions for generating and solving the + 3D Bloch eigenproblem. The approach is to transform the problem + into the (spatial) fourier domain, transforming the equation + 1/mu * curl(1/eps * curl(H)) = (w/c)^2 H + into + conv(1/mu_k, ik x conv(1/eps_k, ik x H_k)) = (w/c)^2 H_k + where: + - the _k subscript denotes a 3D fourier transformed field + - each component of H_k corresponds to a plane wave with wavevector k + - x is the cross product + - conv denotes convolution + + Since k and H are orthogonal for each plane wave, we can use each + k to create an orthogonal basis (k, m, n), with k x m = n, and + |m| = |n| = 1. The cross products are then simplified with + + k @ h = kx hx + ky hy + kz hz = 0 = hk + h = hk + hm + hn = hm + hn + k = kk + km + kn = kk = |k| + + k x h = (ky hz - kz hy, + kz hx - kx hz, + kx hy - ky hx) + = ((k x h) @ k, (k x h) @ m, (k x h) @ n)_kmn + = (0, (m x k) @ h, (n x k) @ h)_kmn # triple product ordering + = (0, kk (-n @ h), kk (m @ h))_kmn # (m x k) = -|k| n, etc. + = |k| (0, -h @ n, h @ m)_kmn + + k x h = (km hn - kn hm, + kn hk - kk hn, + kk hm - km hk)_kmn + = (0, -kk hn, kk hm)_kmn + = (-kk hn)(mx, my, mz) + (kk hm)(nx, ny, nz) + = |k| (hm * (nx, ny, nz) - hn * (mx, my, mz)) + + where h is shorthand for H_k, (...)_kmn deontes the (k, m, n) basis, + and e.g. hm is the component of h in the m direction. + + We can also simplify conv(X_k, Y_k) as fftn(X * ifftn(Y_k)). + + Using these results and storing H_k as h = (hm, hn), we have + e_xyz = fftn(1/eps * ifftn(|k| (hm * n - hn * m))) + b_mn = |k| (-e_xyz @ n, e_xyz @ m) + h_mn = fftn(1/mu * ifftn(b_m * m + b_n * n)) + which forms the operator from the left side of the equation. + + We can then use ARPACK in shift-invert mode (via scipy.linalg.eigs) + to find the eigenvectors for this operator. + + This approach is similar to the one used in MPB and derived at the start of + SG Johnson and JD Joannopoulos, Block-iterative frequency-domain methods + for Maxwell's equations in a planewave basis, Optics Express 8, 3, 173-190 (2001) + + === + + Typically you will want to do something like + + recip_lattice = numpy.diag(1/numpy.array(epsilon[0].shape * dx)) + n, v = bloch.eigsolve(5, k0, recip_lattice, epsilon) + f = numpy.sqrt(-numpy.real(n[0])) + n_eff = norm(recip_lattice @ k0) / f + + v2e = bloch.hmn_2_exyz(k0, recip_lattice, epsilon) + e_field = v2e(v[0]) + + k, f = find_k(frequency=1/1550, + tolerance=(1/1550 - 1/1551), + search_direction=[1, 0, 0], + G_matrix=recip_lattice, + epsilon=epsilon, + band=0) + +''' + +from typing import List, Tuple, Callable, Dict +import numpy +from numpy.fft import fftn, ifftn, fftfreq +import scipy +from scipy.linalg import norm +import scipy.sparse.linalg as spalg + +from . import field_t + + +def generate_kmn(k0: numpy.ndarray, + G_matrix: numpy.ndarray, + shape: numpy.ndarray + ) -> Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]: + """ + Generate a (k, m, n) orthogonal basis for each k-vector in the simulation grid. + + :param k0: [k0x, k0y, k0z], Bloch wavevector, in G basis. + :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + :param shape: [nx, ny, nz] shape of the simulation grid. + :return: (|k|, m, n) where |k| has shape tuple(shape) + (1,) + and m, n have shape tuple(shape) + (3,). + All are given in the xyz basis (e.g. |k|[0,0,0] = norm(G_matrix @ k0)). + """ + k0 = numpy.array(k0) + + Gi_grids = numpy.meshgrid(*(fftfreq(n, 1/n) for n in shape[:3]), indexing='ij') + Gi = numpy.stack(Gi_grids, axis=3) + + k_G = k0[None, None, None, :] - Gi + k_xyz = numpy.rollaxis(G_matrix @ numpy.rollaxis(k_G, 3, 2), 3, 2) + + m = numpy.broadcast_to([0, 1, 0], tuple(shape[:3]) + (3,)).astype(float) + n = numpy.broadcast_to([0, 0, 1], tuple(shape[:3]) + (3,)).astype(float) + + xy_non0 = numpy.any(k_xyz[:, :, :, 0:1] != 0, axis=3) + if numpy.any(xy_non0): + u = numpy.cross(k_xyz[xy_non0], [0, 0, 1]) + m[xy_non0, :] = u / norm(u, axis=1)[:, None] + + z_non0 = numpy.any(k_xyz != 0, axis=3) + if numpy.any(z_non0): + v = numpy.cross(k_xyz[z_non0], m[z_non0]) + n[z_non0, :] = v / norm(v, axis=1)[:, None] + + k_mag = norm(k_xyz, axis=3)[:, :, :, None] + return k_mag, m, n + + +def maxwell_operator(k0: numpy.ndarray, + G_matrix: numpy.ndarray, + epsilon: field_t, + mu: field_t = None + ) -> Callable[[numpy.ndarray], numpy.ndarray]: + """ + Generate the Maxwell operator + conv(1/mu_k, ik x conv(1/eps_k, ik x ___)) + which is the spatial-frequency-space representation of + 1/mu * curl(1/eps * curl(___)) + + The operator is a function that acts on a vector h_mn of size (2 * epsilon[0].size) + + See the module-level docstring for more information. + + :param k0: Bloch wavevector, [k0x, k0y, k0z]. + :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + :param epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + :param mu: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + :return: Function which applies the maxwell operator to h_mn. + """ + + shape = epsilon[0].shape + (1,) + k_mag, m, n = generate_kmn(k0, G_matrix, shape) + + epsilon = numpy.stack(epsilon, 3) + if mu is not None: + mu = numpy.stack(mu, 3) + + def operator(h: numpy.ndarray): + """ + Maxwell operator for Bloch eigenmode simulation. + + h is complex 2-field in (m, n) basis, vectorized + + :param h: Raveled h_mn; size (2 * epsilon[0].size). + :return: Raveled conv(1/mu_k, ik x conv(1/eps_k, ik x h_mn)). + """ + hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] + + #{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis + + # cross product and transform into xyz basis + d_xyz = (n * hin_m - + m * hin_n) * k_mag + + # divide by epsilon + e_xyz = ifftn(fftn(d_xyz, axes=range(3)) / epsilon, axes=range(3)) + + # cross product and transform into mn basis + b_m = numpy.sum(e_xyz * n, axis=3)[:, :, :, None] * -k_mag + b_n = numpy.sum(e_xyz * m, axis=3)[:, :, :, None] * +k_mag + + if mu is None: + h_m, h_n = b_m, b_n + else: + # transform from mn to xyz + b_xyz = (m * b_m[:, :, :, None] + + n * b_n[:, :, :, None]) + + # divide by mu + h_xyz = ifftn(fftn(b_xyz, axes=range(3)) / mu, axes=range(3)) + + # transform back to mn + h_m = numpy.sum(h_xyz * m, axis=3) + h_n = numpy.sum(h_xyz * n, axis=3) + return numpy.hstack((h_m.ravel(), h_n.ravel())) + + return operator + + +def hmn_2_exyz(k0: numpy.ndarray, + G_matrix: numpy.ndarray, + epsilon: field_t, + ) -> Callable[[numpy.ndarray], field_t]: + """ + Generate an operator which converts a vectorized spatial-frequency-space + h_mn into an E-field distribution, i.e. + ifft(conv(1/eps_k, ik x h_mn)) + + The operator is a function that acts on a vector h_mn of size (2 * epsilon[0].size) + + See the module-level docstring for more information. + + :param k0: Bloch wavevector, [k0x, k0y, k0z]. + :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + :param epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + :return: Function for converting h_mn into E_xyz + """ + shape = epsilon[0].shape + (1,) + epsilon = numpy.stack(epsilon, 3) + + k_mag, m, n = generate_kmn(k0, G_matrix, shape) + + def operator(h: numpy.ndarray) -> field_t: + hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] + d_xyz = (n * hin_m - + m * hin_n) * k_mag + + # divide by epsilon + return [ei for ei in numpy.rollaxis(fftn(d_xyz, axes=range(3)) / epsilon, 3)] + + return operator + + +def hmn_2_hxyz(k0: numpy.ndarray, + G_matrix: numpy.ndarray, + epsilon: field_t + ) -> Callable[[numpy.ndarray], field_t]: + """ + Generate an operator which converts a vectorized spatial-frequency-space + h_mn into an H-field distribution, i.e. + ifft(h_mn) + + The operator is a function that acts on a vector h_mn of size (2 * epsilon[0].size) + + See the module-level docstring for more information. + + :param k0: Bloch wavevector, [k0x, k0y, k0z]. + :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + :param epsilon: Dielectric constant distribution for the simulation. + Only epsilon[0].shape is used. + :return: Function for converting h_mn into H_xyz + """ + shape = epsilon[0].shape + (1,) + k_mag, m, n = generate_kmn(k0, G_matrix, shape) + + def operator(h: numpy.ndarray): + hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] + h_xyz = (m * hin_m + + n * hin_n) + return [fftn(hi) for hi in numpy.rollaxis(h_xyz, 3)] + + return operator + + +def inverse_maxwell_operator_approx(k0: numpy.ndarray, + G_matrix: numpy.ndarray, + epsilon: field_t, + mu: field_t = None + ) -> Callable[[numpy.ndarray], numpy.ndarray]: + """ + Generate an approximate inverse of the Maxwell operator, + ik x conv(eps_k, ik x conv(mu_k, ___)) + which can be used to improve the speed of ARPACK in shift-invert mode. + + See the module-level docstring for more information. + + :param k0: Bloch wavevector, [k0x, k0y, k0z]. + :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + :param epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + :param mu: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + :return: Function which applies the approximate inverse of the maxwell operator to h_mn. + """ + shape = epsilon[0].shape + (1,) + epsilon = numpy.stack(epsilon, 3) + + k_mag, m, n = generate_kmn(k0, G_matrix, shape) + + if mu is not None: + mu = numpy.stack(mu, 3) + + def operator(h: numpy.ndarray): + """ + Approximate inverse Maxwell operator for Bloch eigenmode simulation. + + h is complex 2-field in (m, n) basis, vectorized + + :param h: Raveled h_mn; size (2 * epsilon[0].size). + :return: Raveled ik x conv(eps_k, ik x conv(mu_k, h_mn)) + """ + hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] + + #{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis + + if mu is None: + b_m, b_n = hin_m, hin_n + else: + # transform from mn to xyz + h_xyz = (m * hin_m[:, :, :, None] + + n * hin_n[:, :, :, None]) + + # multiply by mu + b_xyz = ifftn(fftn(h_xyz, axes=range(3)) * mu, axes=range(3)) + + # transform back to mn + b_m = numpy.sum(b_xyz * m, axis=3) + b_n = numpy.sum(b_xyz * n, axis=3) + + # cross product and transform into xyz basis + e_xyz = (n * b_m - + m * b_n) / k_mag + + # multiply by epsilon + d_xyz = ifftn(fftn(e_xyz, axes=range(3)) * epsilon, axes=range(3)) + + # cross product and transform into mn basis crossinv_t2c + h_m = numpy.sum(e_xyz * n, axis=3)[:, :, :, None] / +k_mag + h_n = numpy.sum(e_xyz * m, axis=3)[:, :, :, None] / -k_mag + + return numpy.hstack((h_m.ravel(), h_n.ravel())) + + return operator + + +def eigsolve(num_modes: int, + k0: numpy.ndarray, + G_matrix: numpy.ndarray, + epsilon: field_t, + mu: field_t = None + ) -> Tuple[numpy.ndarray, numpy.ndarray]: + """ + Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector + k0 of the specified structure. + + :param k0: Bloch wavevector, [k0x, k0y, k0z]. + :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + :param epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + :param mu: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + :return: (eigenvalues, eigenvectors) where eigenvalues[i] corresponds to the + vector eigenvectors[i, :] + """ + h_size = 2 * epsilon[0].size + + mop = maxwell_operator(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) + imop = inverse_maxwell_operator_approx(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) + + scipy_op = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=mop) + scipy_iop = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=imop) + + _eigvals, eigvecs = spalg.eigs(scipy_op, num_modes, sigma=0, OPinv=scipy_iop, which='LM') + eigvals = numpy.sum(eigvecs * (scipy_op @ eigvecs), axis=0) / numpy.sum(eigvecs * eigvecs, axis=0) + order = numpy.argsort(-eigvals) + return eigvals[order], eigvecs.T[order] + + +def find_k(frequency: float, + tolerance: float, + search_direction: numpy.ndarray, + G_matrix: numpy.ndarray, + epsilon: field_t, + mu: field_t = None, + band: int = 0 + ) -> Tuple[numpy.ndarray, float]: + """ + Search for a bloch vector that has a given frequency. + + :param frequency: Target frequency. + :param tolerance: Target frequency tolerance. + :param search_direction: k-vector direction to search along. + :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + :param epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + :param mu: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + :param band: Which band to search in. Default 0 (lowest frequency). + return: (k, actual_frequency) The found k-vector and its frequency + """ + + search_direction = numpy.array(search_direction) / norm(search_direction) + + def get_f(k0_mag: float, band: int = 0): + k0 = search_direction * k0_mag + n, _v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon) + f = numpy.sqrt(numpy.abs(numpy.real(n[band]))) + return f + + res = scipy.optimize.minimize_scalar(lambda x: abs(get_f(x, band) - frequency), 0.25, + method='Bounded', bounds=(0, 0.5), + options={'xatol': abs(tolerance)}) + return res.x * search_direction, res.fun + frequency + + diff --git a/fdfd_tools/eigensolvers.py b/fdfd_tools/eigensolvers.py index 6c06296..24d7339 100644 --- a/fdfd_tools/eigensolvers.py +++ b/fdfd_tools/eigensolvers.py @@ -29,7 +29,7 @@ def power_iteration(operator: sparse.spmatrix, v = operator @ v v /= norm(v) - lm_eigval = v.conj() @ operator @ v + lm_eigval = v.conj() @ (operator @ v) return lm_eigval, v @@ -59,7 +59,7 @@ def rayleigh_quotient_iteration(operator: sparse.spmatrix, return eigval, v -def signed_eigensolve(operator: sparse.spmatrix, +def signed_eigensolve(operator: sparse.spmatrix or spalg.LinearOperator, how_many: int, negative: bool = False, ) -> Tuple[numpy.ndarray, numpy.ndarray]: @@ -83,12 +83,19 @@ def signed_eigensolve(operator: sparse.spmatrix, largest _positive_ eigenvalues, since any negative eigenvalues will be shifted to the range 0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval) ''' + shift = numpy.abs(lm_eigval) if negative: - shifted_operator = operator - abs(lm_eigval) * sparse.eye(operator.shape[0]) - else: - shifted_operator = operator + abs(lm_eigval) * sparse.eye(operator.shape[0]) + shift *= -1 - eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=50) + # Try to combine, use general LinearOperator if we fail + try: + shifted_operator = operator + shift * sparse.eye(operator.shape[0]) + except TypeError: + shifted_operator = operator + spalg.LinearOperator(shape=operator.shape, + matvec=lambda v: shift * v) + + shifted_eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=50) + eigenvalues = shifted_eigenvalues - shift k = eigenvalues.argsort() return eigenvalues[k], eigenvectors[:, k] diff --git a/fdfd_tools/vectorization.py b/fdfd_tools/vectorization.py index 1c2134a..e7b9645 100644 --- a/fdfd_tools/vectorization.py +++ b/fdfd_tools/vectorization.py @@ -1,7 +1,7 @@ """ Functions for moving between a vector field (list of 3 ndarrays, [f_x, f_y, f_z]) and a 1D array representation of that field [f_x0, f_x1, f_x2,... f_y0,... f_z0,...]. -Vectorized versions of the field use column-major (ie., Fortran, Matlab) ordering. +Vectorized versions of the field use row-major (ie., C-style) ordering. """ From d09eff990f9160e70bc837174990305a7be791db Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 17 Dec 2017 20:51:34 -0800 Subject: [PATCH 049/437] Update Rayleigh quotient iteration to allow arbitrary linear operators --- fdfd_tools/eigensolvers.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/fdfd_tools/eigensolvers.py b/fdfd_tools/eigensolvers.py index 24d7339..e348cd7 100644 --- a/fdfd_tools/eigensolvers.py +++ b/fdfd_tools/eigensolvers.py @@ -33,10 +33,11 @@ def power_iteration(operator: sparse.spmatrix, return lm_eigval, v -def rayleigh_quotient_iteration(operator: sparse.spmatrix, +def rayleigh_quotient_iteration(operator: sparse.spmatrix or spalg.LinearOperator, guess_vector: numpy.ndarray, iterations: int = 40, tolerance: float = 1e-13, + solver=None, ) -> Tuple[complex, numpy.ndarray]: """ Use Rayleigh quotient iteration to refine an eigenvector guess. @@ -46,16 +47,33 @@ def rayleigh_quotient_iteration(operator: sparse.spmatrix, :param iterations: Maximum number of iterations to perform. Default 40. :param tolerance: Stop iteration if (A - I*eigenvalue) @ v < tolerance. Default 1e-13. + :param solver: Solver function of the form x = solver(A, b). + By default, use scipy.sparse.spsolve for sparse matrices and + scipy.sparse.bicgstab for general LinearOperator instances. :return: (eigenvalue, eigenvector) """ + try: + _test = operator - sparse.eye(operator.shape) + shift = lambda eigval: eigval * sparse.eye(operator.shape[0]) + if solver is None: + solver = spalg.spsolve + except TypeError: + shift = lambda eigval: spalg.LinearOperator(shape=operator.shape, + dtype=operator.dtype, + matvec=lambda v: eigval * v) + if solver is None: + solver = lambda A, b: spalg.bicgstab(A, b)[0] + v = guess_vector + v /= norm(v) for _ in range(iterations): - eigval = v.conj() @ operator @ v + eigval = v.conj() @ (operator @ v) if norm(operator @ v - eigval * v) < tolerance: break - v = spalg.spsolve(operator - eigval * sparse.eye(operator.shape[0]), v) - v /= norm(v) + shifted_operator = operator - shift(eigval) + v = solver(shifted_operator, v) + v /= norm(v) return eigval, v From 000cfabd788275be7de7ead2a4d85c8a95e734c6 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 17 Dec 2017 21:32:29 -0800 Subject: [PATCH 050/437] switch fft, ifft --- fdfd_tools/bloch.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index d7a2ffa..df63e6e 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -173,7 +173,7 @@ def maxwell_operator(k0: numpy.ndarray, m * hin_n) * k_mag # divide by epsilon - e_xyz = ifftn(fftn(d_xyz, axes=range(3)) / epsilon, axes=range(3)) + e_xyz = fftn(ifftn(d_xyz, axes=range(3)) / epsilon, axes=range(3)) # cross product and transform into mn basis b_m = numpy.sum(e_xyz * n, axis=3)[:, :, :, None] * -k_mag @@ -187,7 +187,7 @@ def maxwell_operator(k0: numpy.ndarray, n * b_n[:, :, :, None]) # divide by mu - h_xyz = ifftn(fftn(b_xyz, axes=range(3)) / mu, axes=range(3)) + h_xyz = fftn(ifftn(b_xyz, axes=range(3)) / mu, axes=range(3)) # transform back to mn h_m = numpy.sum(h_xyz * m, axis=3) @@ -227,7 +227,7 @@ def hmn_2_exyz(k0: numpy.ndarray, m * hin_n) * k_mag # divide by epsilon - return [ei for ei in numpy.rollaxis(fftn(d_xyz, axes=range(3)) / epsilon, 3)] + return [ei for ei in numpy.rollaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3)] return operator @@ -258,7 +258,7 @@ def hmn_2_hxyz(k0: numpy.ndarray, hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] h_xyz = (m * hin_m + n * hin_n) - return [fftn(hi) for hi in numpy.rollaxis(h_xyz, 3)] + return [ifftn(hi) for hi in numpy.rollaxis(h_xyz, 3)] return operator @@ -312,7 +312,7 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, n * hin_n[:, :, :, None]) # multiply by mu - b_xyz = ifftn(fftn(h_xyz, axes=range(3)) * mu, axes=range(3)) + b_xyz = fftn(ifftn(h_xyz, axes=range(3)) * mu, axes=range(3)) # transform back to mn b_m = numpy.sum(b_xyz * m, axis=3) @@ -323,7 +323,7 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, m * b_n) / k_mag # multiply by epsilon - d_xyz = ifftn(fftn(e_xyz, axes=range(3)) * epsilon, axes=range(3)) + d_xyz = fftn(ifftn(e_xyz, axes=range(3)) * epsilon, axes=range(3)) # cross product and transform into mn basis crossinv_t2c h_m = numpy.sum(e_xyz * n, axis=3)[:, :, :, None] / +k_mag From 4a9596921f507e79a3ea59ac2d0d6129a49b3c08 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 17 Dec 2017 21:32:59 -0800 Subject: [PATCH 051/437] rename search_direction to direction --- fdfd_tools/bloch.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index df63e6e..33e9255 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -68,7 +68,7 @@ This module contains functions for generating and solving the k, f = find_k(frequency=1/1550, tolerance=(1/1550 - 1/1551), - search_direction=[1, 0, 0], + direction=[1, 0, 0], G_matrix=recip_lattice, epsilon=epsilon, band=0) @@ -369,7 +369,7 @@ def eigsolve(num_modes: int, def find_k(frequency: float, tolerance: float, - search_direction: numpy.ndarray, + direction: numpy.ndarray, G_matrix: numpy.ndarray, epsilon: field_t, mu: field_t = None, @@ -380,7 +380,7 @@ def find_k(frequency: float, :param frequency: Target frequency. :param tolerance: Target frequency tolerance. - :param search_direction: k-vector direction to search along. + :param direction: k-vector direction to search along. :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. :param epsilon: Dielectric constant distribution for the simulation. All fields are sampled at cell centers (i.e., NOT Yee-gridded) @@ -390,10 +390,10 @@ def find_k(frequency: float, return: (k, actual_frequency) The found k-vector and its frequency """ - search_direction = numpy.array(search_direction) / norm(search_direction) + direction = numpy.array(direction) / norm(direction) def get_f(k0_mag: float, band: int = 0): - k0 = search_direction * k0_mag + k0 = direction * k0_mag n, _v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon) f = numpy.sqrt(numpy.abs(numpy.real(n[band]))) return f @@ -401,6 +401,6 @@ def find_k(frequency: float, res = scipy.optimize.minimize_scalar(lambda x: abs(get_f(x, band) - frequency), 0.25, method='Bounded', bounds=(0, 0.5), options={'xatol': abs(tolerance)}) - return res.x * search_direction, res.fun + frequency + return res.x * direction, res.fun + frequency From 39979edc440a7e3e547f81921b0bfba8bf8ba895 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 17 Dec 2017 21:33:53 -0800 Subject: [PATCH 052/437] implement eigenvalue algorithm from Johnson paper. Could also use arpack + refinement, but that's also slow. --- fdfd_tools/bloch.py | 109 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 33e9255..8d85b19 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -47,12 +47,10 @@ This module contains functions for generating and solving the h_mn = fftn(1/mu * ifftn(b_m * m + b_n * n)) which forms the operator from the left side of the equation. - We can then use ARPACK in shift-invert mode (via scipy.linalg.eigs) - to find the eigenvectors for this operator. - - This approach is similar to the one used in MPB and derived at the start of - SG Johnson and JD Joannopoulos, Block-iterative frequency-domain methods + We can then use a preconditioned block Rayleigh iteration algorithm, as in + SG Johnson and JD Joannopoulos, Block-iterative frequency-domain methods for Maxwell's equations in a planewave basis, Optics Express 8, 3, 173-190 (2001) + (similar to that used in MPB) to find the eigenvectors for this operator. === @@ -76,14 +74,19 @@ This module contains functions for generating and solving the ''' from typing import List, Tuple, Callable, Dict +import logging import numpy from numpy.fft import fftn, ifftn, fftfreq import scipy +import scipy.optimize from scipy.linalg import norm import scipy.sparse.linalg as spalg +from .eigensolvers import rayleigh_quotient_iteration from . import field_t +logger = logging.getLogger(__name__) + def generate_kmn(k0: numpy.ndarray, G_matrix: numpy.ndarray, @@ -338,7 +341,8 @@ def eigsolve(num_modes: int, k0: numpy.ndarray, G_matrix: numpy.ndarray, epsilon: field_t, - mu: field_t = None + mu: field_t = None, + tolerance = 1e-8, ) -> Tuple[numpy.ndarray, numpy.ndarray]: """ Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector @@ -355,15 +359,102 @@ def eigsolve(num_modes: int, """ h_size = 2 * epsilon[0].size + ''' + Generate the operators + ''' mop = maxwell_operator(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) imop = inverse_maxwell_operator_approx(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) scipy_op = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=mop) scipy_iop = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=imop) - _eigvals, eigvecs = spalg.eigs(scipy_op, num_modes, sigma=0, OPinv=scipy_iop, which='LM') - eigvals = numpy.sum(eigvecs * (scipy_op @ eigvecs), axis=0) / numpy.sum(eigvecs * eigvecs, axis=0) - order = numpy.argsort(-eigvals) + y_shape = (h_size, num_modes) + + def rayleigh_quotient(Z: numpy.ndarray, approx_grad: bool = True): + """ + Absolute value of the block Rayleigh quotient, and the associated gradient. + + See Johnson and Joannopoulos, Opt. Expr. 8, 3 (2001) for details (full + citation in module docstring). + + === + + Notes on my understanding of the procedure: + + Minimize f(Y) = |trace((Y.H @ A @ Y)|, making use of Y = Z @ inv(Z.H @ Z)^(1/2) + (a polar orthogonalization of Y). This gives f(Z) = |trace(Z.H @ A @ Z @ U)|, + where U = inv(Z.H @ Z). We minimize the absolute value to find the eigenvalues + with smallest magnitude. + + The gradient is P @ (A @ Z @ U), where P = (1 - Z @ U @ Z.H) is a projection + onto the space orthonormal to Z. If approx_grad is True, the approximate + inverse of the maxwell operator is used to precondition the gradient. + """ + z = Z.reshape(y_shape) + U = numpy.linalg.inv(z.conj().T @ z) + zU = z @ U + AzU = scipy_op @ zU + zTAzU = z.conj().T @ AzU + f = numpy.real(numpy.trace(zTAzU)) + if approx_grad: + df_dy = scipy_iop @ (AzU - zU @ zTAzU) + else: + df_dy = (AzU - zU @ zTAzU) + return numpy.abs(f), numpy.sign(f) * df_dy.ravel() + + ''' + Use the conjugate gradient method and the approximate gradient calculation to + quickly find approximate eigenvectors. + ''' + result = scipy.optimize.minimize(rayleigh_quotient, + numpy.random.rand(*y_shape), + jac=True, + method='CG', + tol=1e-5, + options={'maxiter': 30, 'disp':True}) + + result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, False), + result.x, + jac=True, + method='CG', + tol=1e-13, + options={'maxiter': 100, 'disp':True}) + + z = result.x.reshape(y_shape) + + ''' + Recover eigenvectors from Z + ''' + U = numpy.linalg.inv(z.conj().T @ z) + y = z @ scipy.linalg.sqrtm(U) + w = y.conj().T @ (scipy_op @ y) + + eigvals, w_eigvecs = numpy.linalg.eig(w) + eigvecs = y @ w_eigvecs + + for i in range(len(eigvals)): + v = eigvecs[:, i] + n = eigvals[i] + v /= norm(v) + logger.info('eigness {}: {}'.format(i, norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v ))) + + ev2 = eigvecs.copy() + for i in range(len(eigvals)): + logger.info('Refining eigenvector {}'.format(i)) + eigvals[i], ev2[:, i] = rayleigh_quotient_iteration(scipy_op, + guess_vector=eigvecs[:, i], + iterations=40, + tolerance=tolerance * numpy.real(numpy.sqrt(eigvals[i])) * 2, + solver = lambda A, b: spalg.bicgstab(A, b, maxiter=200)[0]) + eigvecs = ev2 + order = numpy.argsort(numpy.abs(eigvals)) + + for i in range(len(eigvals)): + v = eigvecs[:, i] + n = eigvals[i] + v /= norm(v) + logger.info('eigness {}: {}'.format(i, norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v ))) + return eigvals[order], eigvecs.T[order] From f312d735039668527a55f628cb548c342511b39a Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 17 Dec 2017 22:55:55 -0800 Subject: [PATCH 053/437] Return real part of the gradient --- fdfd_tools/bloch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 8d85b19..2d343ce 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -400,7 +400,7 @@ def eigsolve(num_modes: int, df_dy = scipy_iop @ (AzU - zU @ zTAzU) else: df_dy = (AzU - zU @ zTAzU) - return numpy.abs(f), numpy.sign(f) * df_dy.ravel() + return numpy.abs(f), numpy.sign(f) * numpy.real(df_dy).ravel() ''' Use the conjugate gradient method and the approximate gradient calculation to From 16f97d7f6be0ccef2bf440bfa58e258857fe22c2 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 18 Dec 2017 00:13:29 -0800 Subject: [PATCH 054/437] Add ability to set bounds for find_k --- fdfd_tools/bloch.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 2d343ce..ad7c555 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -464,7 +464,9 @@ def find_k(frequency: float, G_matrix: numpy.ndarray, epsilon: field_t, mu: field_t = None, - band: int = 0 + band: int = 0, + k_min: float = 0, + k_max: float = 0.5, ) -> Tuple[numpy.ndarray, float]: """ Search for a bloch vector that has a given frequency. @@ -489,8 +491,10 @@ def find_k(frequency: float, f = numpy.sqrt(numpy.abs(numpy.real(n[band]))) return f - res = scipy.optimize.minimize_scalar(lambda x: abs(get_f(x, band) - frequency), 0.25, - method='Bounded', bounds=(0, 0.5), + res = scipy.optimize.minimize_scalar(lambda x: abs(get_f(x, band) - frequency), + (k_min + k_max) / 2, + method='Bounded', + bounds=(k_min, k_max), options={'xatol': abs(tolerance)}) return res.x * direction, res.fun + frequency From 85030448c3917ebeadc9b2c4a690ffc5d6b6f46b Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 21 Dec 2017 20:11:30 -0800 Subject: [PATCH 055/437] Use L-BFGS instead of CG, and remove rayleigh iteration refinement scipy CG doesn't seem to converge as well as L-BFGS... worth looking into later --- fdfd_tools/bloch.py | 46 ++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index ad7c555..739cc61 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -359,6 +359,8 @@ def eigsolve(num_modes: int, """ h_size = 2 * epsilon[0].size + kmag = norm(G_matrix @ k0) + ''' Generate the operators ''' @@ -409,16 +411,26 @@ def eigsolve(num_modes: int, result = scipy.optimize.minimize(rayleigh_quotient, numpy.random.rand(*y_shape), jac=True, - method='CG', - tol=1e-5, - options={'maxiter': 30, 'disp':True}) + method='L-BFGS-B', + tol=1e-20, + options={'maxiter': 2000, 'gtol':0, 'ftol':1e-20 , 'disp':True})#, 'maxls':80, 'm':30}) + result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, False), result.x, jac=True, - method='CG', - tol=1e-13, - options={'maxiter': 100, 'disp':True}) + method='L-BFGS-B', + tol=1e-20, + options={'maxiter': 2000, 'ptol':1e-18, 'disp':True}) + + for i in range(20): + result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, False), + result.x, + jac=True, + method='L-BFGS-B', + tol=1e-20, + options={'maxiter': 70, 'gtol':1e-18, 'disp':True}) + z = result.x.reshape(y_shape) @@ -436,25 +448,13 @@ def eigsolve(num_modes: int, v = eigvecs[:, i] n = eigvals[i] v /= norm(v) - logger.info('eigness {}: {}'.format(i, norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v ))) + eigness = norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v ) + f = numpy.sqrt(-numpy.real(n)) + df = numpy.sqrt(-numpy.real(n + eigness)) + neff_err = kmag * (1/df - 1/f) + logger.info('eigness {}: {}\n neff_err: {}'.format(i, eigness, neff_err)) - ev2 = eigvecs.copy() - for i in range(len(eigvals)): - logger.info('Refining eigenvector {}'.format(i)) - eigvals[i], ev2[:, i] = rayleigh_quotient_iteration(scipy_op, - guess_vector=eigvecs[:, i], - iterations=40, - tolerance=tolerance * numpy.real(numpy.sqrt(eigvals[i])) * 2, - solver = lambda A, b: spalg.bicgstab(A, b, maxiter=200)[0]) - eigvecs = ev2 order = numpy.argsort(numpy.abs(eigvals)) - - for i in range(len(eigvals)): - v = eigvecs[:, i] - n = eigvals[i] - v /= norm(v) - logger.info('eigness {}: {}'.format(i, norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v ))) - return eigvals[order], eigvecs.T[order] From a70687f5e39b27bf9a579529f51f566fead577e1 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 21 Dec 2017 20:11:42 -0800 Subject: [PATCH 056/437] add bloch example --- examples/bloch.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 examples/bloch.py diff --git a/examples/bloch.py b/examples/bloch.py new file mode 100644 index 0000000..8bbd30e --- /dev/null +++ b/examples/bloch.py @@ -0,0 +1,68 @@ +import numpy, scipy, gridlock, fdfd_tools +from fdfd_tools import bloch +from numpy.linalg import norm +import logging + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + + +dx = 40 +x_period = 400 +y_period = z_period = 2000 +g = gridlock.Grid([numpy.arange(-x_period/2, x_period/2, dx), + numpy.arange(-1000, 1000, dx), + numpy.arange(-1000, 1000, dx)], + shifts=numpy.array([[0,0,0]]), + initial=1.445**2, + periodic=True) + +g.draw_cuboid([0,0,0], [200e8, 220, 220], eps=3.47**2) + +#x_period = y_period = z_period = 13000 +#g = gridlock.Grid([numpy.arange(3), ]*3, +# shifts=numpy.array([[0, 0, 0]]), +# initial=2.0**2, +# periodic=True) + +g2 = g.copy() +g2.shifts = numpy.zeros((6,3)) +g2.grids = [numpy.zeros(g.shape) for _ in range(6)] + +epsilon = [g.grids[0],] * 3 +reciprocal_lattice = numpy.diag(1e6/numpy.array([x_period, y_period, z_period])) #cols are vectors + +#print('Finding k at 1550nm') +#k, f = bloch.find_k(frequency=1/1550, +# tolerance=(1/1550 - 1/1551), +# direction=[1, 0, 0], +# G_matrix=reciprocal_lattice, +# epsilon=epsilon, +# band=0) +# +#print("k={}, f={}, 1/f={}, k/f={}".format(k, f, 1/f, norm(reciprocal_lattice @ k) / f )) + +print('Finding f at [0.25, 0, 0]') +for k0x in [.25]: + k0 = numpy.array([k0x, 0, 0]) + + kmag = norm(reciprocal_lattice @ k0) + tolerance = (1e6/1550) * 1e-4/1.5 # df = f * dn_eff / n + logger.info('tolerance {}'.format(tolerance)) + + n, v = bloch.eigsolve(4, k0, G_matrix=reciprocal_lattice, epsilon=epsilon, tolerance=tolerance) + v2e = bloch.hmn_2_exyz(k0, G_matrix=reciprocal_lattice, epsilon=epsilon) + v2h = bloch.hmn_2_hxyz(k0, G_matrix=reciprocal_lattice, epsilon=epsilon) + ki = bloch.generate_kmn(k0, reciprocal_lattice, g.shape) + + z = 0 + e = v2e(v[0]) + for i in range(3): + g2.grids[i] += numpy.real(e[i]) + g2.grids[i+3] += numpy.imag(e[i]) + + f = numpy.sqrt(numpy.real(numpy.abs(n))) # TODO + print('k0x = {:3g}\n eigval = {}\n f = {}\n'.format(k0x, n, f)) + n_eff = norm(reciprocal_lattice @ k0) / f + print('kmag/f = n_eff = {} \n wl = {}\n'.format(n_eff, 1/f )) + From 66712efd49d99fd547196933b4846e022461fbea Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 27 Dec 2017 01:44:45 -0800 Subject: [PATCH 057/437] scipy L-BFGS silently converts to float, so view as floats when dealing with it.' --- fdfd_tools/bloch.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 739cc61..b172e21 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -392,7 +392,7 @@ def eigsolve(num_modes: int, onto the space orthonormal to Z. If approx_grad is True, the approximate inverse of the maxwell operator is used to precondition the gradient. """ - z = Z.reshape(y_shape) + z = Z.view(dtype=complex).reshape(y_shape) U = numpy.linalg.inv(z.conj().T @ z) zU = z @ U AzU = scipy_op @ zU @@ -402,26 +402,35 @@ def eigsolve(num_modes: int, df_dy = scipy_iop @ (AzU - zU @ zTAzU) else: df_dy = (AzU - zU @ zTAzU) - return numpy.abs(f), numpy.sign(f) * numpy.real(df_dy).ravel() + + df_dy_flat = df_dy.view(dtype=float).ravel() + return numpy.abs(f), numpy.sign(f) * df_dy_flat ''' Use the conjugate gradient method and the approximate gradient calculation to quickly find approximate eigenvectors. ''' result = scipy.optimize.minimize(rayleigh_quotient, - numpy.random.rand(*y_shape), + numpy.random.rand(*y_shape, 2), jac=True, method='L-BFGS-B', tol=1e-20, options={'maxiter': 2000, 'gtol':0, 'ftol':1e-20 , 'disp':True})#, 'maxls':80, 'm':30}) + result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, True), + result.x, + jac=True, + method='L-BFGS-B', + tol=1e-20, + options={'maxiter': 2000, 'gtol':0, 'disp':True}) + result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, False), result.x, jac=True, method='L-BFGS-B', tol=1e-20, - options={'maxiter': 2000, 'ptol':1e-18, 'disp':True}) + options={'maxiter': 2000, 'gtol':0, 'disp':True}) for i in range(20): result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, False), @@ -429,10 +438,13 @@ def eigsolve(num_modes: int, jac=True, method='L-BFGS-B', tol=1e-20, - options={'maxiter': 70, 'gtol':1e-18, 'disp':True}) + options={'maxiter': 70, 'gtol':0, 'disp':True}) + if result.nit == 0: + # We took 0 steps, so re-running won't help + break - z = result.x.reshape(y_shape) + z = result.x.view(dtype=complex).reshape(y_shape) ''' Recover eigenvectors from Z From 47dd0df8bc919c9289e9a6434eff5f2e2b4b4452 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 6 Jan 2018 13:51:42 -0800 Subject: [PATCH 058/437] fix operator test --- fdfd_tools/eigensolvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/eigensolvers.py b/fdfd_tools/eigensolvers.py index e348cd7..d0ee541 100644 --- a/fdfd_tools/eigensolvers.py +++ b/fdfd_tools/eigensolvers.py @@ -53,7 +53,7 @@ def rayleigh_quotient_iteration(operator: sparse.spmatrix or spalg.LinearOperato :return: (eigenvalue, eigenvector) """ try: - _test = operator - sparse.eye(operator.shape) + _test = operator - sparse.eye(operator.shape[0]) shift = lambda eigval: eigval * sparse.eye(operator.shape[0]) if solver is None: solver = spalg.spsolve From 40677664784477735b7dc3852e2db9c89c58b487 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 8 Jan 2018 16:16:26 -0800 Subject: [PATCH 059/437] use own CG implementation --- examples/bloch.py | 10 +- fdfd_tools/bloch.py | 378 ++++++++++++++++++++++++++++---------------- 2 files changed, 250 insertions(+), 138 deletions(-) diff --git a/examples/bloch.py b/examples/bloch.py index 8bbd30e..793bd89 100644 --- a/examples/bloch.py +++ b/examples/bloch.py @@ -30,11 +30,11 @@ g2.shifts = numpy.zeros((6,3)) g2.grids = [numpy.zeros(g.shape) for _ in range(6)] epsilon = [g.grids[0],] * 3 -reciprocal_lattice = numpy.diag(1e6/numpy.array([x_period, y_period, z_period])) #cols are vectors +reciprocal_lattice = numpy.diag(1000/numpy.array([x_period, y_period, z_period])) #cols are vectors #print('Finding k at 1550nm') -#k, f = bloch.find_k(frequency=1/1550, -# tolerance=(1/1550 - 1/1551), +#k, f = bloch.find_k(frequency=1000/1550, +# tolerance=(1000 * (1/1550 - 1/1551)), # direction=[1, 0, 0], # G_matrix=reciprocal_lattice, # epsilon=epsilon, @@ -47,10 +47,10 @@ for k0x in [.25]: k0 = numpy.array([k0x, 0, 0]) kmag = norm(reciprocal_lattice @ k0) - tolerance = (1e6/1550) * 1e-4/1.5 # df = f * dn_eff / n + tolerance = (1000/1550) * 1e-4/1.5 # df = f * dn_eff / n logger.info('tolerance {}'.format(tolerance)) - n, v = bloch.eigsolve(4, k0, G_matrix=reciprocal_lattice, epsilon=epsilon, tolerance=tolerance) + n, v = bloch.eigsolve(4, k0, G_matrix=reciprocal_lattice, epsilon=epsilon, tolerance=tolerance**2) v2e = bloch.hmn_2_exyz(k0, G_matrix=reciprocal_lattice, epsilon=epsilon) v2h = bloch.hmn_2_hxyz(k0, G_matrix=reciprocal_lattice, epsilon=epsilon) ki = bloch.generate_kmn(k0, reciprocal_lattice, g.shape) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index b172e21..31d364d 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -76,6 +76,7 @@ This module contains functions for generating and solving the from typing import List, Tuple, Callable, Dict import logging import numpy +from numpy import pi, real, trace from numpy.fft import fftn, ifftn, fftfreq import scipy import scipy.optimize @@ -337,139 +338,6 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, return operator -def eigsolve(num_modes: int, - k0: numpy.ndarray, - G_matrix: numpy.ndarray, - epsilon: field_t, - mu: field_t = None, - tolerance = 1e-8, - ) -> Tuple[numpy.ndarray, numpy.ndarray]: - """ - Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector - k0 of the specified structure. - - :param k0: Bloch wavevector, [k0x, k0y, k0z]. - :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. - :param epsilon: Dielectric constant distribution for the simulation. - All fields are sampled at cell centers (i.e., NOT Yee-gridded) - :param mu: Magnetic permability distribution for the simulation. - Default None (1 everywhere). - :return: (eigenvalues, eigenvectors) where eigenvalues[i] corresponds to the - vector eigenvectors[i, :] - """ - h_size = 2 * epsilon[0].size - - kmag = norm(G_matrix @ k0) - - ''' - Generate the operators - ''' - mop = maxwell_operator(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) - imop = inverse_maxwell_operator_approx(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) - - scipy_op = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=mop) - scipy_iop = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=imop) - - y_shape = (h_size, num_modes) - - def rayleigh_quotient(Z: numpy.ndarray, approx_grad: bool = True): - """ - Absolute value of the block Rayleigh quotient, and the associated gradient. - - See Johnson and Joannopoulos, Opt. Expr. 8, 3 (2001) for details (full - citation in module docstring). - - === - - Notes on my understanding of the procedure: - - Minimize f(Y) = |trace((Y.H @ A @ Y)|, making use of Y = Z @ inv(Z.H @ Z)^(1/2) - (a polar orthogonalization of Y). This gives f(Z) = |trace(Z.H @ A @ Z @ U)|, - where U = inv(Z.H @ Z). We minimize the absolute value to find the eigenvalues - with smallest magnitude. - - The gradient is P @ (A @ Z @ U), where P = (1 - Z @ U @ Z.H) is a projection - onto the space orthonormal to Z. If approx_grad is True, the approximate - inverse of the maxwell operator is used to precondition the gradient. - """ - z = Z.view(dtype=complex).reshape(y_shape) - U = numpy.linalg.inv(z.conj().T @ z) - zU = z @ U - AzU = scipy_op @ zU - zTAzU = z.conj().T @ AzU - f = numpy.real(numpy.trace(zTAzU)) - if approx_grad: - df_dy = scipy_iop @ (AzU - zU @ zTAzU) - else: - df_dy = (AzU - zU @ zTAzU) - - df_dy_flat = df_dy.view(dtype=float).ravel() - return numpy.abs(f), numpy.sign(f) * df_dy_flat - - ''' - Use the conjugate gradient method and the approximate gradient calculation to - quickly find approximate eigenvectors. - ''' - result = scipy.optimize.minimize(rayleigh_quotient, - numpy.random.rand(*y_shape, 2), - jac=True, - method='L-BFGS-B', - tol=1e-20, - options={'maxiter': 2000, 'gtol':0, 'ftol':1e-20 , 'disp':True})#, 'maxls':80, 'm':30}) - - - result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, True), - result.x, - jac=True, - method='L-BFGS-B', - tol=1e-20, - options={'maxiter': 2000, 'gtol':0, 'disp':True}) - - result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, False), - result.x, - jac=True, - method='L-BFGS-B', - tol=1e-20, - options={'maxiter': 2000, 'gtol':0, 'disp':True}) - - for i in range(20): - result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, False), - result.x, - jac=True, - method='L-BFGS-B', - tol=1e-20, - options={'maxiter': 70, 'gtol':0, 'disp':True}) - if result.nit == 0: - # We took 0 steps, so re-running won't help - break - - - z = result.x.view(dtype=complex).reshape(y_shape) - - ''' - Recover eigenvectors from Z - ''' - U = numpy.linalg.inv(z.conj().T @ z) - y = z @ scipy.linalg.sqrtm(U) - w = y.conj().T @ (scipy_op @ y) - - eigvals, w_eigvecs = numpy.linalg.eig(w) - eigvecs = y @ w_eigvecs - - for i in range(len(eigvals)): - v = eigvecs[:, i] - n = eigvals[i] - v /= norm(v) - eigness = norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v ) - f = numpy.sqrt(-numpy.real(n)) - df = numpy.sqrt(-numpy.real(n + eigness)) - neff_err = kmag * (1/df - 1/f) - logger.info('eigness {}: {}\n neff_err: {}'.format(i, eigness, neff_err)) - - order = numpy.argsort(numpy.abs(eigvals)) - return eigvals[order], eigvecs.T[order] - - def find_k(frequency: float, tolerance: float, direction: numpy.ndarray, @@ -511,3 +379,247 @@ def find_k(frequency: float, return res.x * direction, res.fun + frequency + +def eigsolve(num_modes: int, + k0: numpy.ndarray, + G_matrix: numpy.ndarray, + epsilon: field_t, + mu: field_t = None, + tolerance = 1e-20, + ) -> Tuple[numpy.ndarray, numpy.ndarray]: + """ + Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector + k0 of the specified structure. + + :param k0: Bloch wavevector, [k0x, k0y, k0z]. + :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + :param epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + :param mu: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + :param tolerance: Solver stops when fractional change in the objective + trace(Z.H @ A @ Z @ inv(Z Z.H)) is smaller than the tolerance + :return: (eigenvalues, eigenvectors) where eigenvalues[i] corresponds to the + vector eigenvectors[i, :] + """ + h_size = 2 * epsilon[0].size + + kmag = norm(G_matrix @ k0) + + ''' + Generate the operators + ''' + mop = maxwell_operator(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) + imop = inverse_maxwell_operator_approx(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) + + scipy_op = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=mop) + scipy_iop = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=imop) + + y_shape = (h_size, num_modes) + + prev_E = 0 + d_scale = 1 + prev_traceGtKG = 0 + prev_theta = 0.5 + D = numpy.zeros(shape=y_shape, dtype=complex) + + y0 = None + if y0 is None: + Z = numpy.random.rand(*y_shape).astype(complex) + else: + Z = y0 + + while True: + Z2 = Z.conj().T @ Z + Z_norm = numpy.sqrt(real(trace(Z2))) / num_modes + Z /= Z_norm + Z2 /= Z_norm * Z_norm + try: + U = numpy.linalg.inv(Z2) + except numpy.linalg.LinAlgError: + Z = numpy.random.rand(*y_shape).astype(complex) + continue + + trace_U = real(trace(U)) + if trace_U > 1e8 * num_modes: + Z = Z @ scipy.linalg.sqrtm(U).conj().T + prev_traceGtKG = 0 + continue + break + + def rtrace_AtB(A, B): + return real(numpy.sum(A.conj() * B)) + + def symmetrize(A): + return (A + A.conj().T) * 0.5 + + max_iters = 10000 + for iter in range(max_iters): + U = numpy.linalg.inv(Z.conj().T @ Z) + AZ = scipy_op @ Z + AZU = AZ @ U + ZtAZU = Z.conj().T @ AZU + E = real(trace(ZtAZU)) + sgn = numpy.sign(E) + E = numpy.abs(E) + G = (AZU - Z @ U @ ZtAZU) * sgn + + if iter > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7): + logging.info('Optimization succeded: {} - 5e-8 < {} * {} / 2'.format(abs(E - prev_E), tolerance, E + prev_E)) + break + + KG = scipy_iop @ G + traceGtKG = rtrace_AtB(G, KG) + gamma_numerator = traceGtKG + + reset_iters = 100 + if prev_traceGtKG == 0 or iter % reset_iters == 0: + print('RESET!') + gamma = 0 + else: + gamma = gamma_numerator / prev_traceGtKG + + D = gamma * d_scale * D + KG + d_scale = numpy.sqrt(rtrace_AtB(D, D)) / num_modes + D /= d_scale + + AD = scipy_op @ D + DtD = D.conj().T @ D + DtAD = D.conj().T @ AD + + ZtD = Z.conj().T @ D + ZtAD = Z.conj().T @ AD + symZtD = symmetrize(ZtD) + symZtAD = symmetrize(ZtAD) + + U_sZtD = U @ symZtD + + dE = 2.0 * (rtrace_AtB(U, symZtAD) - rtrace_AtB(ZtAZU, U_sZtD)) + + S2 = DtD - 4 * symZtD @ U_sZtD + d2E = 2 * (rtrace_AtB(U, DtAD) - + rtrace_AtB(ZtAZU, U @ S2) - + 4 * rtrace_AtB(U, symZtAD @ U_sZtD)) + + # Newton-Raphson to find a root of the first derivative: + theta = -dE/d2E + + if d2E < 0 or abs(theta) >= pi: + theta = -abs(prev_theta) * numpy.sign(dE) + + # ZtAZU * ZtZ = ZtAZ for use in line search + ZtZ = Z.conj().T @ Z + ZtAZ = ZtAZU @ ZtZ.conj().T + + def Qi_func(theta, memo=[None, None]): + if memo[0] == theta: + return memo[1] + + c = numpy.cos(theta) + s = numpy.sin(theta) + Q = c*c * ZtZ + s*s * DtD + 2*s*c * symZtD + try: + Qi = numpy.linalg.inv(Q) + except numpy.linalg.LinAlgError: + logger.info('taylor Qi') + # if c or s small, taylor expand + if c < 1e-4 * s and c != 0: + Qi = numpy.linalg.inv(DtD) + Qi = Qi / (s*s) - 2*c/(s*s*s) * (Qi @ symZtD.conj().T @ Qi.conj().T) + elif s < 1e-4 * c and s != 0: + Qi = numpy.linalg.inv(ZtZ) + Qi = Qi / (c*c) - 2*s/(c*c*c) * (Qi @ symZtD.conj().T @ Qi.conj().T) + else: + raise Exception('Inexplicable singularity in trace_func') + memo[0] = theta + memo[1] = Qi + return Qi + + def trace_func(theta): + c = numpy.cos(theta) + s = numpy.sin(theta) + Qi = Qi_func(theta) + R = c*c * ZtAZ + s*s * DtAD + 2*s*c * symZtAD + trace = rtrace_AtB(R, Qi) + return numpy.abs(trace) + + #def trace_deriv(theta): + # Qi = Qi_func(theta) + # c2 = numpy.cos(2 * theta) + # s2 = numpy.sin(2 * theta) + # F = -0.5*s2 * (ZtAZ - DtAD) + c2 * symZtAD + # trace_deriv = rtrace_AtB(Qi, F) + + # G = Qi @ F.conj().T @ Qi.conj().T + # H = -0.5*s2 * (ZtZ - DtD) + c2 * symZtD + # trace_deriv -= rtrace_AtB(G, H) + + # trace_deriv *= 2 + # return trace_deriv * sgn + + ''' + theta, new_E, new_dE = linmin(theta, E, dE, 0.1, min(tolerance, 1e-6), 1e-14, 0, -numpy.sign(dE) * K_PI, trace_func) + ''' + #theta, n, _, new_E, _, _new_dE = scipy.optimize.line_search(trace_func, trace_deriv, xk=theta, pk=numpy.ones((1,1)), gfk=dE, old_fval=E, c1=min(tolerance, 1e-6), c2=0.1, amax=pi) + result = scipy.optimize.minimize_scalar(trace_func, bounds=(0, pi), tol=tolerance) + new_E = result.fun + theta = result.x + + improvement = numpy.abs(E - new_E) * 2 / numpy.abs(E + new_E) + logger.info('linmin improvement {}'.format(improvement)) + Z *= numpy.cos(theta) + Z += D * numpy.sin(theta) + + prev_traceGtKG = traceGtKG + prev_theta = theta + prev_E = E + + ''' + Recover eigenvectors from Z + ''' + U = numpy.linalg.inv(Z.conj().T @ Z) + Y = Z @ scipy.linalg.sqrtm(U) + W = Y.conj().T @ (scipy_op @ Y) + + eigvals, W_eigvecs = numpy.linalg.eig(W) + eigvecs = Y @ W_eigvecs + + for i in range(len(eigvals)): + v = eigvecs[:, i] + n = eigvals[i] + v /= norm(v) + eigness = norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v ) + f = numpy.sqrt(-numpy.real(n)) + df = numpy.sqrt(-numpy.real(n + eigness)) + neff_err = kmag * (1/df - 1/f) + logger.info('eigness {}: {}\n neff_err: {}'.format(i, eigness, neff_err)) + + order = numpy.argsort(numpy.abs(eigvals)) + return eigvals[order], eigvecs.T[order] + + #def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): + # if df0 > 0: + # x0, f0, df0 = linmin(-x_guess, f0, -df0, -x_max, f_tol, df_tol, x_tol, -x_min, lambda q, dq: -linmin_func(q, dq)) + # return -x0, f0, -df0 + # elif df0 == 0: + # return 0, f0, df0 + # else: + # x = x_guess + # fx = f0 + # dfx = df0 + + # isave = numpy.zeros((2,), numpy.intc) + # dsave = numpy.zeros((13,), float) + + # x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, + # x_min, x_max, isave, dsave) + # for i in range(int(1e6)): + # if task != 'F': + # logging.info('search converged in {} iterations'.format(i)) + # break + # fx = f(x, dfx) + # x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, + # x_min, x_max, isave, dsave) + + # return x, fx, dfx + From c4cbdff751c19716b788c582ac6d4df0af5d3576 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 8 Jan 2018 23:28:57 -0800 Subject: [PATCH 060/437] cleanup --- fdfd_tools/bloch.py | 125 ++++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 31d364d..de070c0 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -447,15 +447,10 @@ def eigsolve(num_modes: int, continue break - def rtrace_AtB(A, B): - return real(numpy.sum(A.conj() * B)) - - def symmetrize(A): - return (A + A.conj().T) * 0.5 - max_iters = 10000 for iter in range(max_iters): - U = numpy.linalg.inv(Z.conj().T @ Z) + ZtZ = Z.conj().T @ Z + U = numpy.linalg.inv(ZtZ) AZ = scipy_op @ Z AZU = AZ @ U ZtAZU = Z.conj().T @ AZU @@ -469,47 +464,44 @@ def eigsolve(num_modes: int, break KG = scipy_iop @ G - traceGtKG = rtrace_AtB(G, KG) - gamma_numerator = traceGtKG + traceGtKG = _rtrace_AtB(G, KG) - reset_iters = 100 + reset_iters = 100 # TODO if prev_traceGtKG == 0 or iter % reset_iters == 0: - print('RESET!') + logger.inf('CG reset') gamma = 0 else: - gamma = gamma_numerator / prev_traceGtKG + gamma = traceGtKG / prev_traceGtKG D = gamma * d_scale * D + KG - d_scale = numpy.sqrt(rtrace_AtB(D, D)) / num_modes + d_scale = numpy.sqrt(_rtrace_AtB(D, D)) / num_modes D /= d_scale + ZtAZ = Z.conj().T @ AZ + AD = scipy_op @ D DtD = D.conj().T @ D DtAD = D.conj().T @ AD - ZtD = Z.conj().T @ D - ZtAD = Z.conj().T @ AD - symZtD = symmetrize(ZtD) - symZtAD = symmetrize(ZtAD) + symZtD = _symmetrize(Z.conj().T @ D) + symZtAD = _symmetrize(Z.conj().T @ AD) + ''' U_sZtD = U @ symZtD - dE = 2.0 * (rtrace_AtB(U, symZtAD) - rtrace_AtB(ZtAZU, U_sZtD)) + dE = 2.0 * (_rtrace_AtB(U, symZtAD) - + _rtrace_AtB(ZtAZU, U_sZtD)) - S2 = DtD - 4 * symZtD @ U_sZtD - d2E = 2 * (rtrace_AtB(U, DtAD) - - rtrace_AtB(ZtAZU, U @ S2) - - 4 * rtrace_AtB(U, symZtAD @ U_sZtD)) + d2E = 2 * (_rtrace_AtB(U, DtAD) - + _rtrace_AtB(ZtAZU, U @ (DtD - 4 * symZtD @ U_sZtD)) - + 4 * _rtrace_AtB(U, symZtAD @ U_sZtD)) # Newton-Raphson to find a root of the first derivative: theta = -dE/d2E if d2E < 0 or abs(theta) >= pi: theta = -abs(prev_theta) * numpy.sign(dE) - - # ZtAZU * ZtZ = ZtAZ for use in line search - ZtZ = Z.conj().T @ Z - ZtAZ = ZtAZU @ ZtZ.conj().T + ''' def Qi_func(theta, memo=[None, None]): if memo[0] == theta: @@ -525,10 +517,10 @@ def eigsolve(num_modes: int, # if c or s small, taylor expand if c < 1e-4 * s and c != 0: Qi = numpy.linalg.inv(DtD) - Qi = Qi / (s*s) - 2*c/(s*s*s) * (Qi @ symZtD.conj().T @ Qi.conj().T) + Qi = Qi / (s*s) - 2*c/(s*s*s) * (Qi @ (Qi @ symZtD).conj().T) elif s < 1e-4 * c and s != 0: Qi = numpy.linalg.inv(ZtZ) - Qi = Qi / (c*c) - 2*s/(c*c*c) * (Qi @ symZtD.conj().T @ Qi.conj().T) + Qi = Qi / (c*c) - 2*s/(c*c*c) * (Qi @ (Qi @ symZtD).conj().T) else: raise Exception('Inexplicable singularity in trace_func') memo[0] = theta @@ -540,22 +532,24 @@ def eigsolve(num_modes: int, s = numpy.sin(theta) Qi = Qi_func(theta) R = c*c * ZtAZ + s*s * DtAD + 2*s*c * symZtAD - trace = rtrace_AtB(R, Qi) + trace = _rtrace_AtB(R, Qi) return numpy.abs(trace) - #def trace_deriv(theta): - # Qi = Qi_func(theta) - # c2 = numpy.cos(2 * theta) - # s2 = numpy.sin(2 * theta) - # F = -0.5*s2 * (ZtAZ - DtAD) + c2 * symZtAD - # trace_deriv = rtrace_AtB(Qi, F) + ''' + def trace_deriv(theta): + Qi = Qi_func(theta) + c2 = numpy.cos(2 * theta) + s2 = numpy.sin(2 * theta) + F = -0.5*s2 * (ZtAZ - DtAD) + c2 * symZtAD + trace_deriv = _rtrace_AtB(Qi, F) - # G = Qi @ F.conj().T @ Qi.conj().T - # H = -0.5*s2 * (ZtZ - DtD) + c2 * symZtD - # trace_deriv -= rtrace_AtB(G, H) + G = Qi @ F.conj().T @ Qi.conj().T + H = -0.5*s2 * (ZtZ - DtD) + c2 * symZtD + trace_deriv -= _rtrace_AtB(G, H) - # trace_deriv *= 2 - # return trace_deriv * sgn + trace_deriv *= 2 + return trace_deriv * sgn + ''' ''' theta, new_E, new_dE = linmin(theta, E, dE, 0.1, min(tolerance, 1e-6), 1e-14, 0, -numpy.sign(dE) * K_PI, trace_func) @@ -597,29 +591,36 @@ def eigsolve(num_modes: int, order = numpy.argsort(numpy.abs(eigvals)) return eigvals[order], eigvecs.T[order] - #def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): - # if df0 > 0: - # x0, f0, df0 = linmin(-x_guess, f0, -df0, -x_max, f_tol, df_tol, x_tol, -x_min, lambda q, dq: -linmin_func(q, dq)) - # return -x0, f0, -df0 - # elif df0 == 0: - # return 0, f0, df0 - # else: - # x = x_guess - # fx = f0 - # dfx = df0 +#def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): +# if df0 > 0: +# x0, f0, df0 = linmin(-x_guess, f0, -df0, -x_max, f_tol, df_tol, x_tol, -x_min, lambda q, dq: -linmin_func(q, dq)) +# return -x0, f0, -df0 +# elif df0 == 0: +# return 0, f0, df0 +# else: +# x = x_guess +# fx = f0 +# dfx = df0 - # isave = numpy.zeros((2,), numpy.intc) - # dsave = numpy.zeros((13,), float) +# isave = numpy.zeros((2,), numpy.intc) +# dsave = numpy.zeros((13,), float) - # x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, - # x_min, x_max, isave, dsave) - # for i in range(int(1e6)): - # if task != 'F': - # logging.info('search converged in {} iterations'.format(i)) - # break - # fx = f(x, dfx) - # x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, - # x_min, x_max, isave, dsave) +# x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, +# x_min, x_max, isave, dsave) +# for i in range(int(1e6)): +# if task != 'F': +# logging.info('search converged in {} iterations'.format(i)) +# break +# fx = f(x, dfx) +# x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, +# x_min, x_max, isave, dsave) - # return x, fx, dfx +# return x, fx, dfx + + +def _rtrace_AtB(A, B): + return real(numpy.sum(A.conj() * B)) + +def _symmetrize(A): + return (A + A.conj().T) * 0.5 From e02040c7093de5894ad1857a7fae5569d1bfcaee Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 8 Jan 2018 23:33:22 -0800 Subject: [PATCH 061/437] fixes and clarification --- fdfd_tools/bloch.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index de070c0..ea9e760 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -385,7 +385,9 @@ def eigsolve(num_modes: int, G_matrix: numpy.ndarray, epsilon: field_t, mu: field_t = None, - tolerance = 1e-20, + tolerance: float = 1e-20, + max_iters: int = 10000, + reset_iters: int = 100, ) -> Tuple[numpy.ndarray, numpy.ndarray]: """ Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector @@ -447,16 +449,15 @@ def eigsolve(num_modes: int, continue break - max_iters = 10000 for iter in range(max_iters): ZtZ = Z.conj().T @ Z U = numpy.linalg.inv(ZtZ) AZ = scipy_op @ Z AZU = AZ @ U ZtAZU = Z.conj().T @ AZU - E = real(trace(ZtAZU)) - sgn = numpy.sign(E) - E = numpy.abs(E) + E_signed = real(trace(ZtAZU)) + sgn = numpy.sign(E_signed) + E = numpy.abs(E_signed) G = (AZU - Z @ U @ ZtAZU) * sgn if iter > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7): @@ -466,9 +467,8 @@ def eigsolve(num_modes: int, KG = scipy_iop @ G traceGtKG = _rtrace_AtB(G, KG) - reset_iters = 100 # TODO if prev_traceGtKG == 0 or iter % reset_iters == 0: - logger.inf('CG reset') + logger.info('CG reset') gamma = 0 else: gamma = traceGtKG / prev_traceGtKG From 0e47fdd5fb97da65bc211b83ce1c3f8cabf3b985 Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 9 Jan 2018 00:00:58 -0800 Subject: [PATCH 062/437] randomize imaginary part of starting vector --- fdfd_tools/bloch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index ea9e760..168a349 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -422,24 +422,24 @@ def eigsolve(num_modes: int, prev_E = 0 d_scale = 1 prev_traceGtKG = 0 - prev_theta = 0.5 + #prev_theta = 0.5 D = numpy.zeros(shape=y_shape, dtype=complex) y0 = None if y0 is None: - Z = numpy.random.rand(*y_shape).astype(complex) + Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape) else: Z = y0 while True: - Z2 = Z.conj().T @ Z - Z_norm = numpy.sqrt(real(trace(Z2))) / num_modes + ZtZ = Z.conj().T @ Z + Z_norm = numpy.sqrt(real(trace(ZtZ))) / num_modes Z /= Z_norm - Z2 /= Z_norm * Z_norm + ZtZ /= Z_norm * Z_norm try: - U = numpy.linalg.inv(Z2) + U = numpy.linalg.inv(ZtZ) except numpy.linalg.LinAlgError: - Z = numpy.random.rand(*y_shape).astype(complex) + Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape) continue trace_U = real(trace(U)) @@ -565,7 +565,7 @@ def eigsolve(num_modes: int, Z += D * numpy.sin(theta) prev_traceGtKG = traceGtKG - prev_theta = theta + #prev_theta = theta prev_E = E ''' From e8f836c908996fa80f866174f3ff7a1dc7ebb8db Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 22:43:33 -0800 Subject: [PATCH 063/437] Cleanup --- fdfd_tools/bloch.py | 106 +++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 168a349..1816bdc 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -73,7 +73,7 @@ This module contains functions for generating and solving the ''' -from typing import List, Tuple, Callable, Dict +from typing import Tuple, Callable import logging import numpy from numpy import pi, real, trace @@ -83,7 +83,6 @@ import scipy.optimize from scipy.linalg import norm import scipy.sparse.linalg as spalg -from .eigensolvers import rayleigh_quotient_iteration from . import field_t logger = logging.getLogger(__name__) @@ -256,7 +255,7 @@ def hmn_2_hxyz(k0: numpy.ndarray, :return: Function for converting h_mn into H_xyz """ shape = epsilon[0].shape + (1,) - k_mag, m, n = generate_kmn(k0, G_matrix, shape) + _k_mag, m, n = generate_kmn(k0, G_matrix, shape) def operator(h: numpy.ndarray): hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] @@ -379,7 +378,6 @@ def find_k(frequency: float, return res.x * direction, res.fun + frequency - def eigsolve(num_modes: int, k0: numpy.ndarray, G_matrix: numpy.ndarray, @@ -432,10 +430,8 @@ def eigsolve(num_modes: int, Z = y0 while True: + Z *= num_modes / norm(Z) ZtZ = Z.conj().T @ Z - Z_norm = numpy.sqrt(real(trace(ZtZ))) / num_modes - Z /= Z_norm - ZtZ /= Z_norm * Z_norm try: U = numpy.linalg.inv(ZtZ) except numpy.linalg.LinAlgError: @@ -449,7 +445,7 @@ def eigsolve(num_modes: int, continue break - for iter in range(max_iters): + for i in range(max_iters): ZtZ = Z.conj().T @ Z U = numpy.linalg.inv(ZtZ) AZ = scipy_op @ Z @@ -460,22 +456,22 @@ def eigsolve(num_modes: int, E = numpy.abs(E_signed) G = (AZU - Z @ U @ ZtAZU) * sgn - if iter > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7): + if i > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7): logging.info('Optimization succeded: {} - 5e-8 < {} * {} / 2'.format(abs(E - prev_E), tolerance, E + prev_E)) break KG = scipy_iop @ G traceGtKG = _rtrace_AtB(G, KG) - if prev_traceGtKG == 0 or iter % reset_iters == 0: + if prev_traceGtKG == 0 or i % reset_iters == 0: logger.info('CG reset') gamma = 0 else: gamma = traceGtKG / prev_traceGtKG - D = gamma * d_scale * D + KG - d_scale = numpy.sqrt(_rtrace_AtB(D, D)) / num_modes - D /= d_scale + D = gamma / d_scale * D + KG + d_scale = num_modes / norm(D) + D *= d_scale ZtAZ = Z.conj().T @ AZ @@ -486,22 +482,6 @@ def eigsolve(num_modes: int, symZtD = _symmetrize(Z.conj().T @ D) symZtAD = _symmetrize(Z.conj().T @ AD) - ''' - U_sZtD = U @ symZtD - - dE = 2.0 * (_rtrace_AtB(U, symZtAD) - - _rtrace_AtB(ZtAZU, U_sZtD)) - - d2E = 2 * (_rtrace_AtB(U, DtAD) - - _rtrace_AtB(ZtAZU, U @ (DtD - 4 * symZtD @ U_sZtD)) - - 4 * _rtrace_AtB(U, symZtAD @ U_sZtD)) - - # Newton-Raphson to find a root of the first derivative: - theta = -dE/d2E - - if d2E < 0 or abs(theta) >= pi: - theta = -abs(prev_theta) * numpy.sign(dE) - ''' def Qi_func(theta, memo=[None, None]): if memo[0] == theta: @@ -549,12 +529,25 @@ def eigsolve(num_modes: int, trace_deriv *= 2 return trace_deriv * sgn - ''' + U_sZtD = U @ symZtD + + dE = 2.0 * (_rtrace_AtB(U, symZtAD) - + _rtrace_AtB(ZtAZU, U_sZtD)) + + d2E = 2 * (_rtrace_AtB(U, DtAD) - + _rtrace_AtB(ZtAZU, U @ (DtD - 4 * symZtD @ U_sZtD)) - + 4 * _rtrace_AtB(U, symZtAD @ U_sZtD)) + + # Newton-Raphson to find a root of the first derivative: + theta = -dE/d2E + + if d2E < 0 or abs(theta) >= pi: + theta = -abs(prev_theta) * numpy.sign(dE) + + # theta, new_E, new_dE = linmin(theta, E, dE, 0.1, min(tolerance, 1e-6), 1e-14, 0, -numpy.sign(dE) * K_PI, trace_func) + theta, n, _, new_E, _, _new_dE = scipy.optimize.line_search(trace_func, trace_deriv, xk=theta, pk=numpy.ones((1,1)), gfk=dE, old_fval=E, c1=min(tolerance, 1e-6), c2=0.1, amax=pi) ''' - theta, new_E, new_dE = linmin(theta, E, dE, 0.1, min(tolerance, 1e-6), 1e-14, 0, -numpy.sign(dE) * K_PI, trace_func) - ''' - #theta, n, _, new_E, _, _new_dE = scipy.optimize.line_search(trace_func, trace_deriv, xk=theta, pk=numpy.ones((1,1)), gfk=dE, old_fval=E, c1=min(tolerance, 1e-6), c2=0.1, amax=pi) result = scipy.optimize.minimize_scalar(trace_func, bounds=(0, pi), tol=tolerance) new_E = result.fun theta = result.x @@ -591,32 +584,33 @@ def eigsolve(num_modes: int, order = numpy.argsort(numpy.abs(eigvals)) return eigvals[order], eigvecs.T[order] -#def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): -# if df0 > 0: -# x0, f0, df0 = linmin(-x_guess, f0, -df0, -x_max, f_tol, df_tol, x_tol, -x_min, lambda q, dq: -linmin_func(q, dq)) -# return -x0, f0, -df0 -# elif df0 == 0: -# return 0, f0, df0 -# else: -# x = x_guess -# fx = f0 -# dfx = df0 +''' +def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): + if df0 > 0: + x0, f0, df0 = linmin(-x_guess, f0, -df0, -x_max, f_tol, df_tol, x_tol, -x_min, lambda q, dq: -linmin_func(q, dq)) + return -x0, f0, -df0 + elif df0 == 0: + return 0, f0, df0 + else: + x = x_guess + fx = f0 + dfx = df0 -# isave = numpy.zeros((2,), numpy.intc) -# dsave = numpy.zeros((13,), float) + isave = numpy.zeros((2,), numpy.intc) + dsave = numpy.zeros((13,), float) -# x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, -# x_min, x_max, isave, dsave) -# for i in range(int(1e6)): -# if task != 'F': -# logging.info('search converged in {} iterations'.format(i)) -# break -# fx = f(x, dfx) -# x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, -# x_min, x_max, isave, dsave) - -# return x, fx, dfx + x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, + x_min, x_max, isave, dsave) + for i in range(int(1e6)): + if task != 'F': + logging.info('search converged in {} iterations'.format(i)) + break + fx = f(x, dfx) + x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, + x_min, x_max, isave, dsave) + return x, fx, dfx +''' def _rtrace_AtB(A, B): return real(numpy.sum(A.conj() * B)) From c1f65f61c1db4d45a5d2df0233b196aa028a23e1 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 22:43:59 -0800 Subject: [PATCH 064/437] Use pyfftw if available --- fdfd_tools/bloch.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 1816bdc..fd5f758 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -77,7 +77,7 @@ from typing import Tuple, Callable import logging import numpy from numpy import pi, real, trace -from numpy.fft import fftn, ifftn, fftfreq +from numpy.fft import fftfreq import scipy import scipy.optimize from scipy.linalg import norm @@ -88,6 +88,29 @@ from . import field_t logger = logging.getLogger(__name__) +try: + import pyfftw.interfaces.numpy_fft + import pyfftw.interfaces + import multiprocessing + + pyfftw.interfaces.cache.enable() + pyfftw.interfaces.cache.set_keepalive_time(3600) + fftw_args = { + 'threads': multiprocessing.cpu_count(), + 'overwrite_input': True, + 'planner_effort': 'FFTW_EXHAUSTIVE', + } + + def fftn(*args, **kwargs): + return pyfftw.interfaces.numpy_fft.fftn(*args, **kwargs, **fftw_args) + + def ifftn(*args, **kwargs): + return pyfftw.interfaces.numpy_fft.ifftn(*args, **kwargs, **fftw_args) + +except ImportError: + from numpy.fft import fftn, ifftn + + def generate_kmn(k0: numpy.ndarray, G_matrix: numpy.ndarray, shape: numpy.ndarray From ee9abb77d9f42e79074357e95aee960e7588a566 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 22:44:14 -0800 Subject: [PATCH 065/437] Fix approx_inverse operator --- fdfd_tools/bloch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index fd5f758..aabc96c 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -352,8 +352,8 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, d_xyz = fftn(ifftn(e_xyz, axes=range(3)) * epsilon, axes=range(3)) # cross product and transform into mn basis crossinv_t2c - h_m = numpy.sum(e_xyz * n, axis=3)[:, :, :, None] / +k_mag - h_n = numpy.sum(e_xyz * m, axis=3)[:, :, :, None] / -k_mag + h_m = numpy.sum(d_xyz * n, axis=3)[:, :, :, None] / +k_mag + h_n = numpy.sum(d_xyz * m, axis=3)[:, :, :, None] / -k_mag return numpy.hstack((h_m.ravel(), h_n.ravel())) From 323bcf88ad224f5a77df7f4e3d4c25e46055ffcf Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 22:44:26 -0800 Subject: [PATCH 066/437] Propagate mu correctly --- fdfd_tools/bloch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index aabc96c..cbfdf1c 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -389,7 +389,7 @@ def find_k(frequency: float, def get_f(k0_mag: float, band: int = 0): k0 = direction * k0_mag - n, _v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon) + n, _v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) f = numpy.sqrt(numpy.abs(numpy.real(n[band]))) return f From 1f9a9949c06ff33a836b05e2b18f18b1495e39fc Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 22:44:59 -0800 Subject: [PATCH 067/437] Clarify memo and cleanup --- fdfd_tools/bloch.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index cbfdf1c..002234f 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -505,10 +505,11 @@ def eigsolve(num_modes: int, symZtD = _symmetrize(Z.conj().T @ D) symZtAD = _symmetrize(Z.conj().T @ AD) - - def Qi_func(theta, memo=[None, None]): - if memo[0] == theta: - return memo[1] + Qi_memo = [None, None] + def Qi_func(theta): + nonlocal Qi_memo + if Qi_memo[0] == theta: + return Qi_memo[1] c = numpy.cos(theta) s = numpy.sin(theta) @@ -519,15 +520,15 @@ def eigsolve(num_modes: int, logger.info('taylor Qi') # if c or s small, taylor expand if c < 1e-4 * s and c != 0: - Qi = numpy.linalg.inv(DtD) - Qi = Qi / (s*s) - 2*c/(s*s*s) * (Qi @ (Qi @ symZtD).conj().T) + DtDi = numpy.linalg.inv(DtD) + Qi = DtDi / (s*s) - 2*c/(s*s*s) * (DtDi @ (DtDi @ symZtD).conj().T) elif s < 1e-4 * c and s != 0: - Qi = numpy.linalg.inv(ZtZ) - Qi = Qi / (c*c) - 2*s/(c*c*c) * (Qi @ (Qi @ symZtD).conj().T) + ZtZi = numpy.linalg.inv(ZtZ) + Qi = ZtZi / (c*c) - 2*s/(c*c*c) * (ZtZi @ (ZtZi @ symZtD).conj().T) else: raise Exception('Inexplicable singularity in trace_func') - memo[0] = theta - memo[1] = Qi + Qi_memo[0] = theta + Qi_memo[1] = Qi return Qi def trace_func(theta): From c7d4c4a8e6096aa79acc2f9c3003ccd02340224a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:07:13 -0700 Subject: [PATCH 068/437] Add callback for block mode solve progress --- fdfd_tools/bloch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 002234f..598aa5d 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -369,6 +369,7 @@ def find_k(frequency: float, band: int = 0, k_min: float = 0, k_max: float = 0.5, + solve_callback: Callable = None ) -> Tuple[numpy.ndarray, float]: """ Search for a bloch vector that has a given frequency. @@ -389,8 +390,10 @@ def find_k(frequency: float, def get_f(k0_mag: float, band: int = 0): k0 = direction * k0_mag - n, _v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) + n, v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) f = numpy.sqrt(numpy.abs(numpy.real(n[band]))) + if solve_callback: + solve_callback(k0_mag, n, v, f) return f res = scipy.optimize.minimize_scalar(lambda x: abs(get_f(x, band) - frequency), From 41cd94fe48fdd1521c1e833e49e43e916e07bd82 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:07:44 -0700 Subject: [PATCH 069/437] More detailed logging --- fdfd_tools/bloch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 598aa5d..9c252e7 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -92,6 +92,7 @@ try: import pyfftw.interfaces.numpy_fft import pyfftw.interfaces import multiprocessing + logger.info('Using pyfftw') pyfftw.interfaces.cache.enable() pyfftw.interfaces.cache.set_keepalive_time(3600) @@ -109,6 +110,7 @@ try: except ImportError: from numpy.fft import fftn, ifftn + logger.info('Using numpy fft') def generate_kmn(k0: numpy.ndarray, @@ -483,7 +485,7 @@ def eigsolve(num_modes: int, G = (AZU - Z @ U @ ZtAZU) * sgn if i > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7): - logging.info('Optimization succeded: {} - 5e-8 < {} * {} / 2'.format(abs(E - prev_E), tolerance, E + prev_E)) + logger.info('Optimization succeded: {} - 5e-8 < {} * {} / 2'.format(abs(E - prev_E), tolerance, E + prev_E)) break KG = scipy_iop @ G From 001c32a2e0b85c327e36c5bbb62b283c4ba11f10 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:08:33 -0700 Subject: [PATCH 070/437] Partially fix arbitrary mode phase --- fdfd_tools/waveguide.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/waveguide.py b/fdfd_tools/waveguide.py index 1566a74..89e0d9c 100644 --- a/fdfd_tools/waveguide.py +++ b/fdfd_tools/waveguide.py @@ -112,9 +112,16 @@ def normalized_fields(v: numpy.ndarray, P = 0.5 * numpy.real(S.sum()) assert P > 0, 'Found a mode propagating in the wrong direction! P={}'.format(P) + energy = epsilon * e.conj() * e + norm_amplitude = 1 / numpy.sqrt(P) - norm_angle = -numpy.angle(e[e.size//2]) - norm_factor = norm_amplitude * numpy.exp(1j * norm_angle) + norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric + + # Try to break symmetry to assign a consistent sign [experimental] + E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape) + sign = numpy.sign(E_weighted[:, :max(shape[0]//2, 1), :max(shape[1]//2, 1)].real.sum()) + + norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) e *= norm_factor h *= norm_factor From c3f248a73c53adc2ffc24ebf677e3c61b93d673f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:08:44 -0700 Subject: [PATCH 071/437] Clarify beta=wavenumber --- fdfd_tools/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index e50cc6a..b7a17a3 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -55,7 +55,7 @@ def solve_waveguide_mode_2d(mode_number: int, See Numerical Dispersion in Taflove's FDTD book. This correction term reduces the error in emitted power, but additional error is introduced into the E_err and H_err terms. This effect becomes - more pronounced as beta increases. + more pronounced as the wavenumber increases. ''' if wavenumber_correction: wavenumber -= 2 * numpy.sin(numpy.real(wavenumber / 2)) - numpy.real(wavenumber) From 5dd26915fc66ada63d64b628a08211dd47aed0b0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:09:12 -0700 Subject: [PATCH 072/437] wavenumber correction must take dx into account --- fdfd_tools/waveguide_mode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index b7a17a3..96ab16a 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -58,7 +58,8 @@ def solve_waveguide_mode_2d(mode_number: int, more pronounced as the wavenumber increases. ''' if wavenumber_correction: - wavenumber -= 2 * numpy.sin(numpy.real(wavenumber / 2)) - numpy.real(wavenumber) + dx_mean = (numpy.hstack(dxes[0]) + numpy.hstack(dxes[1])).mean() / 2 #TODO figure out what dx to use here + wavenumber -= 2 * numpy.sin(numpy.real(wavenumber * dx_mean / 2)) / dx_mean - numpy.real(wavenumber) shape = [d.size for d in dxes[0]] fields = { From 2b3a74b737b448b8aaa6eef99c01a9d3cba90383 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:11:32 -0700 Subject: [PATCH 073/437] Fix waveguide source computation for different polarities etc. --- fdfd_tools/waveguide_mode.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 96ab16a..917c535 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -101,6 +101,8 @@ def solve_waveguide_mode(mode_number: int, if mu is None: mu = [numpy.ones_like(epsilon[0])] * 3 + slices = tuple(slices) + ''' Solve the 2D problem in the specified plane ''' @@ -183,23 +185,23 @@ def compute_source(E: field_t, J = [None]*3 M = [None]*3 - src_order = numpy.roll(range(3), axis) + src_order = numpy.roll(range(3), -axis) exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) J[src_order[0]] = numpy.zeros_like(E[0]) J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity + rollby = -1 if polarity > 0 else 0 M[src_order[0]] = numpy.zeros_like(E[0]) - M[src_order[1]] = +numpy.roll(E[src_order[2]], -1, axis=axis) - M[src_order[2]] = -numpy.roll(E[src_order[1]], -1, axis=axis) + M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) + M[src_order[2]] = -numpy.roll(E[src_order[1]], rollby, axis=axis) - A1f = functional.curl_h(dxes) + m2j = functional.m2j(omega, dxes, mu) + Jm = m2j(M) - Jm_iw = A1f([M[k] / mu[k] for k in range(3)]) - for k in range(3): - J[k] += Jm_iw[k] / (-1j * omega) + Jtot = [ji + jmi for ji, jmi in zip(J, Jm)] - return J + return Jtot def compute_overlap_e(E: field_t, From 3a5d75cde4155cf615ffb8058eddf99264d13271 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:11:45 -0700 Subject: [PATCH 074/437] fix typo --- fdfd_tools/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index 02c1197..8809d09 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -451,7 +451,7 @@ def avgb(axis: int, shape: List[int]) -> sparse.spmatrix: def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ - Operator for computing the Poynting vector, contining the (E x) portion of the Poynting vector. + Operator for computing the Poynting vector, containing the (E x) portion of the Poynting vector. :param e: Vectorized E-field for the ExH cross product :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header From 2acbda4764483253863cf4bedf3eadb1f4dadf8e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:12:03 -0700 Subject: [PATCH 075/437] Force slices to be a tuple --- fdfd_tools/waveguide_mode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 917c535..ab27d35 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -234,6 +234,8 @@ def compute_overlap_e(E: field_t, :param mu: Magnetic permeability (default 1 everywhere) :return: overlap_e for calculating the mode overlap """ + slices = tuple(slices) + cross_plane = [slice(None)] * 3 cross_plane[axis] = slices[axis] From d462ae94121b4fbeaaecf13444092137f7da1497 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:12:48 -0700 Subject: [PATCH 076/437] unvec to (3, *shape) rather than list-of-ndarrays --- fdfd_tools/vectorization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/vectorization.py b/fdfd_tools/vectorization.py index e7b9645..bdc9d6a 100644 --- a/fdfd_tools/vectorization.py +++ b/fdfd_tools/vectorization.py @@ -45,5 +45,5 @@ def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: """ if numpy.any(numpy.equal(v, None)): return None - return [vi.reshape(shape, order='C') for vi in numpy.split(v, 3)] + return vi.reshape((3, *shape), order='C') From 4c2035c88231ddfd7c3b4b0a25078996bb3df28f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:13:07 -0700 Subject: [PATCH 077/437] Add m2j() function --- fdfd_tools/functional.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/fdfd_tools/functional.py b/fdfd_tools/functional.py index f184223..1d39d84 100644 --- a/fdfd_tools/functional.py +++ b/fdfd_tools/functional.py @@ -147,3 +147,36 @@ def e2h(omega: complex, return e2h_1_1 else: return e2h_mu + + +def m2j(omega: complex, + dxes: dx_lists_t, + mu: field_t = None, + ) -> functional_matrix: + """ + Utility operator for converting magnetic current (M) distribution + into equivalent electric current distribution (J). + For use with e.g. e_full(). + + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param mu: Magnetic permeability (default 1 everywhere) + :return: Function for converting M to J + """ + ch = curl_h(dxes) + + def m2j_mu(m): + m_mu = [m[k] / mu[k] for k in range[3]] + J = [Ji / (-1j * omega) for Ji in ch(m_mu)] + return J + + def m2j_1(m): + J = [Ji / (-1j * omega) for Ji in ch(m)] + return J + + if numpy.any(numpy.equal(mu, None)): + return m2j_1 + else: + return m2j_mu + + From 8e634e35df9d624786c92fd2e03b7b333cdf183c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:13:31 -0700 Subject: [PATCH 078/437] Add experimental source types --- fdfd_tools/waveguide_mode.py | 139 +++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index ab27d35..aac718d 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -343,3 +343,142 @@ def solve_waveguide_mode_cylindrical(mode_number: int, } return fields + + +def compute_source_q(E: field_t, + H: field_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + mu: field_t = None, + ) -> field_t: + A1f = functional.curl_h(dxes) + A2f = functional.curl_e(dxes) + + J = A1f(H) + M = A2f([-E[i] for i in range(3)]) + + m2j = functional.m2j(omega, dxes, mu) + Jm = m2j(M) + + Jtot = [ji + jmi for ji, jmi in zip(J, Jm)] + + return Jtot, J, M + + + +def compute_source_e(QE: field_t, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + epsilon: field_t, + mu: field_t = None, + ) -> field_t: + """ + Want (AQ-QA) E = -iwj, where Q is a mask + If E is an eigenmode, AE = 0 so just AQE = -iwJ + Really only need E in 4 cells along axis (0, 0, Emode1, Emode2), find AE (1 fdtd step), then use center 2 cells as src + """ + slices = tuple(slices) + + # Trim a cell from each end of the propagation axis + slices_reduced = list(slices) + slices_reduced[axis] = slice(slices[axis].start + 1, slices[axis].stop - 1) + slices_reduced = tuple(slices) + + # Don't actually need to mask out E here since it needs to be pre-masked (QE) + + A = functional.e_full(omega, dxes, epsilon, mu) + J4 = [ji / (-1j * omega) for ji in A(QE)] #J4 is 4-cell result of -iwJ = A QE + + J = numpy.zeros_like(J4) + for a in range(3): + J[a][slices_reduced] = J4[a][slices_reduced] + return J + + +def compute_source_wg(E: field_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + epsilon: field_t, + mu: field_t = None, + ) -> field_t: + slices = tuple(slices) + Etgt, _slices2 = compute_overlap_ce(E=E, wavenumber=wavenumber, + dxes=dxes, axis=axis, polarity=polarity, + slices=slices) + + slices4 = list(slices) + slices4[axis] = slice(slices[axis].start - 4 * polarity, slices[axis].start) + slices4 = tuple(slices4) + + J = compute_source_e(QE=Etgt, + omega=omega, dxes=dxes, axis=axis, + polarity=polarity, slices=slices4, + epsilon=epsilon, mu=mu) + +def compute_overlap_ce(E: field_t, + wavenumber: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + ) -> field_t: + slices = tuple(slices) + + Ee = expand_wgmode_e(E=E, wavenumber=wavenumber, + dxes=dxes, axis=axis, polarity=polarity, + slices=slices) + + start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity)) + + slices2 = list(slices) + slices2[axis] = slice(start, stop) + slices2 = tuple(slices2) + + Etgt = numpy.zeros_like(Ee) + for a in range(3): + Etgt[a][slices2] = Ee[a][slices2] + + Etgt /= (Etgt.conj() * Etgt).sum() + + return Etgt, slices2 + + +def expand_wgmode_e(E: field_t, + wavenumber: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + ) -> field_t: + slices = tuple(slices) + + # Determine phase factors for parallel slices + a_shape = numpy.roll([1, -1, 1, 1], axis) + a_E = numpy.real(dxes[0][axis]).cumsum() + r_E = a_E - a_E[slices[axis]] + iphi = polarity * 1j * wavenumber + phase_E = numpy.exp(iphi * r_E).reshape(a_shape) + + # Expand our slice to the entire grid using the phase factors + Ee = numpy.zeros_like(E) + + slices_exp = list(slices) + slices_exp[axis] = slice(E[0].shape[axis]) + slices_exp = (slice(3), *slices_exp) + + Ee[slices_exp] = phase_E * numpy.array(E)[slices_Exp] + + return Ee + + From 9d1d8fe869125974cb134548e28d5f0ce77fcdc9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:13:49 -0700 Subject: [PATCH 079/437] Improve wisdom management --- examples/bloch.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/examples/bloch.py b/examples/bloch.py index 793bd89..fe1d6b1 100644 --- a/examples/bloch.py +++ b/examples/bloch.py @@ -2,11 +2,43 @@ import numpy, scipy, gridlock, fdfd_tools from fdfd_tools import bloch from numpy.linalg import norm import logging +from pathlib import Path logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) +WISDOM_FILEPATH = pathlib.Path.home() / '.local' / 'share' / 'pyfftw' / 'wisdom.pickle' + +def pyfftw_save_wisdom(path): + path = pathlib.Path(path) + try: + import pyfftw + import pickle + except ImportError as e: + pass + + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, 'wb') as f: + pickle.dump(wisdom, f) + + +def pyfftw_load_wisdom(path): + path = pathlib.Path(path) + try: + import pyfftw + import pickle + except ImportError as e: + pass + + try: + with open(path, 'rb') as f: + wisdom = pickle.load(f) + pyfftw.import_wisdom(wisdom) + except FileNotFoundError as e: + pass + +logger.info('Drawing grid...') dx = 40 x_period = 400 y_period = z_period = 2000 @@ -32,6 +64,8 @@ g2.grids = [numpy.zeros(g.shape) for _ in range(6)] epsilon = [g.grids[0],] * 3 reciprocal_lattice = numpy.diag(1000/numpy.array([x_period, y_period, z_period])) #cols are vectors +pyfftw_load_wisdom(WISDOM_FILEPATH) + #print('Finding k at 1550nm') #k, f = bloch.find_k(frequency=1000/1550, # tolerance=(1000 * (1/1550 - 1/1551)), @@ -42,7 +76,7 @@ reciprocal_lattice = numpy.diag(1000/numpy.array([x_period, y_period, z_period]) # #print("k={}, f={}, 1/f={}, k/f={}".format(k, f, 1/f, norm(reciprocal_lattice @ k) / f )) -print('Finding f at [0.25, 0, 0]') +logger.info('Finding f at [0.25, 0, 0]') for k0x in [.25]: k0 = numpy.array([k0x, 0, 0]) @@ -66,3 +100,4 @@ for k0x in [.25]: n_eff = norm(reciprocal_lattice @ k0) / f print('kmag/f = n_eff = {} \n wl = {}\n'.format(n_eff, 1/f )) +pyfftw_save_wisdom(WISDOM_FILEPATH) From 557e7483569314a8dc4c2b0b0a8b15d8671f3558 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:19:35 -0700 Subject: [PATCH 080/437] Reduce number of allocations during maxwell curls --- fdfd_tools/fdtd.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index c4aa897..3687c82 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -26,10 +26,14 @@ def curl_h(dxes: dx_lists_t = None) -> functional_matrix: return f - numpy.roll(f, 1, axis=ax) def ch_fun(h: field_t) -> field_t: - e = [dh(h[2], 1) - dh(h[1], 2), - dh(h[0], 2) - dh(h[2], 0), - dh(h[1], 0) - dh(h[0], 1)] - return e + output = numpy.empty_like(h) + output[0] = dh(h[2], 1) + output[1] = dh(h[0], 2) + output[2] = dh(h[1], 0) + output[0] -= dh(h[1], 2) + output[1] -= dh(h[2], 0) + output[2] -= dh(h[0], 1) + return output return ch_fun @@ -51,10 +55,14 @@ def curl_e(dxes: dx_lists_t = None) -> functional_matrix: return numpy.roll(f, -1, axis=ax) - f def ce_fun(e: field_t) -> field_t: - h = [de(e[2], 1) - de(e[1], 2), - de(e[0], 2) - de(e[2], 0), - de(e[1], 0) - de(e[0], 1)] - return h + output = numpy.empty_like(e) + output[0] = de(e[2], 1) + output[1] = de(e[0], 2) + output[2] = de(e[1], 0) + output[0] -= de(e[1], 2) + output[1] -= de(e[2], 0) + output[2] -= de(e[0], 1) + return output return ce_fun From a8a5a69e1a747250f33f01fd4eb048287c9a969f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:20:05 -0700 Subject: [PATCH 081/437] Eliminate iterations over lists (assume ndarray instead of list of ndarrays) --- fdfd_tools/fdtd.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index 3687c82..f72c7a3 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -71,9 +71,7 @@ def maxwell_e(dt: float, dxes: dx_lists_t = None) -> functional_matrix: curl_h_fun = curl_h(dxes) def me_fun(e: field_t, h: field_t, epsilon: field_t): - ch = curl_h_fun(h) - for ei, ci, epsi in zip(e, ch, epsilon): - ei += dt * ci / epsi + e += dt * curl_h_fun(h)/ epsilon return e return me_fun @@ -83,9 +81,7 @@ def maxwell_h(dt: float, dxes: dx_lists_t = None) -> functional_matrix: curl_e_fun = curl_e(dxes) def mh_fun(e: field_t, h: field_t): - ce = curl_e_fun(e) - for hi, ci in zip(h, ce): - hi -= dt * ci + h -= dt * curl_e_fun(e) return h return mh_fun From 099966f2910e6fb115cc029b7eb9bdc3da547109 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:20:49 -0700 Subject: [PATCH 082/437] Add poynting vector and divergence --- fdfd_tools/fdtd.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index f72c7a3..5279d22 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -241,3 +241,18 @@ def cpml(direction:int, return e, h return pml_e, pml_h, fields + + +def poynting(e, h): + s = [numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], + numpy.roll(e[2], -1, axis=1) * h[0] - numpy.roll(e[0], -1, axis=1) * h[2], + numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]] + return numpy.array(s) + + +def div_poyting(dt, dxes, e, h): + s = poynting(e, h) + ds = (s[0] - numpy.roll(s[0], 1, axis=0) + + s[1] - numpy.roll(s[1], 1, axis=1) + + s[2] - numpy.roll(s[2], 1, axis=2)) + return ds From ecaf9fa3d017c56ab7a12622b5d7b2f6cf5170c4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:21:14 -0700 Subject: [PATCH 083/437] Test code for cylindrical wg modesolver --- examples/tcyl.py | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 examples/tcyl.py diff --git a/examples/tcyl.py b/examples/tcyl.py new file mode 100644 index 0000000..66caeb2 --- /dev/null +++ b/examples/tcyl.py @@ -0,0 +1,92 @@ +import importlib +import numpy +from numpy.linalg import norm + +from fdfd_tools import vec, unvec, waveguide_mode +import fdfd_tools +import fdfd_tools.functional +import fdfd_tools.grid +from fdfd_tools.solvers import generic as generic_solver + +import gridlock + +from matplotlib import pyplot + + +__author__ = 'Jan Petykiewicz' + + +def test1(solver=generic_solver): + dx = 20 # discretization (nm/cell) + pml_thickness = 10 # (number of cells) + + wl = 1550 # Excitation wavelength + omega = 2 * numpy.pi / wl + + # Device design parameters + w = 800 + th = 220 + center = [0, 0, 0] + r0 = 8e3 + + # refractive indices + n_wg = numpy.sqrt(12.6) # ~Si + n_air = 1.0 # air + + # Half-dimensions of the simulation grid + y_max = 1200 + z_max = 900 + xyz_max = numpy.array([800, y_max, z_max]) + (pml_thickness + 2) * dx + + # Coordinates of the edges of the cells. + half_edge_coords = [numpy.arange(dx/2, m + dx/2, step=dx) for m in xyz_max] + edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords] + edge_coords[0] = numpy.array([-dx, dx]) + + # #### Create the grid and draw the device #### + grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3) + grid.draw_cuboid(center=center, dimensions=[8e3, w, th], eps=n_wg**2) + + dxes = [grid.dxyz, grid.autoshifted_dxyz()] + for a in (1, 2): + for p in (-1, 1): + dxes = fdfd_tools.grid.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p, + thickness=pml_thickness) + + wg_args = { + 'omega': omega, + 'dxes': [(d[1], d[2]) for d in dxes], + 'epsilon': vec(g.transpose([1, 2, 0]) for g in grid.grids), + 'r0': r0, + } + + wg_results = waveguide_mode.solve_waveguide_mode_cylindrical(mode_number=0, **wg_args) + + E = wg_results['E'] + + n_eff = wl / (2 * numpy.pi / wg_results['wavenumber']) + print('n =', n_eff) + print('alpha (um^-1) =', -4 * numpy.pi * numpy.imag(n_eff) / (wl * 1e-3)) + + ''' + Plot results + ''' + def pcolor(v): + vmax = numpy.max(numpy.abs(v)) + pyplot.pcolor(v.T, cmap='seismic', vmin=-vmax, vmax=vmax) + pyplot.axis('equal') + pyplot.colorbar() + + pyplot.figure() + pyplot.subplot(2, 2, 1) + pcolor(numpy.real(E[0][:, :])) + pyplot.subplot(2, 2, 2) + pcolor(numpy.real(E[1][:, :])) + pyplot.subplot(2, 2, 3) + pcolor(numpy.real(E[2][:, :])) + pyplot.subplot(2, 2, 4) + pyplot.show() + + +if __name__ == '__main__': + test1() From dd4e6f294fb906fb05352923a6cacb757c5fd6ee Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 15 Jul 2019 01:21:12 -0700 Subject: [PATCH 084/437] update fdtd and add some fdtd tests --- fdfd_tools/fdtd.py | 139 +++++++++++++++++++++++++++++++--------- fdfd_tools/test_fdtd.py | 68 ++++++++++++++++++++ 2 files changed, 176 insertions(+), 31 deletions(-) create mode 100644 fdfd_tools/test_fdtd.py diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index 5279d22..0cdec0d 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -3,6 +3,8 @@ import numpy from . import dx_lists_t, field_t +#TODO fix pmls + __author__ = 'Jan Petykiewicz' @@ -71,7 +73,7 @@ def maxwell_e(dt: float, dxes: dx_lists_t = None) -> functional_matrix: curl_h_fun = curl_h(dxes) def me_fun(e: field_t, h: field_t, epsilon: field_t): - e += dt * curl_h_fun(h)/ epsilon + e += dt * curl_h_fun(h) / epsilon return e return me_fun @@ -150,7 +152,12 @@ def cpml(direction:int, dt: float, epsilon: field_t, thickness: int = 8, + ln_R_per_layer: float = -1.6, epsilon_eff: float = 1, + mu_eff: float = 1, + m: float = 3.5, + ma: float = 1, + cfs_alpha: float = 0, dtype: numpy.dtype = numpy.float32, ) -> Tuple[Callable, Callable, Dict[str, field_t]]: @@ -166,9 +173,9 @@ def cpml(direction:int, if epsilon_eff <= 0: raise Exception('epsilon_eff must be positive') - m = (3.5, 1) - sigma_max = 0.8 * (m[0] + 1) / numpy.sqrt(epsilon_eff) - alpha_max = 0 # TODO: Decide what to do about non-zero alpha + sigma_max = -ln_R_per_layer / 2 * (m + 1) + kappa_max = numpy.sqrt(epsilon_eff * mu_eff) + alpha_max = cfs_alpha transverse = numpy.delete(range(3), direction) u, v = transverse @@ -187,14 +194,17 @@ def cpml(direction:int, expand_slice[direction] = slice(None) def par(x): - sigma = ((x / thickness) ** m[0]) * sigma_max - alpha = ((1 - x / thickness) ** m[1]) * alpha_max - p0 = numpy.exp(-(sigma + alpha) * dt) - p1 = sigma / (sigma + alpha) * (p0 - 1) - return p0[expand_slice], p1[expand_slice] + scaling = (x / thickness) ** m + sigma = scaling * sigma_max + kappa = 1 + scaling * (kappa_max - 1) + alpha = ((1 - x / thickness) ** ma) * alpha_max + p0 = numpy.exp(-(sigma / kappa + alpha) * dt) + p1 = sigma / (sigma + kappa * alpha) * (p0 - 1) + p2 = 1 / kappa + return p0[expand_slice], p1[expand_slice], p2[expand_slice] - p0e, p1e = par(xe) - p0h, p1h = par(xh) + p0e, p1e, p2e = par(xe) + p0h, p1h, p2h = par(xh) region = [slice(None)] * 3 if polarity < 0: @@ -204,12 +214,9 @@ def cpml(direction:int, else: raise Exception('Bad polarity!') - if direction == 1: - se = 1 - else: - se = -1 + se = 1 if direction == 1 else -1 - # TODO check if epsilon is uniform? + # TODO check if epsilon is uniform in pml region? shape = list(epsilon[0].shape) shape[direction] = thickness psi_e = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] @@ -222,37 +229,107 @@ def cpml(direction:int, 'psi_h_v': psi_h[1], } + # Note that this is kinda slow -- would be faster to reuse dHv*p2h for the original + # H update, but then you have multiple arrays and a monolithic (field + pml) update operation def pml_e(e: field_t, h: field_t, epsilon: field_t) -> Tuple[field_t, field_t]: + dHv = h[v][region] - numpy.roll(h[v], 1, axis=direction)[region] + dHu = h[u][region] - numpy.roll(h[u], 1, axis=direction)[region] psi_e[0] *= p0e - psi_e[0] += p1e * (h[v][region] - numpy.roll(h[v], 1, axis=direction)[region]) + psi_e[0] += p1e * dHv * p2e psi_e[1] *= p0e - psi_e[1] += p1e * (h[u][region] - numpy.roll(h[u], 1, axis=direction)[region]) - e[u][region] += se * dt * psi_e[0] / epsilon[u][region] - e[v][region] -= se * dt * psi_e[1] / epsilon[v][region] + psi_e[1] += p1e * dHu * p2e + e[u][region] += se * dt / epsilon[u][region] * (psi_e[0] + (p2e - 1) * dHv) + e[v][region] -= se * dt / epsilon[v][region] * (psi_e[1] + (p2e - 1) * dHu) return e, h def pml_h(e: field_t, h: field_t) -> Tuple[field_t, field_t]: + dEv = (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region]) + dEu = (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region]) psi_h[0] *= p0h - psi_h[0] += p1h * (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region]) + psi_h[0] += p1h * dEv * p2h psi_h[1] *= p0h - psi_h[1] += p1h * (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region]) - h[u][region] -= se * dt * psi_h[0] - h[v][region] += se * dt * psi_h[1] + psi_h[1] += p1h * dEu * p2h + h[u][region] -= se * dt * (psi_h[0] + (p2h - 1) * dEv) + h[v][region] += se * dt * (psi_h[1] + (p2h - 1) * dEu) return e, h return pml_e, pml_h, fields def poynting(e, h): - s = [numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], + s = (numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], numpy.roll(e[2], -1, axis=1) * h[0] - numpy.roll(e[0], -1, axis=1) * h[2], - numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]] + numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]) return numpy.array(s) -def div_poyting(dt, dxes, e, h): - s = poynting(e, h) - ds = (s[0] - numpy.roll(s[0], 1, axis=0) + - s[1] - numpy.roll(s[1], 1, axis=1) + - s[2] - numpy.roll(s[2], 1, axis=2)) +def poynting_divergence(dt, dxes, s=None, *, e=None, h=None): # TODO dxes + if s is None: + s = poynting(e, h) + ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) / numpy.sqrt(dxes[0][0] * dxes[1][0])[:, None, None] + + (s[1] - numpy.roll(s[1], 1, axis=1)) / numpy.sqrt(dxes[0][1] * dxes[1][1])[None, :, None] + + (s[2] - numpy.roll(s[2], 1, axis=2)) / numpy.sqrt(dxes[0][2] * dxes[1][2])[None, None, :] ) return ds + + +def energy_hstep(e0, h1, e2, epsilon=None, mu=None, dxes=None): + u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) + return u + + +def energy_estep(h0, e1, h2, epsilon=None, mu=None, dxes=None): + u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) + return u + + +def delta_energy_h2e(dt, e0, h1, e2, h3, epsilon=None, mu=None, dxes=None): + """ + This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) + """ + de = e2 * (e2 - e0) / dt + dh = h1 * (h3 - h1) / dt + du = dt * dxmul(de, dh, epsilon, mu, dxes) + return du + + +def delta_energy_e2h(dt, h0, e1, h2, e3, epsilon=None, mu=None, dxes=None): + """ + This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) + """ + de = e1 * (e3 - e1) / dt + dh = h2 * (h2 - h0) / dt + du = dxmul(de, dh, epsilon, mu, dxes) + return du + + +def delta_energy_j(j0, e1, dxes=None): + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + + du = ((j0 * e1).sum(axis=0) * + dxes[0][0][:, None, None] * + dxes[0][1][None, :, None] * + dxes[0][2][None, None, :]) + return du + + +def dxmul(ee, hh, epsilon=None, mu=None, dxes=None): + if epsilon is None: + epsilon = 1 + if mu is None: + mu = 1 + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + + result = ((ee * epsilon).sum(axis=0) * + dxes[0][0][:, None, None] * + dxes[0][1][None, :, None] * + dxes[0][2][None, None, :] + + (hh * mu).sum(axis=0) * + dxes[1][0][:, None, None] * + dxes[1][1][None, :, None] * + dxes[1][2][None, None, :]) + return result + + + diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py new file mode 100644 index 0000000..ccdc00c --- /dev/null +++ b/fdfd_tools/test_fdtd.py @@ -0,0 +1,68 @@ +import unittest +import numpy + +from fdfd_tools import fdtd + +class TestBasic2D(unittest.TestCase): + def setUp(self): + shape = [3, 5, 5, 1] + dt = 0.5 + epsilon = numpy.ones(shape, dtype=float) + + src_mask = numpy.zeros_like(epsilon, dtype=bool) + src_mask[1, 2, 2, 0] = True + + e = numpy.zeros_like(epsilon) + h = numpy.zeros_like(epsilon) + e[src_mask] = 32 + es = [e] + hs = [h] + + eh2h = fdtd.maxwell_h(dt=dt) + eh2e = fdtd.maxwell_e(dt=dt) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, epsilon) + es.append(e) + hs.append(h) + + self.es = es + self.hs = hs + self.dt = dt + self.epsilon = epsilon + self.src_mask = src_mask + + def test_initial_fields(self): + # Make sure initial fields didn't change + e0 = self.es[0] + h0 = self.hs[0] + self.assertEqual(e0[1, 2, 2, 0], 32) + + self.assertFalse(e0[~self.src_mask].any()) + self.assertFalse(h0.any()) + + + def test_initial_energy(self): + e0 = self.es[0] + h0 = self.hs[0] + h1 = self.hs[1] + mask = self.src_mask[1] + + # Make sure initial energy and E dot J are correct + energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=self.hs[1]) + e_dot_j_0 = fdtd.delta_energy_j(j0=e0 - 0, e1=e0) + self.assertEqual(energy0[mask], 32 * 32) + self.assertFalse(energy0[~mask].any()) + self.assertEqual(e_dot_j_0[mask], 32 * 32) + self.assertFalse(e_dot_j_0[~mask].any()) + + + def test_energy_conservation(self): + for ii in range(1, 8): + with self.subTest(i=ii): + u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1]) + u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii]) + self.assertTrue(numpy.allclose(u_estep.sum(), 32 * 32)) + self.assertTrue(numpy.allclose(u_hstep.sum(), 32 * 32)) From 79e14af4db371d82bfb4c2eec084d06af7ed827b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 17 Jul 2019 00:50:49 -0700 Subject: [PATCH 085/437] poynting divergence doesn't use dt, and can have default dxes --- fdfd_tools/fdtd.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index 0cdec0d..bc52e45 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -263,9 +263,13 @@ def poynting(e, h): return numpy.array(s) -def poynting_divergence(dt, dxes, s=None, *, e=None, h=None): # TODO dxes +def poynting_divergence(s=None, *, e=None, h=None, dxes=None): # TODO dxes + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + if s is None: s = poynting(e, h) + ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) / numpy.sqrt(dxes[0][0] * dxes[1][0])[:, None, None] + (s[1] - numpy.roll(s[1], 1, axis=1)) / numpy.sqrt(dxes[0][1] * dxes[1][1])[None, :, None] + (s[2] - numpy.roll(s[2], 1, axis=2)) / numpy.sqrt(dxes[0][2] * dxes[1][2])[None, None, :] ) From 935b2c9a80a3f31fa36ee561204d431e76ac524f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 17 Jul 2019 00:51:13 -0700 Subject: [PATCH 086/437] remove extra dt --- fdfd_tools/fdtd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index bc52e45..15b5635 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -292,7 +292,7 @@ def delta_energy_h2e(dt, e0, h1, e2, h3, epsilon=None, mu=None, dxes=None): """ de = e2 * (e2 - e0) / dt dh = h1 * (h3 - h1) / dt - du = dt * dxmul(de, dh, epsilon, mu, dxes) + du = dxmul(de, dh, epsilon, mu, dxes) return du From a528effd89263e6b845e9a438a7a41d665648cb9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 17 Jul 2019 00:51:28 -0700 Subject: [PATCH 087/437] add some more tests --- fdfd_tools/test_fdtd.py | 177 ++++++++++++++++++++++++++++++++-------- 1 file changed, 145 insertions(+), 32 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index ccdc00c..dc7f852 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -3,23 +3,94 @@ import numpy from fdfd_tools import fdtd -class TestBasic2D(unittest.TestCase): +class BasicTests(): + def test_initial_fields(self): + # Make sure initial fields didn't change + e0 = self.es[0] + h0 = self.hs[0] + mask = self.src_mask + + self.assertEqual(e0[mask], self.j_mag / self.epsilon[mask]) + self.assertFalse(e0[~mask].any()) + self.assertFalse(h0.any()) + + + def test_initial_energy(self): + e0 = self.es[0] + h0 = self.hs[0] + h1 = self.hs[1] + mask = self.src_mask[1] + u0 = self.j_mag * self.j_mag / self.epsilon[self.src_mask] + args = {'dxes': self.dxes, + 'epsilon': self.epsilon} + + # Make sure initial energy and E dot J are correct + energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=self.hs[1], **args) + e_dot_j_0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0, dxes=self.dxes) + self.assertEqual(energy0[mask], u0) + self.assertFalse(energy0[~mask].any()) + self.assertEqual(e_dot_j_0[mask], u0) + self.assertFalse(e_dot_j_0[~mask].any()) + + + def test_energy_conservation(self): + e0 = self.es[0] + u0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0).sum() + args = {'dxes': self.dxes, + 'epsilon': self.epsilon} + + for ii in range(1, 8): + with self.subTest(i=ii): + u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) + u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) + self.assertTrue(numpy.allclose(u_hstep.sum(), u0)) + self.assertTrue(numpy.allclose(u_estep.sum(), u0)) + + + def test_poynting(self): + args = {'dxes': self.dxes, + 'epsilon': self.epsilon} + + for ii in range(1, 8): + u_eprev = None + with self.subTest(i=ii): + u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) + u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) + + du_half_h2e = u_estep - u_hstep + div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) + self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e)) + + if u_eprev is None: + u_eprev = u_estep + continue + + # previous half-step + du_half_e2h = u_hstep - u_eprev + div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii-1], dxes=self.dxes) + self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h)) + u_eprev = u_estep + + +class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 1] dt = 0.5 epsilon = numpy.ones(shape, dtype=float) + j_mag = 32 + dxes = None src_mask = numpy.zeros_like(epsilon, dtype=bool) src_mask[1, 2, 2, 0] = True e = numpy.zeros_like(epsilon) h = numpy.zeros_like(epsilon) - e[src_mask] = 32 + e[src_mask] = j_mag / epsilon[src_mask] es = [e] hs = [h] - eh2h = fdtd.maxwell_h(dt=dt) - eh2e = fdtd.maxwell_e(dt=dt) + eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) + eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) for _ in range(9): e = e.copy() h = h.copy() @@ -32,37 +103,79 @@ class TestBasic2D(unittest.TestCase): self.hs = hs self.dt = dt self.epsilon = epsilon + self.dxes = dxes self.src_mask = src_mask - - def test_initial_fields(self): - # Make sure initial fields didn't change - e0 = self.es[0] - h0 = self.hs[0] - self.assertEqual(e0[1, 2, 2, 0], 32) - - self.assertFalse(e0[~self.src_mask].any()) - self.assertFalse(h0.any()) + self.j_mag = j_mag - def test_initial_energy(self): - e0 = self.es[0] - h0 = self.hs[0] - h1 = self.hs[1] - mask = self.src_mask[1] +class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): + def setUp(self): + shape = [3, 5, 5, 5] + dt = 0.33 + epsilon = numpy.ones(shape, dtype=float) + j_mag = 32 + dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - # Make sure initial energy and E dot J are correct - energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=self.hs[1]) - e_dot_j_0 = fdtd.delta_energy_j(j0=e0 - 0, e1=e0) - self.assertEqual(energy0[mask], 32 * 32) - self.assertFalse(energy0[~mask].any()) - self.assertEqual(e_dot_j_0[mask], 32 * 32) - self.assertFalse(e_dot_j_0[~mask].any()) + src_mask = numpy.zeros_like(epsilon, dtype=bool) + src_mask[1, 2, 2, 0] = True + + e = numpy.zeros_like(epsilon) + h = numpy.zeros_like(epsilon) + e[src_mask] = j_mag / epsilon[src_mask] + es = [e] + hs = [h] + + eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) + eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, epsilon) + es.append(e) + hs.append(h) + + self.es = es + self.hs = hs + self.dt = dt + self.epsilon = epsilon + self.dxes = dxes + self.src_mask = src_mask + self.j_mag = j_mag - def test_energy_conservation(self): - for ii in range(1, 8): - with self.subTest(i=ii): - u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1]) - u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii]) - self.assertTrue(numpy.allclose(u_estep.sum(), 32 * 32)) - self.assertTrue(numpy.allclose(u_hstep.sum(), 32 * 32)) +class Basic3DUniformDX(unittest.TestCase, BasicTests): + def setUp(self): + shape = [3, 5, 5, 5] + dt = 0.33 + epsilon = numpy.full(shape, 2, dtype=float) + j_mag = 32 + dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) + + src_mask = numpy.zeros_like(epsilon, dtype=bool) + src_mask[1, 2, 2, 0] = True + + e = numpy.zeros_like(epsilon) + h = numpy.zeros_like(epsilon) + e[src_mask] = j_mag / epsilon[src_mask] + es = [e] + hs = [h] + + eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) + eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, epsilon) + es.append(e) + hs.append(h) + + self.es = es + self.hs = hs + self.dt = dt + self.epsilon = epsilon + self.dxes = dxes + self.src_mask = src_mask + self.j_mag = j_mag + From 2cec4fabaf83cc83e13a8ec2155d8c2927560e58 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 17 Jul 2019 23:47:45 -0700 Subject: [PATCH 088/437] Account for dxes --- fdfd_tools/test_fdtd.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index dc7f852..33f51b0 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -20,7 +20,9 @@ class BasicTests(): h0 = self.hs[0] h1 = self.hs[1] mask = self.src_mask[1] - u0 = self.j_mag * self.j_mag / self.epsilon[self.src_mask] + dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in e0.shape[1:]) for _ in range(2)) + dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) + u0 = self.j_mag * self.j_mag / self.epsilon[self.src_mask] * dV[mask] args = {'dxes': self.dxes, 'epsilon': self.epsilon} @@ -35,7 +37,7 @@ class BasicTests(): def test_energy_conservation(self): e0 = self.es[0] - u0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0).sum() + u0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0, dxes=self.dxes).sum() args = {'dxes': self.dxes, 'epsilon': self.epsilon} From f858cb8bbb0d38bf62a47b7aeb6a7d3fd5a59418 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 17 Jul 2019 23:48:04 -0700 Subject: [PATCH 089/437] Fix poynting e2h test --- fdfd_tools/test_fdtd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 33f51b0..7e1e914 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -53,8 +53,8 @@ class BasicTests(): args = {'dxes': self.dxes, 'epsilon': self.epsilon} + u_eprev = None for ii in range(1, 8): - u_eprev = None with self.subTest(i=ii): u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) @@ -69,7 +69,7 @@ class BasicTests(): # previous half-step du_half_e2h = u_hstep - u_eprev - div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii-1], dxes=self.dxes) + div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h)) u_eprev = u_estep From 950e70213a9b78d8f8d2ba4073749e879be8773c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 18 Jul 2019 00:03:32 -0700 Subject: [PATCH 090/437] Consolidate variables in test case setups --- fdfd_tools/test_fdtd.py | 119 ++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 71 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 7e1e914..dd0192e 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -77,107 +77,84 @@ class BasicTests(): class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 1] - dt = 0.5 - epsilon = numpy.ones(shape, dtype=float) - j_mag = 32 - dxes = None + self.dt = 0.5 + self.epsilon = numpy.ones(shape, dtype=float) + self.j_mag = 32 + self.dxes = None - src_mask = numpy.zeros_like(epsilon, dtype=bool) - src_mask[1, 2, 2, 0] = True + self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) + self.src_mask[1, 2, 2, 0] = True - e = numpy.zeros_like(epsilon) - h = numpy.zeros_like(epsilon) - e[src_mask] = j_mag / epsilon[src_mask] - es = [e] - hs = [h] + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] - eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) - eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) for _ in range(9): e = e.copy() h = h.copy() eh2h(e, h) - eh2e(e, h, epsilon) - es.append(e) - hs.append(h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) - self.es = es - self.hs = hs - self.dt = dt - self.epsilon = epsilon - self.dxes = dxes - self.src_mask = src_mask - self.j_mag = j_mag class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 5] - dt = 0.33 - epsilon = numpy.ones(shape, dtype=float) - j_mag = 32 - dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) + self.dt = 0.33 + self.epsilon = numpy.ones(shape, dtype=float) + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - src_mask = numpy.zeros_like(epsilon, dtype=bool) - src_mask[1, 2, 2, 0] = True + self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) + self.src_mask[1, 2, 2, 0] = True - e = numpy.zeros_like(epsilon) - h = numpy.zeros_like(epsilon) - e[src_mask] = j_mag / epsilon[src_mask] - es = [e] - hs = [h] + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] - eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) - eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) for _ in range(9): e = e.copy() h = h.copy() eh2h(e, h) - eh2e(e, h, epsilon) - es.append(e) - hs.append(h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) - self.es = es - self.hs = hs - self.dt = dt - self.epsilon = epsilon - self.dxes = dxes - self.src_mask = src_mask - self.j_mag = j_mag class Basic3DUniformDX(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 5] - dt = 0.33 - epsilon = numpy.full(shape, 2, dtype=float) - j_mag = 32 - dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) + self.dt = 0.33 + self.epsilon = numpy.full(shape, 2, dtype=float) + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - src_mask = numpy.zeros_like(epsilon, dtype=bool) - src_mask[1, 2, 2, 0] = True + self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) + self.src_mask[1, 2, 2, 0] = True - e = numpy.zeros_like(epsilon) - h = numpy.zeros_like(epsilon) - e[src_mask] = j_mag / epsilon[src_mask] - es = [e] - hs = [h] + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] - eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) - eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) for _ in range(9): e = e.copy() h = h.copy() eh2h(e, h) - eh2e(e, h, epsilon) - es.append(e) - hs.append(h) - - self.es = es - self.hs = hs - self.dt = dt - self.epsilon = epsilon - self.dxes = dxes - self.src_mask = src_mask - self.j_mag = j_mag - + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) From fb3c88a78dcf6a6bdcbb47294eb2b49f0bb10c58 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 19 Jul 2019 00:19:32 -0700 Subject: [PATCH 091/437] add test_poynting_planes --- fdfd_tools/test_fdtd.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index dd0192e..692cd61 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -3,6 +3,7 @@ import numpy from fdfd_tools import fdtd + class BasicTests(): def test_initial_fields(self): # Make sure initial fields didn't change @@ -49,7 +50,7 @@ class BasicTests(): self.assertTrue(numpy.allclose(u_estep.sum(), u0)) - def test_poynting(self): + def test_poynting_divergence(self): args = {'dxes': self.dxes, 'epsilon': self.epsilon} @@ -74,6 +75,29 @@ class BasicTests(): u_eprev = u_estep + def test_poynting_planes(self): + args = {'dxes': self.dxes, + 'epsilon': self.epsilon} + + u_eprev = None + for ii in range(1, 8): + with self.subTest(i=ii): + u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) + u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) + + mx = numpy.roll(self.src_mask, (-1, -1), axis=(0, 1)) + my = numpy.roll(self.src_mask, -1, axis=2) + mz = numpy.roll(self.src_mask, (+1, -1), axis=(0, 3)) + px = numpy.roll(self.src_mask, -1, axis=0) + py = self.src_mask.copy() + pz = numpy.roll(self.src_mask, +1, axis=0) + s_h2e = -fdtd.poynting(e=self.es[ii], h=self.hs[ii]) * self.dt + planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), + s_h2e[py].sum(), -s_h2e[my].sum(), + s_h2e[pz].sum(), -s_h2e[mz].sum()] + self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) + + class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 1] From 223b202d03c8a5298732886bc8b368eca0487029 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 19 Jul 2019 00:19:47 -0700 Subject: [PATCH 092/437] More test cases --- fdfd_tools/test_fdtd.py | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 692cd61..bc995c1 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -126,17 +126,16 @@ class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): self.hs.append(h) - class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 5] - self.dt = 0.33 + self.dt = 0.5 self.epsilon = numpy.ones(shape, dtype=float) self.j_mag = 32 self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) - self.src_mask[1, 2, 2, 0] = True + self.src_mask[1, 2, 2, 2] = True e = numpy.zeros_like(self.epsilon) h = numpy.zeros_like(self.epsilon) @@ -156,16 +155,46 @@ class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): +class Basic3DUniformDXUniformN(unittest.TestCase, BasicTests): + def setUp(self): + shape = [3, 5, 5, 5] + self.dt = 0.5 + self.epsilon = numpy.full(shape, 2, dtype=float) + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) + + self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) + self.src_mask[1, 2, 2, 2] = True + + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] + + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) + + class Basic3DUniformDX(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 5] self.dt = 0.33 - self.epsilon = numpy.full(shape, 2, dtype=float) self.j_mag = 32 self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) - self.src_mask[1, 2, 2, 0] = True + self.src_mask = numpy.zeros(shape, dtype=bool) + self.src_mask[1, 2, 2, 2] = True + + self.epsilon = numpy.full(shape, 1, dtype=float) + self.epsilon[self.src_mask] = 2 e = numpy.zeros_like(self.epsilon) h = numpy.zeros_like(self.epsilon) From 30ddeb7b73e49e88acedd0dcb7d179dbcc1f0007 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 21 Jul 2019 22:05:40 -0700 Subject: [PATCH 093/437] fix typo in fdfd.vec() --- fdfd_tools/vectorization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/vectorization.py b/fdfd_tools/vectorization.py index bdc9d6a..57b58fb 100644 --- a/fdfd_tools/vectorization.py +++ b/fdfd_tools/vectorization.py @@ -45,5 +45,5 @@ def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: """ if numpy.any(numpy.equal(v, None)): return None - return vi.reshape((3, *shape), order='C') + return v.reshape((3, *shape), order='C') From 7f8a3261149e4fb2ca7adb3c9837b74e22ab5311 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 21 Jul 2019 22:06:24 -0700 Subject: [PATCH 094/437] Loosen tolerances on tests --- fdfd_tools/test_fdtd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index bc995c1..a88ca75 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -62,7 +62,7 @@ class BasicTests(): du_half_h2e = u_estep - u_hstep div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) - self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e)) + self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4)) if u_eprev is None: u_eprev = u_estep @@ -71,7 +71,7 @@ class BasicTests(): # previous half-step du_half_e2h = u_hstep - u_eprev div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) - self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h)) + self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4)) u_eprev = u_estep From f1fc308d25b24954513e57bf65c07a909b9f27a9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 21 Jul 2019 22:06:57 -0700 Subject: [PATCH 095/437] Add JdotE test --- fdfd_tools/test_fdtd.py | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index a88ca75..0b6278f 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -96,6 +96,7 @@ class BasicTests(): s_h2e[py].sum(), -s_h2e[my].sum(), s_h2e[pz].sum(), -s_h2e[mz].sum()] self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) +# print(planes, '\n', numpy.rollaxis(u_estep - u_hstep, -1), sum(planes)) class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): @@ -211,3 +212,52 @@ class Basic3DUniformDX(unittest.TestCase, BasicTests): eh2e(e, h, self.epsilon) self.es.append(e) self.hs.append(h) + + +class JdotE_3DUniformDX(unittest.TestCase): + def setUp(self): + shape = [3, 5, 5, 5] + self.dt = 0.5 + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.full(s, 2.0) for s in shape[1:]) for _ in range(2)) + + self.src_mask = numpy.zeros(shape, dtype=bool) + self.src_mask[1, 2, 2, 2] = True + + self.epsilon = numpy.full(shape, 4, dtype=float) + self.epsilon[self.src_mask] = 2 + + e = numpy.random.randint(-128, 128 + 1, size=shape).astype(numpy.float32) + h = numpy.random.randint(-128, 128 + 1, size=shape).astype(numpy.float32) + self.es = [e] + self.hs = [h] + + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) + for ii in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) + + if ii == 1: + e[self.src_mask] += self.j_mag / self.epsilon[self.src_mask] + self.j_dot_e = self.j_mag * e[self.src_mask] + + + def test_j_dot_e(self): + e0 = self.es[2] + j0 = numpy.zeros_like(e0) + j0[self.src_mask] = self.j_mag + u0 = fdtd.delta_energy_j(j0=j0, e1=e0, dxes=self.dxes) + args = {'dxes': self.dxes, + 'epsilon': self.epsilon} + + ii=2 + u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) + u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) + #print(u0.sum(), (u_estep - u_hstep).sum()) + self.assertTrue(numpy.allclose(u0.sum(), (u_estep - u_hstep).sum(), rtol=1e-4)) + From 7092c13088f27413b1d4573bca7cce158c01f7bc Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 22 Jul 2019 00:26:34 -0700 Subject: [PATCH 096/437] better error messages when tests fail --- fdfd_tools/test_fdtd.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 0b6278f..247bd9c 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -31,9 +31,9 @@ class BasicTests(): energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=self.hs[1], **args) e_dot_j_0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0, dxes=self.dxes) self.assertEqual(energy0[mask], u0) - self.assertFalse(energy0[~mask].any()) + self.assertFalse(energy0[~mask].any(), msg='energy0: {}'.format(energy0)) self.assertEqual(e_dot_j_0[mask], u0) - self.assertFalse(e_dot_j_0[~mask].any()) + self.assertFalse(e_dot_j_0[~mask].any(), msg='e_dot_j_0: {}'.format(e_dot_j_0)) def test_energy_conservation(self): @@ -46,8 +46,8 @@ class BasicTests(): with self.subTest(i=ii): u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - self.assertTrue(numpy.allclose(u_hstep.sum(), u0)) - self.assertTrue(numpy.allclose(u_estep.sum(), u0)) + self.assertTrue(numpy.allclose(u_hstep.sum(), u0), msg='u_hstep: {}\n{}'.format(u_hstep.sum(), numpy.rollaxis(u_hstep, -1))) + self.assertTrue(numpy.allclose(u_estep.sum(), u0), msg='u_estep: {}\n{}'.format(u_estep.sum(), numpy.rollaxis(u_estep, -1))) def test_poynting_divergence(self): @@ -63,6 +63,9 @@ class BasicTests(): du_half_h2e = u_estep - u_hstep div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4)) + self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4), + msg='du_half_h2e\n{}\ndiv_s_h2e\n{}'.format(numpy.rollaxis(du_half_h2e, -1), + -numpy.rollaxis(div_s_h2e, -1))) if u_eprev is None: u_eprev = u_estep @@ -72,6 +75,9 @@ class BasicTests(): du_half_e2h = u_hstep - u_eprev div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4)) + self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4), + msg='du_half_e2h\n{}\ndiv_s_e2h\n{}'.format(numpy.rollaxis(du_half_e2h, -1), + -numpy.rollaxis(div_s_e2h, -1))) u_eprev = u_estep @@ -95,8 +101,8 @@ class BasicTests(): planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), s_h2e[py].sum(), -s_h2e[my].sum(), s_h2e[pz].sum(), -s_h2e[mz].sum()] - self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) -# print(planes, '\n', numpy.rollaxis(u_estep - u_hstep, -1), sum(planes)) + self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]]), + msg='planes: {} (sum: {})\n du:\n {}'.format(planes, sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): From 89976647f2a5fdbcbcc62c03c4ef3ae2c7594269 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 22 Jul 2019 00:27:32 -0700 Subject: [PATCH 097/437] test (and fix tests) for constant non-1 dxes Still need to look at non-constant dxes --- fdfd_tools/test_fdtd.py | 78 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 247bd9c..fb29cfb 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -54,6 +54,9 @@ class BasicTests(): args = {'dxes': self.dxes, 'epsilon': self.epsilon} + dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in self.epsilon.shape[1:]) for _ in range(2)) + dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) + u_eprev = None for ii in range(1, 8): with self.subTest(i=ii): @@ -61,8 +64,7 @@ class BasicTests(): u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) du_half_h2e = u_estep - u_hstep - div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) - self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4)) + div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) * dV self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4), msg='du_half_h2e\n{}\ndiv_s_h2e\n{}'.format(numpy.rollaxis(du_half_h2e, -1), -numpy.rollaxis(div_s_h2e, -1))) @@ -73,8 +75,7 @@ class BasicTests(): # previous half-step du_half_e2h = u_hstep - u_eprev - div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) - self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4)) + div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) * dV self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4), msg='du_half_e2h\n{}\ndiv_s_e2h\n{}'.format(numpy.rollaxis(du_half_e2h, -1), -numpy.rollaxis(div_s_e2h, -1))) @@ -84,6 +85,8 @@ class BasicTests(): def test_poynting_planes(self): args = {'dxes': self.dxes, 'epsilon': self.epsilon} + dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in self.epsilon.shape[1:]) for _ in range(2)) + dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) u_eprev = None for ii in range(1, 8): @@ -98,6 +101,9 @@ class BasicTests(): py = self.src_mask.copy() pz = numpy.roll(self.src_mask, +1, axis=0) s_h2e = -fdtd.poynting(e=self.es[ii], h=self.hs[ii]) * self.dt + s_h2e[0] *= dxes[0][1][None, :, None] * dxes[0][2][None, None, :] + s_h2e[1] *= dxes[0][0][:, None, None] * dxes[0][2][None, None, :] + s_h2e[2] *= dxes[0][0][:, None, None] * dxes[0][1][None, :, None] planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), s_h2e[py].sum(), -s_h2e[my].sum(), s_h2e[pz].sum(), -s_h2e[mz].sum()] @@ -133,6 +139,36 @@ class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): self.hs.append(h) +class Basic2DUniformDX3(unittest.TestCase, BasicTests): + def setUp(self): + shape = [3, 5, 5, 1] + self.dt = 0.5 + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.full(s, 2.0) for s in shape[1:]) for _ in range(2)) + + self.src_mask = numpy.zeros(shape, dtype=bool) + self.src_mask[1, 2, 2, 0] = True + + self.epsilon = numpy.full(shape, 1, dtype=float) + self.epsilon[self.src_mask] = 2 + + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] + + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) + + class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 5] @@ -220,6 +256,40 @@ class Basic3DUniformDX(unittest.TestCase, BasicTests): self.hs.append(h) +class Basic3DUniformDX3(unittest.TestCase, BasicTests): + def setUp(self): + shape = [3, 5, 5, 5] + self.dt = 0.5 + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.full(s, 3.0) for s in shape[1:]) for _ in range(2)) + + self.src_mask = numpy.zeros(shape, dtype=bool) + self.src_mask[1, 2, 2, 2] = True + + self.epsilon = numpy.full(shape, 1, dtype=float) + self.epsilon[self.src_mask] = 2 + + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] + + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) + logging.basicConfig(level=logging.DEBUG) + + def tearDown(self): + logging.basicConfig(level=logging.INFO) + + class JdotE_3DUniformDX(unittest.TestCase): def setUp(self): shape = [3, 5, 5, 5] From 39c05d2cabbbf15255c6f575e985c6568720ce51 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 22 Jul 2019 00:27:48 -0700 Subject: [PATCH 098/437] no reason to demand float32 yet --- fdfd_tools/test_fdtd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index fb29cfb..6628ab6 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -303,8 +303,8 @@ class JdotE_3DUniformDX(unittest.TestCase): self.epsilon = numpy.full(shape, 4, dtype=float) self.epsilon[self.src_mask] = 2 - e = numpy.random.randint(-128, 128 + 1, size=shape).astype(numpy.float32) - h = numpy.random.randint(-128, 128 + 1, size=shape).astype(numpy.float32) + e = numpy.random.randint(-128, 128 + 1, size=shape).astype(float) + h = numpy.random.randint(-128, 128 + 1, size=shape).astype(float) self.es = [e] self.hs = [h] From b4bbfdb7300f44cbe01660a8f941609cabd3f884 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 24 Jul 2019 22:42:11 -0700 Subject: [PATCH 099/437] remove old logging stuff --- fdfd_tools/test_fdtd.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 6628ab6..879d3d4 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -284,10 +284,6 @@ class Basic3DUniformDX3(unittest.TestCase, BasicTests): eh2e(e, h, self.epsilon) self.es.append(e) self.hs.append(h) - logging.basicConfig(level=logging.DEBUG) - - def tearDown(self): - logging.basicConfig(level=logging.INFO) class JdotE_3DUniformDX(unittest.TestCase): From f2d061c9210b1495e9e420d7e4cab1dacc9c7adb Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 24 Jul 2019 22:42:36 -0700 Subject: [PATCH 100/437] Test poynting planes on both half-steps --- fdfd_tools/test_fdtd.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 879d3d4..15c5b4d 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -88,18 +88,19 @@ class BasicTests(): dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in self.epsilon.shape[1:]) for _ in range(2)) dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) + mx = numpy.roll(self.src_mask, (-1, -1), axis=(0, 1)) + my = numpy.roll(self.src_mask, -1, axis=2) + mz = numpy.roll(self.src_mask, (+1, -1), axis=(0, 3)) + px = numpy.roll(self.src_mask, -1, axis=0) + py = self.src_mask.copy() + pz = numpy.roll(self.src_mask, +1, axis=0) + u_eprev = None for ii in range(1, 8): with self.subTest(i=ii): u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - mx = numpy.roll(self.src_mask, (-1, -1), axis=(0, 1)) - my = numpy.roll(self.src_mask, -1, axis=2) - mz = numpy.roll(self.src_mask, (+1, -1), axis=(0, 3)) - px = numpy.roll(self.src_mask, -1, axis=0) - py = self.src_mask.copy() - pz = numpy.roll(self.src_mask, +1, axis=0) s_h2e = -fdtd.poynting(e=self.es[ii], h=self.hs[ii]) * self.dt s_h2e[0] *= dxes[0][1][None, :, None] * dxes[0][2][None, None, :] s_h2e[1] *= dxes[0][0][:, None, None] * dxes[0][2][None, None, :] @@ -110,6 +111,23 @@ class BasicTests(): self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]]), msg='planes: {} (sum: {})\n du:\n {}'.format(planes, sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) + if u_eprev is None: + u_eprev = u_estep + continue + + s_e2h = -fdtd.poynting(e=self.es[ii - 1], h=self.hs[ii]) * self.dt + s_e2h[0] *= dxes[0][1][None, :, None] * dxes[0][2][None, None, :] + s_e2h[1] *= dxes[0][0][:, None, None] * dxes[0][2][None, None, :] + s_e2h[2] *= dxes[0][0][:, None, None] * dxes[0][1][None, :, None] + planes = [s_e2h[px].sum(), -s_e2h[mx].sum(), + s_e2h[py].sum(), -s_e2h[my].sum(), + s_e2h[pz].sum(), -s_e2h[mz].sum()] + self.assertTrue(numpy.allclose(sum(planes), (u_hstep - u_eprev)[self.src_mask[1]]), + msg='planes: {} (sum: {})\n du:\n {}'.format(planes, sum(planes), (u_hstep - u_eprev)[self.src_mask[1]])) + + # previous half-step + u_eprev = u_estep + class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): From 56a1349959c5f00d142a471ccb6603452140823a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 1 Aug 2019 23:16:32 -0700 Subject: [PATCH 101/437] Add missing return --- fdfd_tools/waveguide_mode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index aac718d..398f2eb 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -425,6 +425,8 @@ def compute_source_wg(E: field_t, omega=omega, dxes=dxes, axis=axis, polarity=polarity, slices=slices4, epsilon=epsilon, mu=mu) + return J + def compute_overlap_ce(E: field_t, wavenumber: complex, From 1d9c9644ee6d243e0674b452c765bd6121ca9d2a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 1 Aug 2019 23:17:13 -0700 Subject: [PATCH 102/437] input shouldn't be sliced with expanded slices --- fdfd_tools/waveguide_mode.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 398f2eb..2db9d68 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -479,7 +479,9 @@ def expand_wgmode_e(E: field_t, slices_exp[axis] = slice(E[0].shape[axis]) slices_exp = (slice(3), *slices_exp) - Ee[slices_exp] = phase_E * numpy.array(E)[slices_Exp] + slices_in = tuple(slice(3), *slices) + + Ee[slices_exp] = phase_E * numpy.array(E)[slices_in] return Ee From 1793e8cc3755ba12d85234a7169cb9b526390d6e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 1 Aug 2019 23:48:25 -0700 Subject: [PATCH 103/437] move to 3xNxMxP arrays --- fdfd_tools/waveguide_mode.py | 63 ++++++++++++++---------------------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 2db9d68..5400f30 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -99,7 +99,7 @@ def solve_waveguide_mode(mode_number: int, :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ if mu is None: - mu = [numpy.ones_like(epsilon[0])] * 3 + mu = numpy.ones_like(epsilon) slices = tuple(slices) @@ -131,18 +131,14 @@ def solve_waveguide_mode(mode_number: int, # Apply phase shift to H-field d_prop = 0.5 * sum(dxab_forward) - for a in range(3): - fields_2d['H'][a] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * d_prop) + fields_2d['H'] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * d_prop) # Expand E, H to full epsilon space we were given - E = [None]*3 - H = [None]*3 + E = numpy.zeros_like(epsilon, dtype=complex) + H = numpy.zeros_like(epsilon, dtype=complex) for a, o in enumerate(reverse_order): - E[a] = numpy.zeros_like(epsilon[0], dtype=complex) - H[a] = numpy.zeros_like(epsilon[0], dtype=complex) - - E[a][slices] = fields_2d['E'][o][:, :, None].transpose(reverse_order) - H[a][slices] = fields_2d['H'][o][:, :, None].transpose(reverse_order) + E[(a, *slices)] = fields_2d['E'][o][:, :, None].transpose(reverse_order) + H[(a, *slices)] = fields_2d['H'][o][:, :, None].transpose(reverse_order) results = { 'wavenumber': fields_2d['wavenumber'], @@ -180,27 +176,24 @@ def compute_source(E: field_t, :return: J distribution for the unidirectional source """ if mu is None: - mu = [1] * 3 + mu = numpy.ones(3) - J = [None]*3 - M = [None]*3 + J = numpy.zeros_like(E, dtype=complex) + M = numpy.zeros_like(E, dtype=complex) src_order = numpy.roll(range(3), -axis) exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) - J[src_order[0]] = numpy.zeros_like(E[0]) J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity rollby = -1 if polarity > 0 else 0 - M[src_order[0]] = numpy.zeros_like(E[0]) M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) M[src_order[2]] = -numpy.roll(E[src_order[1]], rollby, axis=axis) m2j = functional.m2j(omega, dxes, mu) Jm = m2j(M) - Jtot = [ji + jmi for ji, jmi in zip(J, Jm)] - + Jtot = J + Jm return Jtot @@ -236,8 +229,9 @@ def compute_overlap_e(E: field_t, """ slices = tuple(slices) - cross_plane = [slice(None)] * 3 - cross_plane[axis] = slices[axis] + cross_plane = [slice(None)] * 4 + cross_plane[axis + 1] = slices[axis] + cross_plane = tuple(cross_plane) # Determine phase factors for parallel slices a_shape = numpy.roll([-1, 1, 1], axis) @@ -248,11 +242,8 @@ def compute_overlap_e(E: field_t, phase_H = numpy.exp(iphi * (a_H - a_H[slices[axis]])).reshape(a_shape) # Expand our slice to the entire grid using the calculated phase factors - Ee = [None]*3 - He = [None]*3 - for k in range(3): - Ee[k] = phase_E * E[k][tuple(cross_plane)] - He[k] = phase_H * H[k][tuple(cross_plane)] + Ee = phase_E * E[cross_plane] + He = phase_H * H[cross_plane] # Write out the operator product for the mode orthogonality integral @@ -359,17 +350,16 @@ def compute_source_q(E: field_t, A2f = functional.curl_e(dxes) J = A1f(H) - M = A2f([-E[i] for i in range(3)]) + M = A2f(-E) m2j = functional.m2j(omega, dxes, mu) Jm = m2j(M) - Jtot = [ji + jmi for ji, jmi in zip(J, Jm)] + Jtot = J + Jm return Jtot, J, M - def compute_source_e(QE: field_t, omega: complex, dxes: dx_lists_t, @@ -389,16 +379,15 @@ def compute_source_e(QE: field_t, # Trim a cell from each end of the propagation axis slices_reduced = list(slices) slices_reduced[axis] = slice(slices[axis].start + 1, slices[axis].stop - 1) - slices_reduced = tuple(slices) + slices_reduced = tuple(slice(None), *slices) # Don't actually need to mask out E here since it needs to be pre-masked (QE) A = functional.e_full(omega, dxes, epsilon, mu) - J4 = [ji / (-1j * omega) for ji in A(QE)] #J4 is 4-cell result of -iwJ = A QE + J4 = A(QE) / (-1j * omega) #J4 is 4-cell result of -iwJ = A QE J = numpy.zeros_like(J4) - for a in range(3): - J[a][slices_reduced] = J4[a][slices_reduced] + J[slices_reduced] = J4[slices_reduced] return J @@ -445,14 +434,12 @@ def compute_overlap_ce(E: field_t, slices2 = list(slices) slices2[axis] = slice(start, stop) - slices2 = tuple(slices2) + slices2 = tuple(slice(None), slices2) Etgt = numpy.zeros_like(Ee) - for a in range(3): - Etgt[a][slices2] = Ee[a][slices2] + Etgt[slices2] = Ee[slices2] Etgt /= (Etgt.conj() * Etgt).sum() - return Etgt, slices2 @@ -476,10 +463,10 @@ def expand_wgmode_e(E: field_t, Ee = numpy.zeros_like(E) slices_exp = list(slices) - slices_exp[axis] = slice(E[0].shape[axis]) - slices_exp = (slice(3), *slices_exp) + slices_exp[axis] = slice(E.shape[axis + 1]) + slices_exp = (slice(None), *slices_exp) - slices_in = tuple(slice(3), *slices) + slices_in = tuple(slice(None), *slices) Ee[slices_exp] = phase_E * numpy.array(E)[slices_in] From 148930883770de8fa7272c3c49100dc8b3baabf2 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 3 Aug 2019 12:11:45 -0700 Subject: [PATCH 104/437] Comment capitalization fix --- fdfd_tools/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 5400f30..d12ffbf 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -370,7 +370,7 @@ def compute_source_e(QE: field_t, mu: field_t = None, ) -> field_t: """ - Want (AQ-QA) E = -iwj, where Q is a mask + Want (AQ-QA) E = -iwJ, where Q is a mask If E is an eigenmode, AE = 0 so just AQE = -iwJ Really only need E in 4 cells along axis (0, 0, Emode1, Emode2), find AE (1 fdtd step), then use center 2 cells as src """ From 06a491a96005874be4ba0c41a2a437824817e485 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 3 Aug 2019 12:12:18 -0700 Subject: [PATCH 105/437] don't throw out our newly-reduced slices... --- fdfd_tools/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index d12ffbf..0b839d8 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -379,7 +379,7 @@ def compute_source_e(QE: field_t, # Trim a cell from each end of the propagation axis slices_reduced = list(slices) slices_reduced[axis] = slice(slices[axis].start + 1, slices[axis].stop - 1) - slices_reduced = tuple(slice(None), *slices) + slices_reduced = tuple(slice(None), *slices_reduced) # Don't actually need to mask out E here since it needs to be pre-masked (QE) From 32055ec8d302ccb0d2187bdcfa686f32f7bfe192 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 02:50:09 -0700 Subject: [PATCH 106/437] Use pytest for testing; generalize existing fdtd tests --- fdfd_tools/test/test_fdtd.py | 308 ++++++++++++++++++++++++++++++ fdfd_tools/test_fdtd.py | 353 ----------------------------------- 2 files changed, 308 insertions(+), 353 deletions(-) create mode 100644 fdfd_tools/test/test_fdtd.py delete mode 100644 fdfd_tools/test_fdtd.py diff --git a/fdfd_tools/test/test_fdtd.py b/fdfd_tools/test/test_fdtd.py new file mode 100644 index 0000000..53fe9f8 --- /dev/null +++ b/fdfd_tools/test/test_fdtd.py @@ -0,0 +1,308 @@ +import numpy +import pytest +import dataclasses +from typing import List, Tuple +from numpy.testing import assert_allclose, assert_array_equal + +from fdfd_tools import fdtd + + +prng = numpy.random.RandomState(12345) + +def assert_fields_close(a, b, *args, **kwargs): + numpy.testing.assert_allclose(a, b, verbose=False, err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(a, -1), + numpy.rollaxis(b, -1)), *args, **kwargs) + +def assert_close(a, b, *args, **kwargs): + numpy.testing.assert_allclose(a, b, *args, **kwargs) + + +def test_initial_fields(sim): + # Make sure initial fields didn't change + e0 = sim.es[0] + h0 = sim.hs[0] + j0 = sim.js[0] + mask = (j0 != 0) + + assert_fields_close(e0[mask], j0[mask] / sim.epsilon[mask]) + assert not e0[~mask].any() + assert not h0.any() + + +def test_initial_energy(sim): + """ + Assumes fields start at 0 before J0 is added + """ + j0 = sim.js[0] + e0 = sim.es[0] + h0 = sim.hs[0] + h1 = sim.hs[1] + mask = (j0 != 0) + dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0) + u0 = (j0 * j0.conj() / sim.epsilon * dV).sum(axis=0) + args = {'dxes': sim.dxes, + 'epsilon': sim.epsilon} + + # Make sure initial energy and E dot J are correct + energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=h1, **args) + e0_dot_j0 = fdtd.delta_energy_j(j0=j0, e1=e0, dxes=sim.dxes) + assert_fields_close(energy0, u0) + assert_fields_close(e0_dot_j0, u0) + + +def test_energy_conservation(sim): + """ + Assumes fields start at 0 before J0 is added + """ + e0 = sim.es[0] + j0 = sim.js[0] + u = fdtd.delta_energy_j(j0=j0, e1=e0, dxes=sim.dxes).sum() + args = {'dxes': sim.dxes, + 'epsilon': sim.epsilon} + + for ii in range(1, 8): + u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii-1], dxes=sim.dxes) + delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) + + u += delta_j_A.sum() + assert_close(u_hstep.sum(), u) + u += delta_j_B.sum() + assert_close(u_estep.sum(), u) + + +def test_poynting_divergence(sim): + args = {'dxes': sim.dxes, + 'epsilon': sim.epsilon} + + dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0) + + u_eprev = None + for ii in range(1, 8): + u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) + + du_half_h2e = u_estep - u_hstep - delta_j_B + div_s_h2e = sim.dt * fdtd.poynting_divergence(e=sim.es[ii], h=sim.hs[ii], dxes=sim.dxes) * dV + assert_fields_close(du_half_h2e, -div_s_h2e, rtol=1e-4) + + if u_eprev is None: + u_eprev = u_estep + continue + + # previous half-step + delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii-1], dxes=sim.dxes) + + du_half_e2h = u_hstep - u_eprev - delta_j_A + div_s_e2h = sim.dt * fdtd.poynting_divergence(e=sim.es[ii-1], h=sim.hs[ii], dxes=sim.dxes) * dV + assert_fields_close(du_half_e2h, -div_s_e2h, rtol=1e-4) + u_eprev = u_estep + + +def test_poynting_planes(sim): + mask = (sim.js[0] != 0) + if mask.sum() > 1: + pytest.skip('test_poynting_planes can only test single point sources') + + args = {'dxes': sim.dxes, + 'epsilon': sim.epsilon} + dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0) + + mx = numpy.roll(mask, (-1, -1), axis=(0, 1)) + my = numpy.roll(mask, -1, axis=2) + mz = numpy.roll(mask, (+1, -1), axis=(0, 3)) + px = numpy.roll(mask, -1, axis=0) + py = mask.copy() + pz = numpy.roll(mask, +1, axis=0) + + u_eprev = None + for ii in range(1, 8): + u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + + s_h2e = -fdtd.poynting(e=sim.es[ii], h=sim.hs[ii]) * sim.dt + s_h2e[0] *= sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] + s_h2e[1] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][2][None, None, :] + s_h2e[2] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] + planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), + s_h2e[py].sum(), -s_h2e[my].sum(), + s_h2e[pz].sum(), -s_h2e[mz].sum()] + assert_close(sum(planes), (u_estep - u_hstep).sum()) + if u_eprev is None: + u_eprev = u_estep + continue + + s_e2h = -fdtd.poynting(e=sim.es[ii - 1], h=sim.hs[ii]) * sim.dt + s_e2h[0] *= sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] + s_e2h[1] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][2][None, None, :] + s_e2h[2] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] + planes = [s_e2h[px].sum(), -s_e2h[mx].sum(), + s_e2h[py].sum(), -s_e2h[my].sum(), + s_e2h[pz].sum(), -s_e2h[mz].sum()] + assert_close(sum(planes), (u_hstep - u_eprev).sum()) + + # previous half-step + u_eprev = u_estep + +## Now tested elsewhere +#def test_j_dot_e(sim): +# for tt in sim.j_steps: +# e0 = sim.es[tt - 1] +# j1 = sim.js[tt] +# e1 = sim.es[tt] +# +# delta_j_A = fdtd.delta_energy_j(j0=j1, e1=e0, dxes=sim.dxes) +# delta_j_B = fdtd.delta_energy_j(j0=j1, e1=e1, dxes=sim.dxes) +# +# args = {'dxes': sim.dxes, +# 'epsilon': sim.epsilon} +# +# u_eprev = fdtd.energy_estep(h0=sim.hs[tt-1], e1=sim.es[tt-1], h2=sim.hs[tt], **args) +# u_hstep = fdtd.energy_hstep(e0=sim.es[tt-1], h1=sim.hs[tt], e2=sim.es[tt], **args) +# u_estep = fdtd.energy_estep(h0=sim.hs[tt], e1=sim.es[tt], h2=sim.hs[tt + 1], **args) +# +# assert_close(delta_j_A.sum(), (u_hstep - u_eprev).sum(), rtol=1e-4) +# assert_close(delta_j_B.sum(), (u_estep - u_hstep).sum(), rtol=1e-4) + + +@pytest.fixture(scope='module', + params=[(5, 5, 1), + (5, 1, 5), + (5, 5, 5), +# (7, 7, 7), + ]) +def shape(request): + yield (3, *request.param) + + +@pytest.fixture(scope='module', params=[0.3]) +def dt(request): + yield request.param + + +@pytest.fixture(scope='module', params=[1.0, 1.5]) +def epsilon_bg(request): + yield request.param + + +@pytest.fixture(scope='module', params=[1.0, 2.5]) +def epsilon_fg(request): + yield request.param + + +@pytest.fixture(scope='module', params=['center', '000', 'random']) +def epsilon(request, shape, epsilon_bg, epsilon_fg): + is3d = (numpy.array(shape) == 1).sum() == 0 + if is3d: + if request.param == '000': + pytest.skip('Skipping 000 epsilon because test is 3D (for speed)') + if epsilon_bg != 1: + pytest.skip('Skipping epsilon_bg != 1 because test is 3D (for speed)') + if epsilon_fg not in (1.0, 2.0): + pytest.skip('Skipping epsilon_fg not in (1, 2) because test is 3D (for speed)') + + epsilon = numpy.full(shape, epsilon_bg, dtype=float) + if request.param == 'center': + epsilon[:, shape[1]//2, shape[2]//2, shape[3]//2] = epsilon_fg + elif request.param == '000': + epsilon[:, 0, 0, 0] = epsilon_fg + elif request.param == 'random': + epsilon[:] = prng.uniform(low=min(epsilon_bg, epsilon_fg), + high=max(epsilon_bg, epsilon_fg), + size=shape) + + yield epsilon + + +@pytest.fixture(scope='module', params=[1.0])#, 1.5]) +def j_mag(request): + yield request.param + + +@pytest.fixture(scope='module', params=['center', 'random']) +def j_distribution(request, shape, j_mag): + j = numpy.zeros(shape) + if request.param == 'center': + j[:, shape[1]//2, shape[2]//2, shape[3]//2] = j_mag + elif request.param == '000': + j[:, 0, 0, 0] = j_mag + elif request.param == 'random': + j[:] = prng.uniform(low=-j_mag, high=j_mag, size=shape) + yield j + + +@pytest.fixture(scope='module', params=[1.0, 1.5]) +def dx(request): + yield request.param + + +@pytest.fixture(scope='module', params=['uniform']) +def dxes(request, shape, dx): + if request.param == 'uniform': + dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] + yield dxes + + +@pytest.fixture(scope='module', + params=[(0,), + (0, 4, 8), + ] + ) +def j_steps(request): + yield request.param + + +@dataclasses.dataclass() +class SimResult: + shape: Tuple[int] + dt: float + dxes: List[List[numpy.ndarray]] + epsilon: numpy.ndarray + j_distribution: numpy.ndarray + j_steps: Tuple[int] + es: List[numpy.ndarray] = dataclasses.field(default_factory=list) + hs: List[numpy.ndarray] = dataclasses.field(default_factory=list) + js: List[numpy.ndarray] = dataclasses.field(default_factory=list) + + +@pytest.fixture(scope='module') +def sim(request, shape, epsilon, dxes, dt, j_distribution, j_steps): + is3d = (numpy.array(shape) == 1).sum() == 0 + if is3d: + if dt != 0.3: + pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)') + + sim = SimResult( + shape=shape, + dt=dt, + dxes=dxes, + epsilon=epsilon, + j_distribution=j_distribution, + j_steps=j_steps, + ) + + e = numpy.zeros_like(epsilon) + h = numpy.zeros_like(epsilon) + + assert 0 in j_steps + j_zeros = numpy.zeros_like(j_distribution) + + eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) + eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + for tt in range(10): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, epsilon) + if tt in j_steps: + e += j_distribution / epsilon + sim.js.append(j_distribution) + else: + sim.js.append(j_zeros) + sim.es.append(e) + sim.hs.append(h) + return sim + + diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py deleted file mode 100644 index 15c5b4d..0000000 --- a/fdfd_tools/test_fdtd.py +++ /dev/null @@ -1,353 +0,0 @@ -import unittest -import numpy - -from fdfd_tools import fdtd - - -class BasicTests(): - def test_initial_fields(self): - # Make sure initial fields didn't change - e0 = self.es[0] - h0 = self.hs[0] - mask = self.src_mask - - self.assertEqual(e0[mask], self.j_mag / self.epsilon[mask]) - self.assertFalse(e0[~mask].any()) - self.assertFalse(h0.any()) - - - def test_initial_energy(self): - e0 = self.es[0] - h0 = self.hs[0] - h1 = self.hs[1] - mask = self.src_mask[1] - dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in e0.shape[1:]) for _ in range(2)) - dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) - u0 = self.j_mag * self.j_mag / self.epsilon[self.src_mask] * dV[mask] - args = {'dxes': self.dxes, - 'epsilon': self.epsilon} - - # Make sure initial energy and E dot J are correct - energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=self.hs[1], **args) - e_dot_j_0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0, dxes=self.dxes) - self.assertEqual(energy0[mask], u0) - self.assertFalse(energy0[~mask].any(), msg='energy0: {}'.format(energy0)) - self.assertEqual(e_dot_j_0[mask], u0) - self.assertFalse(e_dot_j_0[~mask].any(), msg='e_dot_j_0: {}'.format(e_dot_j_0)) - - - def test_energy_conservation(self): - e0 = self.es[0] - u0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0, dxes=self.dxes).sum() - args = {'dxes': self.dxes, - 'epsilon': self.epsilon} - - for ii in range(1, 8): - with self.subTest(i=ii): - u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) - u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - self.assertTrue(numpy.allclose(u_hstep.sum(), u0), msg='u_hstep: {}\n{}'.format(u_hstep.sum(), numpy.rollaxis(u_hstep, -1))) - self.assertTrue(numpy.allclose(u_estep.sum(), u0), msg='u_estep: {}\n{}'.format(u_estep.sum(), numpy.rollaxis(u_estep, -1))) - - - def test_poynting_divergence(self): - args = {'dxes': self.dxes, - 'epsilon': self.epsilon} - - dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in self.epsilon.shape[1:]) for _ in range(2)) - dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) - - u_eprev = None - for ii in range(1, 8): - with self.subTest(i=ii): - u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) - u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - - du_half_h2e = u_estep - u_hstep - div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) * dV - self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4), - msg='du_half_h2e\n{}\ndiv_s_h2e\n{}'.format(numpy.rollaxis(du_half_h2e, -1), - -numpy.rollaxis(div_s_h2e, -1))) - - if u_eprev is None: - u_eprev = u_estep - continue - - # previous half-step - du_half_e2h = u_hstep - u_eprev - div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) * dV - self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4), - msg='du_half_e2h\n{}\ndiv_s_e2h\n{}'.format(numpy.rollaxis(du_half_e2h, -1), - -numpy.rollaxis(div_s_e2h, -1))) - u_eprev = u_estep - - - def test_poynting_planes(self): - args = {'dxes': self.dxes, - 'epsilon': self.epsilon} - dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in self.epsilon.shape[1:]) for _ in range(2)) - dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) - - mx = numpy.roll(self.src_mask, (-1, -1), axis=(0, 1)) - my = numpy.roll(self.src_mask, -1, axis=2) - mz = numpy.roll(self.src_mask, (+1, -1), axis=(0, 3)) - px = numpy.roll(self.src_mask, -1, axis=0) - py = self.src_mask.copy() - pz = numpy.roll(self.src_mask, +1, axis=0) - - u_eprev = None - for ii in range(1, 8): - with self.subTest(i=ii): - u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) - u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - - s_h2e = -fdtd.poynting(e=self.es[ii], h=self.hs[ii]) * self.dt - s_h2e[0] *= dxes[0][1][None, :, None] * dxes[0][2][None, None, :] - s_h2e[1] *= dxes[0][0][:, None, None] * dxes[0][2][None, None, :] - s_h2e[2] *= dxes[0][0][:, None, None] * dxes[0][1][None, :, None] - planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), - s_h2e[py].sum(), -s_h2e[my].sum(), - s_h2e[pz].sum(), -s_h2e[mz].sum()] - self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]]), - msg='planes: {} (sum: {})\n du:\n {}'.format(planes, sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) - - if u_eprev is None: - u_eprev = u_estep - continue - - s_e2h = -fdtd.poynting(e=self.es[ii - 1], h=self.hs[ii]) * self.dt - s_e2h[0] *= dxes[0][1][None, :, None] * dxes[0][2][None, None, :] - s_e2h[1] *= dxes[0][0][:, None, None] * dxes[0][2][None, None, :] - s_e2h[2] *= dxes[0][0][:, None, None] * dxes[0][1][None, :, None] - planes = [s_e2h[px].sum(), -s_e2h[mx].sum(), - s_e2h[py].sum(), -s_e2h[my].sum(), - s_e2h[pz].sum(), -s_e2h[mz].sum()] - self.assertTrue(numpy.allclose(sum(planes), (u_hstep - u_eprev)[self.src_mask[1]]), - msg='planes: {} (sum: {})\n du:\n {}'.format(planes, sum(planes), (u_hstep - u_eprev)[self.src_mask[1]])) - - # previous half-step - u_eprev = u_estep - - -class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 1] - self.dt = 0.5 - self.epsilon = numpy.ones(shape, dtype=float) - self.j_mag = 32 - self.dxes = None - - self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) - self.src_mask[1, 2, 2, 0] = True - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - -class Basic2DUniformDX3(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 1] - self.dt = 0.5 - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.full(s, 2.0) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros(shape, dtype=bool) - self.src_mask[1, 2, 2, 0] = True - - self.epsilon = numpy.full(shape, 1, dtype=float) - self.epsilon[self.src_mask] = 2 - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - -class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 5] - self.dt = 0.5 - self.epsilon = numpy.ones(shape, dtype=float) - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) - self.src_mask[1, 2, 2, 2] = True - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - - -class Basic3DUniformDXUniformN(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 5] - self.dt = 0.5 - self.epsilon = numpy.full(shape, 2, dtype=float) - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) - self.src_mask[1, 2, 2, 2] = True - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - -class Basic3DUniformDX(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 5] - self.dt = 0.33 - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros(shape, dtype=bool) - self.src_mask[1, 2, 2, 2] = True - - self.epsilon = numpy.full(shape, 1, dtype=float) - self.epsilon[self.src_mask] = 2 - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - -class Basic3DUniformDX3(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 5] - self.dt = 0.5 - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.full(s, 3.0) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros(shape, dtype=bool) - self.src_mask[1, 2, 2, 2] = True - - self.epsilon = numpy.full(shape, 1, dtype=float) - self.epsilon[self.src_mask] = 2 - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - -class JdotE_3DUniformDX(unittest.TestCase): - def setUp(self): - shape = [3, 5, 5, 5] - self.dt = 0.5 - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.full(s, 2.0) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros(shape, dtype=bool) - self.src_mask[1, 2, 2, 2] = True - - self.epsilon = numpy.full(shape, 4, dtype=float) - self.epsilon[self.src_mask] = 2 - - e = numpy.random.randint(-128, 128 + 1, size=shape).astype(float) - h = numpy.random.randint(-128, 128 + 1, size=shape).astype(float) - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for ii in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - if ii == 1: - e[self.src_mask] += self.j_mag / self.epsilon[self.src_mask] - self.j_dot_e = self.j_mag * e[self.src_mask] - - - def test_j_dot_e(self): - e0 = self.es[2] - j0 = numpy.zeros_like(e0) - j0[self.src_mask] = self.j_mag - u0 = fdtd.delta_energy_j(j0=j0, e1=e0, dxes=self.dxes) - args = {'dxes': self.dxes, - 'epsilon': self.epsilon} - - ii=2 - u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) - u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - #print(u0.sum(), (u_estep - u_hstep).sum()) - self.assertTrue(numpy.allclose(u0.sum(), (u_estep - u_hstep).sum(), rtol=1e-4)) - From 557a3b0d9c407f44c2a33a9de673c96852f26141 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 02:53:04 -0700 Subject: [PATCH 107/437] Remove unused test code and tighten tolerances --- fdfd_tools/test/test_fdtd.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/fdfd_tools/test/test_fdtd.py b/fdfd_tools/test/test_fdtd.py index 53fe9f8..d74b8be 100644 --- a/fdfd_tools/test/test_fdtd.py +++ b/fdfd_tools/test/test_fdtd.py @@ -86,7 +86,7 @@ def test_poynting_divergence(sim): du_half_h2e = u_estep - u_hstep - delta_j_B div_s_h2e = sim.dt * fdtd.poynting_divergence(e=sim.es[ii], h=sim.hs[ii], dxes=sim.dxes) * dV - assert_fields_close(du_half_h2e, -div_s_h2e, rtol=1e-4) + assert_fields_close(du_half_h2e, -div_s_h2e) if u_eprev is None: u_eprev = u_estep @@ -97,7 +97,7 @@ def test_poynting_divergence(sim): du_half_e2h = u_hstep - u_eprev - delta_j_A div_s_e2h = sim.dt * fdtd.poynting_divergence(e=sim.es[ii-1], h=sim.hs[ii], dxes=sim.dxes) * dV - assert_fields_close(du_half_e2h, -div_s_e2h, rtol=1e-4) + assert_fields_close(du_half_e2h, -div_s_e2h) u_eprev = u_estep @@ -146,26 +146,10 @@ def test_poynting_planes(sim): # previous half-step u_eprev = u_estep -## Now tested elsewhere -#def test_j_dot_e(sim): -# for tt in sim.j_steps: -# e0 = sim.es[tt - 1] -# j1 = sim.js[tt] -# e1 = sim.es[tt] -# -# delta_j_A = fdtd.delta_energy_j(j0=j1, e1=e0, dxes=sim.dxes) -# delta_j_B = fdtd.delta_energy_j(j0=j1, e1=e1, dxes=sim.dxes) -# -# args = {'dxes': sim.dxes, -# 'epsilon': sim.epsilon} -# -# u_eprev = fdtd.energy_estep(h0=sim.hs[tt-1], e1=sim.es[tt-1], h2=sim.hs[tt], **args) -# u_hstep = fdtd.energy_hstep(e0=sim.es[tt-1], h1=sim.hs[tt], e2=sim.es[tt], **args) -# u_estep = fdtd.energy_estep(h0=sim.hs[tt], e1=sim.es[tt], h2=sim.hs[tt + 1], **args) -# -# assert_close(delta_j_A.sum(), (u_hstep - u_eprev).sum(), rtol=1e-4) -# assert_close(delta_j_B.sum(), (u_estep - u_hstep).sum(), rtol=1e-4) +##################################### +# Test fixtures +##################################### @pytest.fixture(scope='module', params=[(5, 5, 1), From 3d07969fd2a4ac40028dabfab54f020d94888662 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 03:06:14 -0700 Subject: [PATCH 108/437] rename examples to avoid triggering pytest --- examples/{test.py => fdfd.py} | 0 examples/{test_fdtd.py => fdtd.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename examples/{test.py => fdfd.py} (100%) rename examples/{test_fdtd.py => fdtd.py} (100%) diff --git a/examples/test.py b/examples/fdfd.py similarity index 100% rename from examples/test.py rename to examples/fdfd.py diff --git a/examples/test_fdtd.py b/examples/fdtd.py similarity index 100% rename from examples/test_fdtd.py rename to examples/fdtd.py From 25cb83089dd9947344ee649efd0ea5179af7aa66 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 03:06:32 -0700 Subject: [PATCH 109/437] modernize setup.py --- fdfd_tools/__init__.py | 1 + setup.py | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/fdfd_tools/__init__.py b/fdfd_tools/__init__.py index a4efa89..ecf4e15 100644 --- a/fdfd_tools/__init__.py +++ b/fdfd_tools/__init__.py @@ -23,3 +23,4 @@ from .vectorization import vec, unvec, field_t, vfield_t from .grid import dx_lists_t __author__ = 'Jan Petykiewicz' +version = '0.5' diff --git a/setup.py b/setup.py index ef1df08..2a64b37 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,36 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from setuptools import setup, find_packages +import fdfd_tools + +with open('README.md', 'r') as f: + long_description = f.read() setup(name='fdfd_tools', - version='0.4', + version=fdfd_tools.version, description='FDFD Electromagnetic simulation tools', + long_description=long_description, + long_description_content_type='text/markdown', author='Jan Petykiewicz', author_email='anewusername@gmail.com', - url='https://mpxd.net/gogs/jan/fdfd_tools', + url='https://mpxd.net/code/jan/fdfd_tools', packages=find_packages(), install_requires=[ 'numpy', 'scipy', ], extras_require={ + 'test': [ + 'pytest', + 'dataclasses', + ], }, + classifiers=[ + 'Programming Language :: Python :: 3', + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: GNU Affero General Public License v3', + 'Topic :: Scientific/Engineering :: Physics', + ], ) From f61bcf3dfa6874d09b5c4eecc9774b3cd6aaeb7c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 13:48:41 -0700 Subject: [PATCH 110/437] rename to meanas and split fdtd/fdfd --- README.md | 50 +-- fdfd_tools/__init__.py | 26 -- fdfd_tools/fdtd.py | 339 ------------------ meanas/__init__.py | 48 +++ {fdfd_tools => meanas}/eigensolvers.py | 0 {fdfd_tools => meanas/fdfd}/bloch.py | 0 {fdfd_tools => meanas/fdfd}/farfield.py | 0 {fdfd_tools => meanas/fdfd}/functional.py | 16 +- {fdfd_tools => meanas/fdfd}/operators.py | 30 +- fdfd_tools/grid.py => meanas/fdfd/scpml.py | 1 - {fdfd_tools => meanas/fdfd}/solvers.py | 2 +- {fdfd_tools => meanas/fdfd}/waveguide.py | 24 +- {fdfd_tools => meanas/fdfd}/waveguide_mode.py | 10 +- meanas/fdtd/__init__.py | 9 + meanas/fdtd/base.py | 87 +++++ meanas/fdtd/boundaries.py | 68 ++++ meanas/fdtd/energy.py | 84 +++++ meanas/fdtd/pml.py | 122 +++++++ {fdfd_tools => meanas}/test/test_fdtd.py | 2 +- meanas/types.py | 22 ++ {fdfd_tools => meanas}/vectorization.py | 10 +- setup.py | 8 +- 22 files changed, 519 insertions(+), 439 deletions(-) delete mode 100644 fdfd_tools/__init__.py delete mode 100644 fdfd_tools/fdtd.py create mode 100644 meanas/__init__.py rename {fdfd_tools => meanas}/eigensolvers.py (100%) rename {fdfd_tools => meanas/fdfd}/bloch.py (100%) rename {fdfd_tools => meanas/fdfd}/farfield.py (100%) rename {fdfd_tools => meanas/fdfd}/functional.py (87%) rename {fdfd_tools => meanas/fdfd}/operators.py (92%) rename fdfd_tools/grid.py => meanas/fdfd/scpml.py (99%) rename {fdfd_tools => meanas/fdfd}/solvers.py (97%) rename {fdfd_tools => meanas/fdfd}/waveguide.py (92%) rename {fdfd_tools => meanas/fdfd}/waveguide_mode.py (97%) create mode 100644 meanas/fdtd/__init__.py create mode 100644 meanas/fdtd/base.py create mode 100644 meanas/fdtd/boundaries.py create mode 100644 meanas/fdtd/energy.py create mode 100644 meanas/fdtd/pml.py rename {fdfd_tools => meanas}/test/test_fdtd.py (99%) create mode 100644 meanas/types.py rename {fdfd_tools => meanas}/vectorization.py (87%) diff --git a/README.md b/README.md index 5a3f49c..3ead0ca 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,56 @@ -# fdfd_tools +# meanas -**fdfd_tools** is a python package containing utilities for -creating and analyzing 2D and 3D finite-difference frequency-domain (FDFD) -electromagnetic simulations. +**meanas** is a python package for electromagnetic simulations + +This package is intended for building simulation inputs, analyzing +simulation outputs, and running short simulations on unspecialized hardware. +It is designed to provide tooling and a baseline for other, high-performance +purpose- and hardware-specific solvers. **Contents** -* Library of sparse matrices for representing the electromagnetic wave - equation in 3D, as well as auxiliary matrices for conversion between fields -* Waveguide mode solver and waveguide mode operators -* Stretched-coordinate PML boundaries (SCPML) -* Functional versions of most operators -* Anisotropic media (eps_xx, eps_yy, eps_zz, mu_xx, ...) -* Arbitrary distributions of perfect electric and magnetic conductors (PEC / PMC) +- Finite difference frequency domain (FDFD) + * Library of sparse matrices for representing the electromagnetic wave + equation in 3D, as well as auxiliary matrices for conversion between fields + * Waveguide mode operators + * Waveguide mode eigensolver + * Stretched-coordinate PML boundaries (SCPML) + * Functional versions of most operators + * Anisotropic media (limited to diagonal elements eps_xx, eps_yy, eps_zz, mu_xx, ...) + * Arbitrary distributions of perfect electric and magnetic conductors (PEC / PMC) +- Finite difference time domain (FDTD) + * Basic Maxwell time-steps + * Poynting vector and energy calculation + * Convolutional PMLs This package does *not* provide a fast matrix solver, though by default -```fdfd_tools.solvers.generic(...)``` will call -```scipy.sparse.linalg.qmr(...)``` to perform a solve. -For 2D problems this should be fine; likewise, the waveguide mode +`meanas.fdfd.solvers.generic(...)` will call +`scipy.sparse.linalg.qmr(...)` to perform a solve. +For 2D FDFD problems this should be fine; likewise, the waveguide mode solver uses scipy's eigenvalue solver, with reasonable results. -For solving large (or 3D) problems, I recommend a GPU-based iterative -solver, such as [opencl_fdfd](https://mpxd.net/gogs/jan/opencl_fdfd) or +For solving large (or 3D) FDFD problems, I recommend a GPU-based iterative +solver, such as [opencl_fdfd](https://mpxd.net/code/jan/opencl_fdfd) or those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html)). Your solver will need the ability to solve complex symmetric (non-Hermitian) linear systems, ideally with double precision. + ## Installation **Requirements:** -* python 3 (written and tested with 3.5) +* python 3 (tests require 3.7) * numpy * scipy Install with pip, via git: ```bash -pip install git+https://mpxd.net/gogs/jan/fdfd_tools.git@release +pip install git+https://mpxd.net/code/jan/meanas.git@release ``` ## Use -See examples/test.py for some simple examples; you may need additional -packages such as [gridlock](https://mpxd.net/gogs/jan/gridlock) +See `examples/` for some simple examples; you may need additional +packages such as [gridlock](https://mpxd.net/code/jan/gridlock) to run the examples. diff --git a/fdfd_tools/__init__.py b/fdfd_tools/__init__.py deleted file mode 100644 index ecf4e15..0000000 --- a/fdfd_tools/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Electromagnetic FDFD simulation tools - -Tools for 3D and 2D Electromagnetic Finite Difference Frequency Domain (FDFD) -simulations. These tools handle conversion of fields to/from vector form, -creation of the wave operator matrix, stretched-coordinate PMLs, PECs and PMCs, -field conversion operators, waveguide mode operator, and waveguide mode -solver. - -This package only contains a solver for the waveguide mode eigenproblem; -if you want to solve 3D problems you can use your favorite iterative sparse -matrix solver (so long as it can handle complex symmetric [non-Hermitian] -matrices, ideally with double precision). - - -Dependencies: -- numpy -- scipy - -""" - -from .vectorization import vec, unvec, field_t, vfield_t -from .grid import dx_lists_t - -__author__ = 'Jan Petykiewicz' -version = '0.5' diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py deleted file mode 100644 index 15b5635..0000000 --- a/fdfd_tools/fdtd.py +++ /dev/null @@ -1,339 +0,0 @@ -from typing import List, Callable, Tuple, Dict -import numpy - -from . import dx_lists_t, field_t - -#TODO fix pmls - -__author__ = 'Jan Petykiewicz' - - -functional_matrix = Callable[[field_t], field_t] - - -def curl_h(dxes: dx_lists_t = None) -> functional_matrix: - """ - Curl operator for use with the H field. - - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header - :return: Function for taking the discretized curl of the H-field, F(H) -> curlH - """ - if dxes: - dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') - - def dh(f, ax): - return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] - else: - def dh(f, ax): - return f - numpy.roll(f, 1, axis=ax) - - def ch_fun(h: field_t) -> field_t: - output = numpy.empty_like(h) - output[0] = dh(h[2], 1) - output[1] = dh(h[0], 2) - output[2] = dh(h[1], 0) - output[0] -= dh(h[1], 2) - output[1] -= dh(h[2], 0) - output[2] -= dh(h[0], 1) - return output - - return ch_fun - - -def curl_e(dxes: dx_lists_t = None) -> functional_matrix: - """ - Curl operator for use with the E field. - - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header - :return: Function for taking the discretized curl of the E-field, F(E) -> curlE - """ - if dxes is not None: - dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') - - def de(f, ax): - return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] - else: - def de(f, ax): - return numpy.roll(f, -1, axis=ax) - f - - def ce_fun(e: field_t) -> field_t: - output = numpy.empty_like(e) - output[0] = de(e[2], 1) - output[1] = de(e[0], 2) - output[2] = de(e[1], 0) - output[0] -= de(e[1], 2) - output[1] -= de(e[2], 0) - output[2] -= de(e[0], 1) - return output - - return ce_fun - - -def maxwell_e(dt: float, dxes: dx_lists_t = None) -> functional_matrix: - curl_h_fun = curl_h(dxes) - - def me_fun(e: field_t, h: field_t, epsilon: field_t): - e += dt * curl_h_fun(h) / epsilon - return e - - return me_fun - - -def maxwell_h(dt: float, dxes: dx_lists_t = None) -> functional_matrix: - curl_e_fun = curl_e(dxes) - - def mh_fun(e: field_t, h: field_t): - h -= dt * curl_e_fun(e) - return h - - return mh_fun - - -def conducting_boundary(direction: int, - polarity: int - ) -> Tuple[functional_matrix, functional_matrix]: - dirs = [0, 1, 2] - if direction not in dirs: - raise Exception('Invalid direction: {}'.format(direction)) - dirs.remove(direction) - u, v = dirs - - if polarity < 0: - boundary_slice = [slice(None)] * 3 - shifted1_slice = [slice(None)] * 3 - boundary_slice[direction] = 0 - shifted1_slice[direction] = 1 - - def en(e: field_t): - e[direction][boundary_slice] = 0 - e[u][boundary_slice] = e[u][shifted1_slice] - e[v][boundary_slice] = e[v][shifted1_slice] - return e - - def hn(h: field_t): - h[direction][boundary_slice] = h[direction][shifted1_slice] - h[u][boundary_slice] = 0 - h[v][boundary_slice] = 0 - return h - - return en, hn - - elif polarity > 0: - boundary_slice = [slice(None)] * 3 - shifted1_slice = [slice(None)] * 3 - shifted2_slice = [slice(None)] * 3 - boundary_slice[direction] = -1 - shifted1_slice[direction] = -2 - shifted2_slice[direction] = -3 - - def ep(e: field_t): - e[direction][boundary_slice] = -e[direction][shifted2_slice] - e[direction][shifted1_slice] = 0 - e[u][boundary_slice] = e[u][shifted1_slice] - e[v][boundary_slice] = e[v][shifted1_slice] - return e - - def hp(h: field_t): - h[direction][boundary_slice] = h[direction][shifted1_slice] - h[u][boundary_slice] = -h[u][shifted2_slice] - h[u][shifted1_slice] = 0 - h[v][boundary_slice] = -h[v][shifted2_slice] - h[v][shifted1_slice] = 0 - return h - - return ep, hp - - else: - raise Exception('Bad polarity: {}'.format(polarity)) - - -def cpml(direction:int, - polarity: int, - dt: float, - epsilon: field_t, - thickness: int = 8, - ln_R_per_layer: float = -1.6, - epsilon_eff: float = 1, - mu_eff: float = 1, - m: float = 3.5, - ma: float = 1, - cfs_alpha: float = 0, - dtype: numpy.dtype = numpy.float32, - ) -> Tuple[Callable, Callable, Dict[str, field_t]]: - - if direction not in range(3): - raise Exception('Invalid direction: {}'.format(direction)) - - if polarity not in (-1, 1): - raise Exception('Invalid polarity: {}'.format(polarity)) - - if thickness <= 2: - raise Exception('It would be wise to have a pml with 4+ cells of thickness') - - if epsilon_eff <= 0: - raise Exception('epsilon_eff must be positive') - - sigma_max = -ln_R_per_layer / 2 * (m + 1) - kappa_max = numpy.sqrt(epsilon_eff * mu_eff) - alpha_max = cfs_alpha - transverse = numpy.delete(range(3), direction) - u, v = transverse - - xe = numpy.arange(1, thickness+1, dtype=float) - xh = numpy.arange(1, thickness+1, dtype=float) - if polarity > 0: - xe -= 0.5 - elif polarity < 0: - xh -= 0.5 - xe = xe[::-1] - xh = xh[::-1] - else: - raise Exception('Bad polarity!') - - expand_slice = [None] * 3 - expand_slice[direction] = slice(None) - - def par(x): - scaling = (x / thickness) ** m - sigma = scaling * sigma_max - kappa = 1 + scaling * (kappa_max - 1) - alpha = ((1 - x / thickness) ** ma) * alpha_max - p0 = numpy.exp(-(sigma / kappa + alpha) * dt) - p1 = sigma / (sigma + kappa * alpha) * (p0 - 1) - p2 = 1 / kappa - return p0[expand_slice], p1[expand_slice], p2[expand_slice] - - p0e, p1e, p2e = par(xe) - p0h, p1h, p2h = par(xh) - - region = [slice(None)] * 3 - if polarity < 0: - region[direction] = slice(None, thickness) - elif polarity > 0: - region[direction] = slice(-thickness, None) - else: - raise Exception('Bad polarity!') - - se = 1 if direction == 1 else -1 - - # TODO check if epsilon is uniform in pml region? - shape = list(epsilon[0].shape) - shape[direction] = thickness - psi_e = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] - psi_h = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] - - fields = { - 'psi_e_u': psi_e[0], - 'psi_e_v': psi_e[1], - 'psi_h_u': psi_h[0], - 'psi_h_v': psi_h[1], - } - - # Note that this is kinda slow -- would be faster to reuse dHv*p2h for the original - # H update, but then you have multiple arrays and a monolithic (field + pml) update operation - def pml_e(e: field_t, h: field_t, epsilon: field_t) -> Tuple[field_t, field_t]: - dHv = h[v][region] - numpy.roll(h[v], 1, axis=direction)[region] - dHu = h[u][region] - numpy.roll(h[u], 1, axis=direction)[region] - psi_e[0] *= p0e - psi_e[0] += p1e * dHv * p2e - psi_e[1] *= p0e - psi_e[1] += p1e * dHu * p2e - e[u][region] += se * dt / epsilon[u][region] * (psi_e[0] + (p2e - 1) * dHv) - e[v][region] -= se * dt / epsilon[v][region] * (psi_e[1] + (p2e - 1) * dHu) - return e, h - - def pml_h(e: field_t, h: field_t) -> Tuple[field_t, field_t]: - dEv = (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region]) - dEu = (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region]) - psi_h[0] *= p0h - psi_h[0] += p1h * dEv * p2h - psi_h[1] *= p0h - psi_h[1] += p1h * dEu * p2h - h[u][region] -= se * dt * (psi_h[0] + (p2h - 1) * dEv) - h[v][region] += se * dt * (psi_h[1] + (p2h - 1) * dEu) - return e, h - - return pml_e, pml_h, fields - - -def poynting(e, h): - s = (numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], - numpy.roll(e[2], -1, axis=1) * h[0] - numpy.roll(e[0], -1, axis=1) * h[2], - numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]) - return numpy.array(s) - - -def poynting_divergence(s=None, *, e=None, h=None, dxes=None): # TODO dxes - if dxes is None: - dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) - - if s is None: - s = poynting(e, h) - - ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) / numpy.sqrt(dxes[0][0] * dxes[1][0])[:, None, None] + - (s[1] - numpy.roll(s[1], 1, axis=1)) / numpy.sqrt(dxes[0][1] * dxes[1][1])[None, :, None] + - (s[2] - numpy.roll(s[2], 1, axis=2)) / numpy.sqrt(dxes[0][2] * dxes[1][2])[None, None, :] ) - return ds - - -def energy_hstep(e0, h1, e2, epsilon=None, mu=None, dxes=None): - u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) - return u - - -def energy_estep(h0, e1, h2, epsilon=None, mu=None, dxes=None): - u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) - return u - - -def delta_energy_h2e(dt, e0, h1, e2, h3, epsilon=None, mu=None, dxes=None): - """ - This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) - """ - de = e2 * (e2 - e0) / dt - dh = h1 * (h3 - h1) / dt - du = dxmul(de, dh, epsilon, mu, dxes) - return du - - -def delta_energy_e2h(dt, h0, e1, h2, e3, epsilon=None, mu=None, dxes=None): - """ - This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) - """ - de = e1 * (e3 - e1) / dt - dh = h2 * (h2 - h0) / dt - du = dxmul(de, dh, epsilon, mu, dxes) - return du - - -def delta_energy_j(j0, e1, dxes=None): - if dxes is None: - dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) - - du = ((j0 * e1).sum(axis=0) * - dxes[0][0][:, None, None] * - dxes[0][1][None, :, None] * - dxes[0][2][None, None, :]) - return du - - -def dxmul(ee, hh, epsilon=None, mu=None, dxes=None): - if epsilon is None: - epsilon = 1 - if mu is None: - mu = 1 - if dxes is None: - dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) - - result = ((ee * epsilon).sum(axis=0) * - dxes[0][0][:, None, None] * - dxes[0][1][None, :, None] * - dxes[0][2][None, None, :] + - (hh * mu).sum(axis=0) * - dxes[1][0][:, None, None] * - dxes[1][1][None, :, None] * - dxes[1][2][None, None, :]) - return result - - - diff --git a/meanas/__init__.py b/meanas/__init__.py new file mode 100644 index 0000000..d4288a5 --- /dev/null +++ b/meanas/__init__.py @@ -0,0 +1,48 @@ +""" +Electromagnetic simulation tools + +This package is intended for building simulation inputs, analyzing +simulation outputs, and running short simulations on unspecialized hardware. +It is designed to provide tooling and a baseline for other, high-performance +purpose- and hardware-specific solvers. + + +**Contents** +- Finite difference frequency domain (FDFD) + * Library of sparse matrices for representing the electromagnetic wave + equation in 3D, as well as auxiliary matrices for conversion between fields + * Waveguide mode operators + * Waveguide mode eigensolver + * Stretched-coordinate PML boundaries (SCPML) + * Functional versions of most operators + * Anisotropic media (limited to diagonal elements eps_xx, eps_yy, eps_zz, mu_xx, ...) + * Arbitrary distributions of perfect electric and magnetic conductors (PEC / PMC) +- Finite difference time domain (FDTD) + * Basic Maxwell time-steps + * Poynting vector and energy calculation + * Convolutional PMLs + +This package does *not* provide a fast matrix solver, though by default +```meanas.fdfd.solvers.generic(...)``` will call +```scipy.sparse.linalg.qmr(...)``` to perform a solve. +For 2D FDFD problems this should be fine; likewise, the waveguide mode +solver uses scipy's eigenvalue solver, with reasonable results. + +For solving large (or 3D) FDFD problems, I recommend a GPU-based iterative +solver, such as [opencl_fdfd](https://mpxd.net/code/jan/opencl_fdfd) or +those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html)). Your +solver will need the ability to solve complex symmetric (non-Hermitian) +linear systems, ideally with double precision. + + +Dependencies: +- numpy +- scipy + +""" + +from .types import dx_lists_t, field_t, vfield_t, field_updater +from .vectorization import vec, unvec + +__author__ = 'Jan Petykiewicz' +version = '0.5' diff --git a/fdfd_tools/eigensolvers.py b/meanas/eigensolvers.py similarity index 100% rename from fdfd_tools/eigensolvers.py rename to meanas/eigensolvers.py diff --git a/fdfd_tools/bloch.py b/meanas/fdfd/bloch.py similarity index 100% rename from fdfd_tools/bloch.py rename to meanas/fdfd/bloch.py diff --git a/fdfd_tools/farfield.py b/meanas/fdfd/farfield.py similarity index 100% rename from fdfd_tools/farfield.py rename to meanas/fdfd/farfield.py diff --git a/fdfd_tools/functional.py b/meanas/fdfd/functional.py similarity index 87% rename from fdfd_tools/functional.py rename to meanas/fdfd/functional.py index 1d39d84..e57fe88 100644 --- a/fdfd_tools/functional.py +++ b/meanas/fdfd/functional.py @@ -2,8 +2,8 @@ Functional versions of many FDFD operators. These can be useful for performing FDFD calculations without needing to construct large matrices in memory. -The functions generated here expect inputs in the form E = [E_x, E_y, E_z], where each - component E_* is an ndarray of equal shape. +The functions generated here expect field inputs with shape (3, X, Y, Z), +e.g. E = [E_x, E_y, E_z] where each component has shape (X, Y, Z) """ from typing import List, Callable import numpy @@ -20,7 +20,7 @@ def curl_h(dxes: dx_lists_t) -> functional_matrix: """ Curl operator for use with the H field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Function for taking the discretized curl of the H-field, F(H) -> curlH """ dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') @@ -41,7 +41,7 @@ def curl_e(dxes: dx_lists_t) -> functional_matrix: """ Curl operator for use with the E field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Function for taking the discretized curl of the E-field, F(E) -> curlE """ dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') @@ -69,7 +69,7 @@ def e_full(omega: complex, (del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) :return: Function implementing the wave operator A(E) -> E @@ -100,7 +100,7 @@ def eh_full(omega: complex, Wave operator for full (both E and H) field representation. :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) :return: Function implementing the wave operator A(E, H) -> (E, H) @@ -131,7 +131,7 @@ def e2h(omega: complex, For use with e_full -- assumes that there is no magnetic current M. :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param mu: Magnetic permeability (default 1 everywhere) :return: Function for converting E to H """ @@ -159,7 +159,7 @@ def m2j(omega: complex, For use with e.g. e_full(). :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param mu: Magnetic permeability (default 1 everywhere) :return: Function for converting M to J """ diff --git a/fdfd_tools/operators.py b/meanas/fdfd/operators.py similarity index 92% rename from fdfd_tools/operators.py rename to meanas/fdfd/operators.py index 8809d09..3b2de68 100644 --- a/fdfd_tools/operators.py +++ b/meanas/fdfd/operators.py @@ -3,17 +3,13 @@ Sparse matrix operators for use with electromagnetic wave equations. These functions return sparse-matrix (scipy.sparse.spmatrix) representations of a variety of operators, intended for use with E and H fields vectorized using the - fdfd_tools.vec() and .unvec() functions (column-major/Fortran ordering). + meanas.vec() and .unvec() functions (column-major/Fortran ordering). E- and H-field values are defined on a Yee cell; epsilon values should be calculated for cells centered at each E component (mu at each H component). -Many of these functions require a 'dxes' parameter, of type fdfd_tools.dx_lists_type, - which contains grid cell width information in the following format: - [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]], - [[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]] - where dx_e_0 is the x-width of the x=0 cells, as used when calculating dE/dx, - and dy_h_0 is the y-width of the y=0 cells, as used when calculating dH/dy, etc. +Many of these functions require a 'dxes' parameter, of type meanas.dx_lists_type; see +the meanas.types submodule for details. The following operators are included: @@ -57,7 +53,7 @@ def e_full(omega: complex, To make this matrix symmetric, use the preconditions from e_full_preconditioners(). :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Vectorized dielectric constant :param mu: Vectorized magnetic permeability (default 1 everywhere). :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted @@ -101,7 +97,7 @@ def e_full_preconditioners(dxes: dx_lists_t The preconditioner matrices are diagonal and complex, with Pr = 1 / Pl - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Preconditioner matrices (Pl, Pr) """ p_squared = [dxes[0][0][:, None, None] * dxes[1][1][None, :, None] * dxes[1][2][None, None, :], @@ -127,7 +123,7 @@ def h_full(omega: complex, (del x (1/epsilon * del x) - omega**2 * mu) H = i * omega * M :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Vectorized dielectric constant :param mu: Vectorized magnetic permeability (default 1 everywhere) :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted @@ -177,7 +173,7 @@ def eh_full(omega: complex, for use with a field vector of the form hstack(vec(E), vec(H)). :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Vectorized dielectric constant :param mu: Vectorized magnetic permeability (default 1 everywhere) :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted @@ -216,7 +212,7 @@ def curl_h(dxes: dx_lists_t) -> sparse.spmatrix: """ Curl operator for use with the H field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Sparse matrix for taking the discretized curl of the H-field """ return cross(deriv_back(dxes[1])) @@ -226,7 +222,7 @@ def curl_e(dxes: dx_lists_t) -> sparse.spmatrix: """ Curl operator for use with the E field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Sparse matrix for taking the discretized curl of the E-field """ return cross(deriv_forward(dxes[0])) @@ -242,7 +238,7 @@ def e2h(omega: complex, For use with e_full -- assumes that there is no magnetic current M. :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param mu: Vectorized magnetic permeability (default 1 everywhere) :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted as containing a perfect magnetic conductor (PMC). @@ -270,7 +266,7 @@ def m2j(omega: complex, For use with eg. e_full. :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param mu: Vectorized magnetic permeability (default 1 everywhere) :return: Sparse matrix for converting E to H """ @@ -454,7 +450,7 @@ def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: Operator for computing the Poynting vector, containing the (E x) portion of the Poynting vector. :param e: Vectorized E-field for the ExH cross product - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Sparse matrix containing (E x) portion of Poynting cross product """ shape = [len(dx) for dx in dxes[0]] @@ -483,7 +479,7 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector. :param h: Vectorized H-field for the HxE cross product - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Sparse matrix containing (H x) portion of Poynting cross product """ shape = [len(dx) for dx in dxes[0]] diff --git a/fdfd_tools/grid.py b/meanas/fdfd/scpml.py similarity index 99% rename from fdfd_tools/grid.py rename to meanas/fdfd/scpml.py index 8ecb44f..c4091a0 100644 --- a/fdfd_tools/grid.py +++ b/meanas/fdfd/scpml.py @@ -8,7 +8,6 @@ import numpy __author__ = 'Jan Petykiewicz' -dx_lists_t = List[List[numpy.ndarray]] s_function_type = Callable[[float], float] diff --git a/fdfd_tools/solvers.py b/meanas/fdfd/solvers.py similarity index 97% rename from fdfd_tools/solvers.py rename to meanas/fdfd/solvers.py index 066725c..a0ce403 100644 --- a/fdfd_tools/solvers.py +++ b/meanas/fdfd/solvers.py @@ -70,7 +70,7 @@ def generic(omega: complex, """ Conjugate gradient FDFD solver using CSR sparse matrices. - All ndarray arguments should be 1D array, as returned by fdfd_tools.vec(). + All ndarray arguments should be 1D array, as returned by meanas.vec(). :param omega: Complex frequency to solve at. :param dxes: [[dx_e, dy_e, dz_e], [dx_h, dy_h, dz_h]] (complex cell sizes) diff --git a/fdfd_tools/waveguide.py b/meanas/fdfd/waveguide.py similarity index 92% rename from fdfd_tools/waveguide.py rename to meanas/fdfd/waveguide.py index 89e0d9c..48a1510 100644 --- a/fdfd_tools/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -51,7 +51,7 @@ def operator(omega: complex, z-dependence is assumed for the fields). :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Sparse matrix representation of the operator @@ -91,7 +91,7 @@ def normalized_fields(v: numpy.ndarray, :param v: Vector containing H_x and H_y fields :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Normalized, vectorized (e, h) containing all vector components. @@ -120,6 +120,8 @@ def normalized_fields(v: numpy.ndarray, # Try to break symmetry to assign a consistent sign [experimental] E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape) sign = numpy.sign(E_weighted[:, :max(shape[0]//2, 1), :max(shape[1]//2, 1)].real.sum()) + logger.debug('norm_angle = {}'.format(norm_angle)) + logger.debug('norm_sign = {}'.format(sign) norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) @@ -140,7 +142,7 @@ def v2h(v: numpy.ndarray, :param v: Vector containing H_x and H_y fields :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Vectorized H field with all vector components """ @@ -172,7 +174,7 @@ def v2e(v: numpy.ndarray, :param v: Vector containing H_x and H_y fields :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Vectorized E field with all vector components. @@ -192,7 +194,7 @@ def e2h(wavenumber: complex, :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Sparse matrix representation of the operator """ @@ -213,7 +215,7 @@ def h2e(wavenumber: complex, :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :return: Sparse matrix representation of the operator """ @@ -226,7 +228,7 @@ def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: Discretized curl operator for use with the waveguide E field. :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :return: Sparse matrix representation of the operator """ n = 1 @@ -243,7 +245,7 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: Discretized curl operator for use with the waveguide H field. :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :return: Sparse matrix representation of the operator """ n = 1 @@ -268,7 +270,7 @@ def h_err(h: vfield_t, :param h: Vectorized H field :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Relative error norm(OP @ h) / norm(h) @@ -299,7 +301,7 @@ def e_err(e: vfield_t, :param e: Vectorized E field :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Relative error norm(OP @ e) / norm(e) @@ -335,7 +337,7 @@ def cylindrical_operator(omega: complex, theta-dependence is assumed for the fields). :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param r0: Radius of curvature for the simulation. This should be the minimum value of r within the simulation domain. diff --git a/fdfd_tools/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py similarity index 97% rename from fdfd_tools/waveguide_mode.py rename to meanas/fdfd/waveguide_mode.py index 0b839d8..6e2b192 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -19,7 +19,7 @@ def solve_waveguide_mode_2d(mode_number: int, :param mode_number: Number of the mode, 0-indexed. :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) :param wavenumber_correction: Whether to correct the wavenumber to @@ -87,7 +87,7 @@ def solve_waveguide_mode(mode_number: int, :param mode_number: Number of the mode, 0-indexed :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param axis: Propagation axis (0=x, 1=y, 2=z) :param polarity: Propagation direction (+1 for +ve, -1 for -ve) :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use @@ -167,7 +167,7 @@ def compute_source(E: field_t, :param H: H-field of the mode (advanced by half of a Yee cell from E) :param wavenumber: Wavenumber of the mode :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param axis: Propagation axis (0=x, 1=y, 2=z) :param polarity: Propagation direction (+1 for +ve, -1 for -ve) :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use @@ -219,7 +219,7 @@ def compute_overlap_e(E: field_t, :param H: H-field of the mode (advanced by half of a Yee cell from E) :param wavenumber: Wavenumber of the mode :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param axis: Propagation axis (0=x, 1=y, 2=z) :param polarity: Propagation direction (+1 for +ve, -1 for -ve) :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use @@ -283,7 +283,7 @@ def solve_waveguide_mode_cylindrical(mode_number: int, :param mode_number: Number of the mode, 0-indexed :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header. + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types. The first coordinate is assumed to be r, the second is y. :param epsilon: Dielectric constant :param r0: Radius of curvature for the simulation. This should be the minimum value of diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py new file mode 100644 index 0000000..a1d278a --- /dev/null +++ b/meanas/fdtd/__init__.py @@ -0,0 +1,9 @@ +""" +Basic FDTD functionality +""" + +from .base import maxwell_e, maxwell_h +from .pml import cpml +from .energy import (poynting, poynting_divergence, energy_hstep, energy_estep, + delta_energy_h2e, delta_energy_h2e, delta_energy_j) +from .boundaries import conducting_boundary diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py new file mode 100644 index 0000000..8dd1df3 --- /dev/null +++ b/meanas/fdtd/base.py @@ -0,0 +1,87 @@ +""" +Basic FDTD field updates +""" +from typing import List, Callable, Tuple, Dict +import numpy + +from .. import dx_lists_t, field_t, field_updater + +__author__ = 'Jan Petykiewicz' + + +def curl_h(dxes: dx_lists_t = None) -> field_updater: + """ + Curl operator for use with the H field. + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Function for taking the discretized curl of the H-field, F(H) -> curlH + """ + if dxes: + dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') + + def dh(f, ax): + return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] + else: + def dh(f, ax): + return f - numpy.roll(f, 1, axis=ax) + + def ch_fun(h: field_t) -> field_t: + output = numpy.empty_like(h) + output[0] = dh(h[2], 1) + output[1] = dh(h[0], 2) + output[2] = dh(h[1], 0) + output[0] -= dh(h[1], 2) + output[1] -= dh(h[2], 0) + output[2] -= dh(h[0], 1) + return output + + return ch_fun + + +def curl_e(dxes: dx_lists_t = None) -> field_updater: + """ + Curl operator for use with the E field. + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Function for taking the discretized curl of the E-field, F(E) -> curlE + """ + if dxes is not None: + dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') + + def de(f, ax): + return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] + else: + def de(f, ax): + return numpy.roll(f, -1, axis=ax) - f + + def ce_fun(e: field_t) -> field_t: + output = numpy.empty_like(e) + output[0] = de(e[2], 1) + output[1] = de(e[0], 2) + output[2] = de(e[1], 0) + output[0] -= de(e[1], 2) + output[1] -= de(e[2], 0) + output[2] -= de(e[0], 1) + return output + + return ce_fun + + +def maxwell_e(dt: float, dxes: dx_lists_t = None) -> field_updater: + curl_h_fun = curl_h(dxes) + + def me_fun(e: field_t, h: field_t, epsilon: field_t): + e += dt * curl_h_fun(h) / epsilon + return e + + return me_fun + + +def maxwell_h(dt: float, dxes: dx_lists_t = None) -> field_updater: + curl_e_fun = curl_e(dxes) + + def mh_fun(e: field_t, h: field_t): + h -= dt * curl_e_fun(e) + return h + + return mh_fun diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py new file mode 100644 index 0000000..34a8d4a --- /dev/null +++ b/meanas/fdtd/boundaries.py @@ -0,0 +1,68 @@ +""" +Boundary conditions +""" + +from typing import List, Callable, Tuple, Dict +import numpy + +from .. import dx_lists_t, field_t, field_updater + + +def conducting_boundary(direction: int, + polarity: int + ) -> Tuple[field_updater, field_updater]: + dirs = [0, 1, 2] + if direction not in dirs: + raise Exception('Invalid direction: {}'.format(direction)) + dirs.remove(direction) + u, v = dirs + + if polarity < 0: + boundary_slice = [slice(None)] * 3 + shifted1_slice = [slice(None)] * 3 + boundary_slice[direction] = 0 + shifted1_slice[direction] = 1 + + def en(e: field_t): + e[direction][boundary_slice] = 0 + e[u][boundary_slice] = e[u][shifted1_slice] + e[v][boundary_slice] = e[v][shifted1_slice] + return e + + def hn(h: field_t): + h[direction][boundary_slice] = h[direction][shifted1_slice] + h[u][boundary_slice] = 0 + h[v][boundary_slice] = 0 + return h + + return en, hn + + elif polarity > 0: + boundary_slice = [slice(None)] * 3 + shifted1_slice = [slice(None)] * 3 + shifted2_slice = [slice(None)] * 3 + boundary_slice[direction] = -1 + shifted1_slice[direction] = -2 + shifted2_slice[direction] = -3 + + def ep(e: field_t): + e[direction][boundary_slice] = -e[direction][shifted2_slice] + e[direction][shifted1_slice] = 0 + e[u][boundary_slice] = e[u][shifted1_slice] + e[v][boundary_slice] = e[v][shifted1_slice] + return e + + def hp(h: field_t): + h[direction][boundary_slice] = h[direction][shifted1_slice] + h[u][boundary_slice] = -h[u][shifted2_slice] + h[u][shifted1_slice] = 0 + h[v][boundary_slice] = -h[v][shifted2_slice] + h[v][shifted1_slice] = 0 + return h + + return ep, hp + + else: + raise Exception('Bad polarity: {}'.format(polarity)) + + diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py new file mode 100644 index 0000000..26fb036 --- /dev/null +++ b/meanas/fdtd/energy.py @@ -0,0 +1,84 @@ +from typing import List, Callable, Tuple, Dict +import numpy + +from .. import dx_lists_t, field_t, field_updater + + +def poynting(e, h): + s = (numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], + numpy.roll(e[2], -1, axis=1) * h[0] - numpy.roll(e[0], -1, axis=1) * h[2], + numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]) + return numpy.array(s) + + +def poynting_divergence(s=None, *, e=None, h=None, dxes=None): # TODO dxes + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + + if s is None: + s = poynting(e, h) + + ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) / numpy.sqrt(dxes[0][0] * dxes[1][0])[:, None, None] + + (s[1] - numpy.roll(s[1], 1, axis=1)) / numpy.sqrt(dxes[0][1] * dxes[1][1])[None, :, None] + + (s[2] - numpy.roll(s[2], 1, axis=2)) / numpy.sqrt(dxes[0][2] * dxes[1][2])[None, None, :] ) + return ds + + +def energy_hstep(e0, h1, e2, epsilon=None, mu=None, dxes=None): + u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) + return u + + +def energy_estep(h0, e1, h2, epsilon=None, mu=None, dxes=None): + u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) + return u + + +def delta_energy_h2e(dt, e0, h1, e2, h3, epsilon=None, mu=None, dxes=None): + """ + This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) + """ + de = e2 * (e2 - e0) / dt + dh = h1 * (h3 - h1) / dt + du = dxmul(de, dh, epsilon, mu, dxes) + return du + + +def delta_energy_e2h(dt, h0, e1, h2, e3, epsilon=None, mu=None, dxes=None): + """ + This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) + """ + de = e1 * (e3 - e1) / dt + dh = h2 * (h2 - h0) / dt + du = dxmul(de, dh, epsilon, mu, dxes) + return du + + +def delta_energy_j(j0, e1, dxes=None): + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + + du = ((j0 * e1).sum(axis=0) * + dxes[0][0][:, None, None] * + dxes[0][1][None, :, None] * + dxes[0][2][None, None, :]) + return du + + +def dxmul(ee, hh, epsilon=None, mu=None, dxes=None): + if epsilon is None: + epsilon = 1 + if mu is None: + mu = 1 + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + + result = ((ee * epsilon).sum(axis=0) * + dxes[0][0][:, None, None] * + dxes[0][1][None, :, None] * + dxes[0][2][None, None, :] + + (hh * mu).sum(axis=0) * + dxes[1][0][:, None, None] * + dxes[1][1][None, :, None] * + dxes[1][2][None, None, :]) + return result diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py new file mode 100644 index 0000000..3e10aa6 --- /dev/null +++ b/meanas/fdtd/pml.py @@ -0,0 +1,122 @@ +""" +PML implementations + +""" +# TODO retest pmls! + +from typing import List, Callable, Tuple, Dict +import numpy + +from .. import dx_lists_t, field_t, field_updater + + +__author__ = 'Jan Petykiewicz' + + +def cpml(direction:int, + polarity: int, + dt: float, + epsilon: field_t, + thickness: int = 8, + ln_R_per_layer: float = -1.6, + epsilon_eff: float = 1, + mu_eff: float = 1, + m: float = 3.5, + ma: float = 1, + cfs_alpha: float = 0, + dtype: numpy.dtype = numpy.float32, + ) -> Tuple[Callable, Callable, Dict[str, field_t]]: + + if direction not in range(3): + raise Exception('Invalid direction: {}'.format(direction)) + + if polarity not in (-1, 1): + raise Exception('Invalid polarity: {}'.format(polarity)) + + if thickness <= 2: + raise Exception('It would be wise to have a pml with 4+ cells of thickness') + + if epsilon_eff <= 0: + raise Exception('epsilon_eff must be positive') + + sigma_max = -ln_R_per_layer / 2 * (m + 1) + kappa_max = numpy.sqrt(epsilon_eff * mu_eff) + alpha_max = cfs_alpha + transverse = numpy.delete(range(3), direction) + u, v = transverse + + xe = numpy.arange(1, thickness+1, dtype=float) + xh = numpy.arange(1, thickness+1, dtype=float) + if polarity > 0: + xe -= 0.5 + elif polarity < 0: + xh -= 0.5 + xe = xe[::-1] + xh = xh[::-1] + else: + raise Exception('Bad polarity!') + + expand_slice = [None] * 3 + expand_slice[direction] = slice(None) + + def par(x): + scaling = (x / thickness) ** m + sigma = scaling * sigma_max + kappa = 1 + scaling * (kappa_max - 1) + alpha = ((1 - x / thickness) ** ma) * alpha_max + p0 = numpy.exp(-(sigma / kappa + alpha) * dt) + p1 = sigma / (sigma + kappa * alpha) * (p0 - 1) + p2 = 1 / kappa + return p0[expand_slice], p1[expand_slice], p2[expand_slice] + + p0e, p1e, p2e = par(xe) + p0h, p1h, p2h = par(xh) + + region = [slice(None)] * 3 + if polarity < 0: + region[direction] = slice(None, thickness) + elif polarity > 0: + region[direction] = slice(-thickness, None) + else: + raise Exception('Bad polarity!') + + se = 1 if direction == 1 else -1 + + # TODO check if epsilon is uniform in pml region? + shape = list(epsilon[0].shape) + shape[direction] = thickness + psi_e = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] + psi_h = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] + + fields = { + 'psi_e_u': psi_e[0], + 'psi_e_v': psi_e[1], + 'psi_h_u': psi_h[0], + 'psi_h_v': psi_h[1], + } + + # Note that this is kinda slow -- would be faster to reuse dHv*p2h for the original + # H update, but then you have multiple arrays and a monolithic (field + pml) update operation + def pml_e(e: field_t, h: field_t, epsilon: field_t) -> Tuple[field_t, field_t]: + dHv = h[v][region] - numpy.roll(h[v], 1, axis=direction)[region] + dHu = h[u][region] - numpy.roll(h[u], 1, axis=direction)[region] + psi_e[0] *= p0e + psi_e[0] += p1e * dHv * p2e + psi_e[1] *= p0e + psi_e[1] += p1e * dHu * p2e + e[u][region] += se * dt / epsilon[u][region] * (psi_e[0] + (p2e - 1) * dHv) + e[v][region] -= se * dt / epsilon[v][region] * (psi_e[1] + (p2e - 1) * dHu) + return e, h + + def pml_h(e: field_t, h: field_t) -> Tuple[field_t, field_t]: + dEv = (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region]) + dEu = (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region]) + psi_h[0] *= p0h + psi_h[0] += p1h * dEv * p2h + psi_h[1] *= p0h + psi_h[1] += p1h * dEu * p2h + h[u][region] -= se * dt * (psi_h[0] + (p2h - 1) * dEv) + h[v][region] += se * dt * (psi_h[1] + (p2h - 1) * dEu) + return e, h + + return pml_e, pml_h, fields diff --git a/fdfd_tools/test/test_fdtd.py b/meanas/test/test_fdtd.py similarity index 99% rename from fdfd_tools/test/test_fdtd.py rename to meanas/test/test_fdtd.py index d74b8be..8443afc 100644 --- a/fdfd_tools/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -4,7 +4,7 @@ import dataclasses from typing import List, Tuple from numpy.testing import assert_allclose, assert_array_equal -from fdfd_tools import fdtd +from meanas import fdtd prng = numpy.random.RandomState(12345) diff --git a/meanas/types.py b/meanas/types.py new file mode 100644 index 0000000..7dc5c1c --- /dev/null +++ b/meanas/types.py @@ -0,0 +1,22 @@ +""" +Types shared across multiple submodules +""" +import numpy +from typing import List, Callable + + +# Field types +field_t = numpy.ndarray # vector field with shape (3, X, Y, Z) (e.g. [E_x, E_y, E_z]) +vfield_t = numpy.ndarray # linearized vector field (vector of length 3*X*Y*Z) + +''' + 'dxes' datastructure which contains grid cell width information in the following format: + [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]], + [[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]] + where dx_e_0 is the x-width of the x=0 cells, as used when calculating dE/dx, + and dy_h_0 is the y-width of the y=0 cells, as used when calculating dH/dy, etc. +''' +dx_lists_t = List[List[numpy.ndarray]] + + +field_updater = Callable[[field_t], field_t] diff --git a/fdfd_tools/vectorization.py b/meanas/vectorization.py similarity index 87% rename from fdfd_tools/vectorization.py rename to meanas/vectorization.py index 57b58fb..fd6cdcf 100644 --- a/fdfd_tools/vectorization.py +++ b/meanas/vectorization.py @@ -4,15 +4,13 @@ and a 1D array representation of that field [f_x0, f_x1, f_x2,... f_y0,... f_z0, Vectorized versions of the field use row-major (ie., C-style) ordering. """ - from typing import List import numpy -__author__ = 'Jan Petykiewicz' +from .types import field_t, vfield_t -# Types -field_t = List[numpy.ndarray] # vector field (eg. [E_x, E_y, E_z] -vfield_t = numpy.ndarray # linearized vector field + +__author__ = 'Jan Petykiewicz' def vec(f: field_t) -> vfield_t: @@ -27,7 +25,7 @@ def vec(f: field_t) -> vfield_t: """ if numpy.any(numpy.equal(f, None)): return None - return numpy.hstack(tuple((fi.ravel(order='C') for fi in f))) + return numpy.ravel(f, order='C') def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: diff --git a/setup.py b/setup.py index 2a64b37..8e817cb 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 from setuptools import setup, find_packages -import fdfd_tools +import meanas with open('README.md', 'r') as f: long_description = f.read() -setup(name='fdfd_tools', - version=fdfd_tools.version, - description='FDFD Electromagnetic simulation tools', +setup(name='meanas', + version=meanas.version, + description='Electromagnetic simulation tools', long_description=long_description, long_description_content_type='text/markdown', author='Jan Petykiewicz', From 94ff3f785330c6f38e9c1300de3f47a8cc876be1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 14:13:51 -0700 Subject: [PATCH 111/437] further fdfd_tools->meanas updates --- examples/bloch.py | 4 ++-- examples/fdfd.py | 27 +++++++++++++-------------- examples/fdtd.py | 2 +- examples/tcyl.py | 12 +++++------- meanas/fdtd/base.py | 4 ++-- 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/examples/bloch.py b/examples/bloch.py index fe1d6b1..e77c80f 100644 --- a/examples/bloch.py +++ b/examples/bloch.py @@ -1,5 +1,5 @@ -import numpy, scipy, gridlock, fdfd_tools -from fdfd_tools import bloch +import numpy, scipy, gridlock, meanas +from meanas.fdfd import bloch from numpy.linalg import norm import logging from pathlib import Path diff --git a/examples/fdfd.py b/examples/fdfd.py index a7e1746..5ee3477 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -2,11 +2,10 @@ import importlib import numpy from numpy.linalg import norm -from fdfd_tools import vec, unvec, waveguide_mode -import fdfd_tools -import fdfd_tools.functional -import fdfd_tools.grid -from fdfd_tools.solvers import generic as generic_solver +import meanas +from meanas import vec, unvec +from meanas.fdfd import waveguide_mode, functional, scpml +from meanas.fdfd.solvers import generic as generic_solver import gridlock @@ -57,8 +56,8 @@ def test0(solver=generic_solver): dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (0, 1, 2): for p in (-1, 1): - dxes = fdfd_tools.grid.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, - thickness=pml_thickness) + dxes = meanas.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, + thickness=pml_thickness) J = [numpy.zeros_like(grid.grids[0], dtype=complex) for _ in range(3)] J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1e5 @@ -68,7 +67,7 @@ def test0(solver=generic_solver): ''' x = solver(J=vec(J), **sim_args) - A = fdfd_tools.functional.e_full(omega, dxes, vec(grid.grids)).tocsr() + A = functional.e_full(omega, dxes, vec(grid.grids)).tocsr() b = -1j * omega * vec(J) print('Norm of the residual is ', norm(A @ x - b)) @@ -113,8 +112,8 @@ def test1(solver=generic_solver): dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (0, 1, 2): for p in (-1, 1): - dxes = fdfd_tools.grid.stretch_with_scpml(dxes,omega=omega, axis=a, polarity=p, - thickness=pml_thickness) + dxes = scpml.stretch_with_scpml(dxes,omega=omega, axis=a, polarity=p, + thickness=pml_thickness) half_dims = numpy.array([10, 20, 15]) * dx dims = [-half_dims, half_dims] @@ -155,7 +154,7 @@ def test1(solver=generic_solver): x = solver(J=vec(J), **sim_args) b = -1j * omega * vec(J) - A = fdfd_tools.operators.e_full(**sim_args).tocsr() + A = operators.e_full(**sim_args).tocsr() print('Norm of the residual is ', norm(A @ x - b)) E = unvec(x, grid.shape) @@ -181,9 +180,9 @@ def test1(solver=generic_solver): def poyntings(E): e = vec(E) - h = fdfd_tools.operators.e2h(omega, dxes) @ e - cross1 = fdfd_tools.operators.poynting_e_cross(e, dxes) @ h.conj() - cross2 = fdfd_tools.operators.poynting_h_cross(h.conj(), dxes) @ e + h = operators.e2h(omega, dxes) @ e + cross1 = operators.poynting_e_cross(e, dxes) @ h.conj() + cross2 = operators.poynting_h_cross(h.conj(), dxes) @ e s1 = unvec(0.5 * numpy.real(cross1), grid.shape) s2 = unvec(0.5 * numpy.real(-cross2), grid.shape) return s1, s2 diff --git a/examples/fdtd.py b/examples/fdtd.py index 1a25be4..be3942b 100644 --- a/examples/fdtd.py +++ b/examples/fdtd.py @@ -10,7 +10,7 @@ import time import numpy import h5py -from fdfd_tools import fdtd +from meanas import fdtd from masque import Pattern, shapes import gridlock import pcgen diff --git a/examples/tcyl.py b/examples/tcyl.py index 66caeb2..b0b57a2 100644 --- a/examples/tcyl.py +++ b/examples/tcyl.py @@ -2,11 +2,9 @@ import importlib import numpy from numpy.linalg import norm -from fdfd_tools import vec, unvec, waveguide_mode -import fdfd_tools -import fdfd_tools.functional -import fdfd_tools.grid -from fdfd_tools.solvers import generic as generic_solver +from meanas import vec, unvec +from meanas.fdfd import waveguide_mode, functional, scpml +from meanas.fdfd.solvers import generic as generic_solver import gridlock @@ -50,8 +48,8 @@ def test1(solver=generic_solver): dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (1, 2): for p in (-1, 1): - dxes = fdfd_tools.grid.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p, - thickness=pml_thickness) + dxes = scmpl.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p, + thickness=pml_thickness) wg_args = { 'omega': omega, diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py index 8dd1df3..389a7b5 100644 --- a/meanas/fdtd/base.py +++ b/meanas/fdtd/base.py @@ -13,7 +13,7 @@ def curl_h(dxes: dx_lists_t = None) -> field_updater: """ Curl operator for use with the H field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Function for taking the discretized curl of the H-field, F(H) -> curlH """ if dxes: @@ -42,7 +42,7 @@ def curl_e(dxes: dx_lists_t = None) -> field_updater: """ Curl operator for use with the E field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Function for taking the discretized curl of the E-field, F(E) -> curlE """ if dxes is not None: From 5951f2bdb12bb57bc5ddaaa0b28de5d1f96acf34 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 5 Aug 2019 00:20:06 -0700 Subject: [PATCH 112/437] various fixes and improvements --- examples/fdfd.py | 17 +++++++++----- meanas/fdfd/farfield.py | 10 ++++---- meanas/fdfd/functional.py | 2 +- meanas/fdfd/operators.py | 2 +- meanas/fdfd/scpml.py | 2 ++ meanas/fdfd/waveguide.py | 17 +++++++------- meanas/fdfd/waveguide_mode.py | 43 ++++++++--------------------------- meanas/test/test_fdtd.py | 1 + 8 files changed, 40 insertions(+), 54 deletions(-) diff --git a/examples/fdfd.py b/examples/fdfd.py index 5ee3477..c20fc3e 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -4,7 +4,7 @@ from numpy.linalg import norm import meanas from meanas import vec, unvec -from meanas.fdfd import waveguide_mode, functional, scpml +from meanas.fdfd import waveguide_mode, functional, scpml, operators from meanas.fdfd.solvers import generic as generic_solver import gridlock @@ -56,18 +56,23 @@ def test0(solver=generic_solver): dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (0, 1, 2): for p in (-1, 1): - dxes = meanas.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, - thickness=pml_thickness) + dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, + thickness=pml_thickness) J = [numpy.zeros_like(grid.grids[0], dtype=complex) for _ in range(3)] - J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1e5 + J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1 ''' Solve! ''' + sim_args = { + 'omega': omega, + 'dxes': dxes, + 'epsilon': vec(grid.grids), + } x = solver(J=vec(J), **sim_args) - A = functional.e_full(omega, dxes, vec(grid.grids)).tocsr() + A = operators.e_full(omega, dxes, vec(grid.grids)).tocsr() b = -1j * omega * vec(J) print('Norm of the residual is ', norm(A @ x - b)) @@ -208,7 +213,7 @@ def module_available(name): if __name__ == '__main__': - # test0() + test0() if module_available('opencl_fdfd'): from opencl_fdfd import cg_solver as opencl_solver diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index 84a04ba..faa25b5 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -6,9 +6,11 @@ import numpy from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift from numpy import pi +from .. import field_t -def near_to_farfield(E_near: List[numpy.ndarray], - H_near: List[numpy.ndarray], + +def near_to_farfield(E_near: field_t, + H_near: field_t, dx: float, dy: float, padded_size: List[int] = None @@ -115,8 +117,8 @@ def near_to_farfield(E_near: List[numpy.ndarray], -def far_to_nearfield(E_far: List[numpy.ndarray], - H_far: List[numpy.ndarray], +def far_to_nearfield(E_far: field_t, + H_far: field_t, dkx: float, dky: float, padded_size: List[int] = None diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index e57fe88..dd94421 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -8,7 +8,7 @@ e.g. E = [E_x, E_y, E_z] where each component has shape (X, Y, Z) from typing import List, Callable import numpy -from . import dx_lists_t, field_t +from .. import dx_lists_t, field_t __author__ = 'Jan Petykiewicz' diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 3b2de68..774c3d9 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -32,7 +32,7 @@ from typing import List, Tuple import numpy import scipy.sparse as sparse -from . import vec, dx_lists_t, vfield_t +from .. import vec, dx_lists_t, vfield_t __author__ = 'Jan Petykiewicz' diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py index c4091a0..897d43a 100644 --- a/meanas/fdfd/scpml.py +++ b/meanas/fdfd/scpml.py @@ -5,6 +5,8 @@ Functions for creating stretched coordinate PMLs. from typing import List, Callable import numpy +from .. import dx_lists_t + __author__ = 'Jan Petykiewicz' diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py index 48a1510..91b023c 100644 --- a/meanas/fdfd/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -23,7 +23,7 @@ import numpy from numpy.linalg import norm import scipy.sparse as sparse -from . import vec, unvec, dx_lists_t, field_t, vfield_t +from .. import vec, unvec, dx_lists_t, field_t, vfield_t from . import operators @@ -82,7 +82,8 @@ def normalized_fields(v: numpy.ndarray, omega: complex, dxes: dx_lists_t, epsilon: vfield_t, - mu: vfield_t = None + mu: vfield_t = None, + dx_prop: float = 0, ) -> Tuple[vfield_t, vfield_t]: """ Given a vector v containing the vectorized H_x and H_y fields, @@ -94,6 +95,7 @@ def normalized_fields(v: numpy.ndarray, :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :param dxes_prop: Grid cell width in the propagation direction. Default 0 (continuous). :return: Normalized, vectorized (e, h) containing all vector components. """ e = v2e(v, wavenumber, omega, dxes, epsilon, mu=mu) @@ -105,11 +107,10 @@ def normalized_fields(v: numpy.ndarray, E = unvec(e, shape) H = unvec(h, shape) - S1 = E[0] * numpy.roll(numpy.conj(H[1]), 1, axis=0) * dxes_real[0][1] * dxes_real[1][0] - S2 = E[1] * numpy.roll(numpy.conj(H[0]), 1, axis=1) * dxes_real[0][0] * dxes_real[1][1] - S = 0.25 * ((S1 + numpy.roll(S1, 1, axis=0)) - - (S2 + numpy.roll(S2, 1, axis=1))) - P = 0.5 * numpy.real(S.sum()) + phase = numpy.exp(-1j * wavenumber * dx_prop / 2) + S1 = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0] + S2 = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1] + P = numpy.real(S1.sum() - S2.sum()) assert P > 0, 'Found a mode propagating in the wrong direction! P={}'.format(P) energy = epsilon * e.conj() * e @@ -120,8 +121,6 @@ def normalized_fields(v: numpy.ndarray, # Try to break symmetry to assign a consistent sign [experimental] E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape) sign = numpy.sign(E_weighted[:, :max(shape[0]//2, 1), :max(shape[1]//2, 1)].real.sum()) - logger.debug('norm_angle = {}'.format(norm_angle)) - logger.debug('norm_sign = {}'.format(sign) norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 6e2b192..7182377 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -2,9 +2,9 @@ from typing import Dict, List import numpy import scipy.sparse as sparse -from . import vec, unvec, dx_lists_t, vfield_t, field_t +from .. import vec, unvec, dx_lists_t, vfield_t, field_t from . import operators, waveguide, functional -from .eigensolvers import signed_eigensolve, rayleigh_quotient_iteration +from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration def solve_waveguide_mode_2d(mode_number: int, @@ -12,7 +12,7 @@ def solve_waveguide_mode_2d(mode_number: int, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, - wavenumber_correction: bool = True, + dx_prop: float = 0, ) -> Dict[str, complex or field_t]: """ Given a 2d region, attempts to solve for the eigenmode with the specified mode number. @@ -22,8 +22,8 @@ def solve_waveguide_mode_2d(mode_number: int, :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) - :param wavenumber_correction: Whether to correct the wavenumber to - account for numerical dispersion (default True) + :param dx_prop: The cell width in the the propagation direction, used to apply a + correction to the wavenumber. Default 0 (i.e. continuous propagation direction) :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ @@ -51,15 +51,9 @@ def solve_waveguide_mode_2d(mode_number: int, ''' Perform correction on wavenumber to account for numerical dispersion. - - See Numerical Dispersion in Taflove's FDTD book. - This correction term reduces the error in emitted power, but additional - error is introduced into the E_err and H_err terms. This effect becomes - more pronounced as the wavenumber increases. ''' - if wavenumber_correction: - dx_mean = (numpy.hstack(dxes[0]) + numpy.hstack(dxes[1])).mean() / 2 #TODO figure out what dx to use here - wavenumber -= 2 * numpy.sin(numpy.real(wavenumber * dx_mean / 2)) / dx_mean - numpy.real(wavenumber) + if dx_prop != 0: + wavenumber = 2 / dx_prop * numpy.sin(wavenumber * dx_prop / 2) shape = [d.size for d in dxes[0]] fields = { @@ -79,7 +73,6 @@ def solve_waveguide_mode(mode_number: int, slices: List[slice], epsilon: field_t, mu: field_t = None, - wavenumber_correction: bool = True ) -> Dict[str, complex or numpy.ndarray]: """ Given a 3D grid, selects a slice from the grid and attempts to @@ -94,8 +87,6 @@ def solve_waveguide_mode(mode_number: int, as the waveguide cross-section. slices[axis] should select only one :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) - :param wavenumber_correction: Whether to correct the wavenumber to - account for numerical dispersion (default True) :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ if mu is None: @@ -115,7 +106,7 @@ def solve_waveguide_mode(mode_number: int, 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), 'mu': vec([mu[i][slices].transpose(order) for i in order]), - 'wavenumber_correction': wavenumber_correction, + 'dx_prop': dxes[0][order[2]][slices[order[2]]], } fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d) @@ -175,9 +166,6 @@ def compute_source(E: field_t, :param mu: Magnetic permeability (default 1 everywhere) :return: J distribution for the unidirectional source """ - if mu is None: - mu = numpy.ones(3) - J = numpy.zeros_like(E, dtype=complex) M = numpy.zeros_like(E, dtype=complex) @@ -275,9 +263,9 @@ def solve_waveguide_mode_cylindrical(mode_number: int, dxes: dx_lists_t, epsilon: vfield_t, r0: float, - wavenumber_correction: bool = True, ) -> Dict[str, complex or field_t]: """ + TODO: fixup Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode of the bent waveguide with the specified mode number. @@ -288,8 +276,6 @@ def solve_waveguide_mode_cylindrical(mode_number: int, :param epsilon: Dielectric constant :param r0: Radius of curvature for the simulation. This should be the minimum value of r within the simulation domain. - :param wavenumber_correction: Whether to correct the wavenumber to - account for numerical dispersion (default True) :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ @@ -313,16 +299,7 @@ def solve_waveguide_mode_cylindrical(mode_number: int, wavenumber = numpy.sqrt(eigval) wavenumber *= numpy.sign(numpy.real(wavenumber)) - ''' - Perform correction on wavenumber to account for numerical dispersion. - - See Numerical Dispersion in Taflove's FDTD book. - This correction term reduces the error in emitted power, but additional - error is introduced into the E_err and H_err terms. This effect becomes - more pronounced as the wavenumber increases. - ''' - if wavenumber_correction: - wavenumber -= 2 * numpy.sin(numpy.real(wavenumber / 2)) - numpy.real(wavenumber) + # TODO: Perform correction on wavenumber to account for numerical dispersion. shape = [d.size for d in dxes[0]] v = numpy.hstack((v, numpy.zeros(shape[0] * shape[1]))) diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 8443afc..08b678e 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -9,6 +9,7 @@ from meanas import fdtd prng = numpy.random.RandomState(12345) + def assert_fields_close(a, b, *args, **kwargs): numpy.testing.assert_allclose(a, b, verbose=False, err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(a, -1), numpy.rollaxis(b, -1)), *args, **kwargs) From 938c4c9a3503efc429eabeefbebff7a50630506c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 5 Aug 2019 01:09:52 -0700 Subject: [PATCH 113/437] move to 3xnnn arrays --- meanas/fdfd/functional.py | 43 +++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index dd94421..655d9b8 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -29,9 +29,13 @@ def curl_h(dxes: dx_lists_t) -> functional_matrix: return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] def ch_fun(h: field_t) -> field_t: - e = [dh(h[2], 1) - dh(h[1], 2), - dh(h[0], 2) - dh(h[2], 0), - dh(h[1], 0) - dh(h[0], 1)] + e = numpy.empty_like(h) + e[0] = dh(h[2], 1) + e[0] -= dh(h[1], 2) + e[1] = dh(h[0], 2) + e[1] -= dh(h[2], 0) + e[2] = dh(h[1], 0) + e[2] -= dh(h[0], 1) return e return ch_fun @@ -50,9 +54,13 @@ def curl_e(dxes: dx_lists_t) -> functional_matrix: return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] def ce_fun(e: field_t) -> field_t: - h = [de(e[2], 1) - de(e[1], 2), - de(e[0], 2) - de(e[2], 0), - de(e[1], 0) - de(e[0], 1)] + h = numpy.empty_like(e) + h[0] = de(e[2], 1) + h[0] -= de(e[1], 2) + h[1] = de(e[0], 2) + h[1] -= de(e[2], 0) + h[2] = de(e[1], 0) + h[2] -= de(e[0], 1) return h return ce_fun @@ -79,11 +87,11 @@ def e_full(omega: complex, def op_1(e): curls = ch(ce(e)) - return [c - omega ** 2 * e * x for c, e, x in zip(curls, epsilon, e)] + return curls - omega ** 2 * epsilon * e def op_mu(e): - curls = ch([m * y for m, y in zip(mu, ce(e))]) - return [c - omega ** 2 * p * x for c, p, x in zip(curls, epsilon, e)] + curls = ch(mu * ce(e)) + return curls - omega ** 2 * epsilon * e if numpy.any(numpy.equal(mu, None)): return op_1 @@ -109,12 +117,12 @@ def eh_full(omega: complex, ce = curl_e(dxes) def op_1(e, h): - return ([c - 1j * omega * p * x for c, p, x in zip(ch(h), epsilon, e)], - [c + 1j * omega * y for c, y in zip(ce(e), h)]) + return (ch(h) - 1j * omega * epsilon * e, + ce(e) + 1j * omega * h) def op_mu(e, h): - return ([c - 1j * omega * p * x for c, p, x in zip(ch(h), epsilon, e)], - [c + 1j * omega * m * y for c, m, y in zip(ce(e), mu, h)]) + return (ch(h) - 1j * omega * epsilon * e, + ce(e) + 1j * omega * mu * h) if numpy.any(numpy.equal(mu, None)): return op_1 @@ -138,10 +146,10 @@ def e2h(omega: complex, A2 = curl_e(dxes) def e2h_1_1(e): - return [y / (-1j * omega) for y in A2(e)] + return A2(e) / (-1j * omega) def e2h_mu(e): - return [y / (-1j * omega * m) for y, m in zip(A2(e), mu)] + return A2(e) / (-1j * omega * mu) if numpy.any(numpy.equal(mu, None)): return e2h_1_1 @@ -166,12 +174,11 @@ def m2j(omega: complex, ch = curl_h(dxes) def m2j_mu(m): - m_mu = [m[k] / mu[k] for k in range[3]] - J = [Ji / (-1j * omega) for Ji in ch(m_mu)] + J = ch(m / mu) / (-1j * omega) return J def m2j_1(m): - J = [Ji / (-1j * omega) for Ji in ch(m)] + J = ch(m) / (-1j * omega) return J if numpy.any(numpy.equal(mu, None)): From 342912099344b19d7fb78f7048cb686b935c3024 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 7 Aug 2019 01:00:21 -0700 Subject: [PATCH 114/437] d_prop -> dx_prop --- meanas/fdfd/waveguide_mode.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 7182377..7fab6e6 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -101,28 +101,28 @@ def solve_waveguide_mode(mode_number: int, order = numpy.roll(range(3), 2 - axis) reverse_order = numpy.roll(range(3), axis - 2) + # Find dx in propagation direction + dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes]) + dx_prop = 0.5 * sum(dxab_forward) + # Reduce to 2D and solve the 2D problem args_2d = { 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), 'mu': vec([mu[i][slices].transpose(order) for i in order]), - 'dx_prop': dxes[0][order[2]][slices[order[2]]], + 'dx_prop': dx_prop, } fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d) ''' Apply corrections and expand to 3D ''' - # Scale based on dx in propagation direction - dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes]) - # Adjust for propagation direction fields_2d['E'][2] *= polarity fields_2d['H'][2] *= polarity # Apply phase shift to H-field - d_prop = 0.5 * sum(dxab_forward) - fields_2d['H'] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * d_prop) + fields_2d['H'] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * dx_prop) # Expand E, H to full epsilon space we were given E = numpy.zeros_like(epsilon, dtype=complex) @@ -136,7 +136,6 @@ def solve_waveguide_mode(mode_number: int, 'H': H, 'E': E, } - return results From 2c91ea249f87dc9572480f26dc37f563965e85b7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 7 Aug 2019 01:00:57 -0700 Subject: [PATCH 115/437] Fix wgmode expansion --- meanas/fdfd/waveguide_mode.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 7fab6e6..b6a66de 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -436,16 +436,15 @@ def expand_wgmode_e(E: field_t, phase_E = numpy.exp(iphi * r_E).reshape(a_shape) # Expand our slice to the entire grid using the phase factors - Ee = numpy.zeros_like(E) + E_expanded = numpy.zeros_like(E) slices_exp = list(slices) slices_exp[axis] = slice(E.shape[axis + 1]) slices_exp = (slice(None), *slices_exp) - slices_in = tuple(slice(None), *slices) + slices_in = (slice(None), *slices) - Ee[slices_exp] = phase_E * numpy.array(E)[slices_in] - - return Ee + E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in] + return E_expanded From 1a04bab361b4db0933a6215310990aa1c9daf011 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 7 Aug 2019 01:01:35 -0700 Subject: [PATCH 116/437] Fixup slices --- meanas/fdfd/waveguide_mode.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index b6a66de..1603547 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -346,16 +346,26 @@ def compute_source_e(QE: field_t, mu: field_t = None, ) -> field_t: """ - Want (AQ-QA) E = -iwJ, where Q is a mask - If E is an eigenmode, AE = 0 so just AQE = -iwJ - Really only need E in 4 cells along axis (0, 0, Emode1, Emode2), find AE (1 fdtd step), then use center 2 cells as src + Want AQE = -iwJ, where Q is mask and normally AE = -iwJ + ## Want (AQ-QA) E = -iwJ, where Q is a mask + ## If E is an eigenmode, AE = 0 so just AQE = -iwJ + Really only need E in 4 cells along axis (0, 0, Emode1, Emode2), find AE (1 iteration), then use center 2 cells as src + Maybe better to use (0, Emode1, Emode2, Emode3), find AE (1 iteration), then use left 2 cells as src? """ slices = tuple(slices) # Trim a cell from each end of the propagation axis slices_reduced = list(slices) - slices_reduced[axis] = slice(slices[axis].start + 1, slices[axis].stop - 1) - slices_reduced = tuple(slice(None), *slices_reduced) + for aa in range(3): + if aa == axis: + if polarity > 0: + slices_reduced[axis] = slice(slices[axis].start, slices[axis].start+2) + else: + slices_reduced[axis] = slice(slices[axis].stop-2, slices[axis].stop) + else: + start = slices[aa].start + stop = slices[aa].stop + slices_reduced = (slice(None), *slices_reduced) # Don't actually need to mask out E here since it needs to be pre-masked (QE) @@ -410,7 +420,7 @@ def compute_overlap_ce(E: field_t, slices2 = list(slices) slices2[axis] = slice(start, stop) - slices2 = tuple(slice(None), slices2) + slices2 = (slice(None), *slices2) Etgt = numpy.zeros_like(Ee) Etgt[slices2] = Ee[slices2] From 07c94617fefd5b5da72331958d1e48350601ba0d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 7 Aug 2019 01:01:55 -0700 Subject: [PATCH 117/437] Operator-based soruce --- meanas/fdfd/waveguide_mode.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 1603547..1cce856 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -170,18 +170,25 @@ def compute_source(E: field_t, src_order = numpy.roll(range(3), -axis) exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) - J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity - J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity rollby = -1 if polarity > 0 else 0 - M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) - M[src_order[2]] = -numpy.roll(E[src_order[1]], rollby, axis=axis) +# J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity / dxes[1][axis][slices[axis]] +# J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity / dxes[1][axis][slices[axis]] +# M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) / dxes[0][axis][slices[axis]] +# M[src_order[2]] = -numpy.roll(E[src_order[1]], rollby, axis=axis) / dxes[0][axis][slices[axis]] + + s2 = [slice(None), slice(None), slice(None)] + s2[axis] = slice(slices[axis].start, slices[axis].stop) + s2 = (src_order, *s2) + + J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H), rollby, axis=axis+1)[s2] * polarity * exp_iphi + M[s2] = numpy.roll(functional.curl_e(dxes=dxes)(E), -rollby, axis=axis+1)[s2] m2j = functional.m2j(omega, dxes, mu) Jm = m2j(M) Jtot = J + Jm - return Jtot + return Jtot.conj() def compute_overlap_e(E: field_t, @@ -332,7 +339,6 @@ def compute_source_q(E: field_t, Jm = m2j(M) Jtot = J + Jm - return Jtot, J, M From aade81c21e9df18643d18404fffcf10980d1dfc4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 7 Aug 2019 02:27:04 -0700 Subject: [PATCH 118/437] alternate src formulation --- meanas/fdfd/waveguide_mode.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 1cce856..59a3f37 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -169,9 +169,9 @@ def compute_source(E: field_t, M = numpy.zeros_like(E, dtype=complex) src_order = numpy.roll(range(3), -axis) - exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) - rollby = -1 if polarity > 0 else 0 +# exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) +# rollby = -1 if polarity > 0 else 0 # J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity / dxes[1][axis][slices[axis]] # J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity / dxes[1][axis][slices[axis]] # M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) / dxes[0][axis][slices[axis]] @@ -181,14 +181,16 @@ def compute_source(E: field_t, s2[axis] = slice(slices[axis].start, slices[axis].stop) s2 = (src_order, *s2) - J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H), rollby, axis=axis+1)[s2] * polarity * exp_iphi - M[s2] = numpy.roll(functional.curl_e(dxes=dxes)(E), -rollby, axis=axis+1)[s2] + rollby = 1 if polarity > 0 else 0 + exp_iphi = numpy.exp(-1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) + J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H.conj()), -rollby, axis=axis+1)[s2] * polarity * exp_iphi + M[s2] = -numpy.roll(functional.curl_e(dxes=dxes)(E.conj()), rollby, axis=axis+1)[s2] m2j = functional.m2j(omega, dxes, mu) Jm = m2j(M) Jtot = J + Jm - return Jtot.conj() + return Jtot def compute_overlap_e(E: field_t, @@ -371,6 +373,17 @@ def compute_source_e(QE: field_t, else: start = slices[aa].start stop = slices[aa].stop +# if start is not None or stop is not None: +# if start is None: +# start = 1 +# stop -= 1 +# elif stop is None: +# stop = E.shape[aa + 1] - 1 +# start += 1 +# else: +# start += 1 +# stop -= 1 +# slices_reduced[aa] = slice(start, stop) slices_reduced = (slice(None), *slices_reduced) # Don't actually need to mask out E here since it needs to be pre-masked (QE) From ccdb423ba2091ff911fcecd48494cb0e123122cf Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:15:34 -0700 Subject: [PATCH 119/437] add e_tfsf_source --- meanas/fdfd/functional.py | 17 +++++++++++++++++ meanas/fdfd/operators.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index 655d9b8..fe11e51 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -187,3 +187,20 @@ def m2j(omega: complex, return m2j_mu +def e_tfsf_source(TF_region: field_t, + omega: complex, + dxes: dx_lists_t, + epsilon: field_t, + mu: field_t = None, + ) -> functional_matrix: + """ + Operator that turuns an E-field distribution into a total-field/scattered-field + (TFSF) source. + """ + # TODO documentation + A = e_full(omega, dxes, epsilon, mu) + + def op(e): + neg_iwj = A(TF_region * e) - TF_region * A(e) + return neg_iwj / (-1j * omega) + diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 774c3d9..3042af4 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -501,3 +501,21 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: [ bx @ Hz @ fy @ dagx, zero, -bz @ Hx @ fy @ dagz], [-bx @ Hy @ fz @ dagx, by @ Hx @ fz @ dagy, zero]]) return P + + +def e_tfsf_source(TF_region: vfield_t, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + ) -> sparse.spmatrix: + """ + Operator that turns an E-field distribution into a total-field/scattered-field + (TFSF) source. + """ + # TODO documentation + A = e_full(omega, dxes, epsilon, mu) + Q = sparse.diags(TF_region) + return (A @ Q - Q @ A) / (-1j * omega) + + From b466ed02eae2d4420ef7318683c754ed1b358341 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:16:27 -0700 Subject: [PATCH 120/437] Add e_boundary_source --- meanas/fdfd/operators.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 3042af4..e156d2e 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -519,3 +519,32 @@ def e_tfsf_source(TF_region: vfield_t, return (A @ Q - Q @ A) / (-1j * omega) +def e_boundary_source(mask: vfield_t, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + periodic_mask_edges: bool = False, + ) -> sparse.spmatrix: + """ + Operator that turns an E-field distrubtion into a current (J) distribution + along the edges (external and internal) of the provided mask. This is just an + e_tfsf_source with an additional masking step. + """ + full = e_tfsf_source(TF_region=mask, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) + + shape = [len(dxe) for dxe in dxes[0]] + jmask = numpy.zeros_like(mask, dtype=bool) + + if periodic_mask_edges: + shift = lambda axis, polarity: rotation(axis=axis, shape=shape, shift_distance=polarity) + else: + shift = lambda axis, polarity: shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity) + + for axis in (0, 1, 2): + for polarity in (-1, +1): + r = shift(axis, polarity) - sparse.eye(numpy.prod(shape)) # shifted minus original + r3 = sparse.block_diag((r, r, r)) + jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask)) + + return sparse.diags(jmask.astype(int)) @ full, jmask From 0503e9d6ef88f43c9840897037a145f7a2b8f3f1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:16:45 -0700 Subject: [PATCH 121/437] Fix shift_with_mirror() for C-ordered arrays --- meanas/fdfd/operators.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index e156d2e..13b2691 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -341,9 +341,7 @@ def shift_with_mirror(axis: int, shape: List[int], shift_distance: int=1) -> spa n = numpy.prod(shape) i_ind = numpy.arange(n) - j_ind = ijk[0] + ijk[1] * shape[0] - if len(shape) == 3: - j_ind += ijk[2] * shape[0] * shape[1] + j_ind = numpy.ravel_multi_index(ijk, shape, order='C') vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) From 054ac994d501c214af6f7d8e90c86b8ab24a5a6d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:17:52 -0700 Subject: [PATCH 122/437] Don't perform dx_prop wavenumber correction in waveguide_mode_2d It's technically a correction for discretization in the third direction --- meanas/fdfd/waveguide_mode.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 59a3f37..3dbdfd8 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -12,7 +12,6 @@ def solve_waveguide_mode_2d(mode_number: int, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, - dx_prop: float = 0, ) -> Dict[str, complex or field_t]: """ Given a 2d region, attempts to solve for the eigenmode with the specified mode number. @@ -22,8 +21,6 @@ def solve_waveguide_mode_2d(mode_number: int, :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) - :param dx_prop: The cell width in the the propagation direction, used to apply a - correction to the wavenumber. Default 0 (i.e. continuous propagation direction) :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ @@ -49,12 +46,6 @@ def solve_waveguide_mode_2d(mode_number: int, e, h = waveguide.normalized_fields(v, wavenumber, omega, dxes, epsilon, mu) - ''' - Perform correction on wavenumber to account for numerical dispersion. - ''' - if dx_prop != 0: - wavenumber = 2 / dx_prop * numpy.sin(wavenumber * dx_prop / 2) - shape = [d.size for d in dxes[0]] fields = { 'wavenumber': wavenumber, @@ -110,7 +101,6 @@ def solve_waveguide_mode(mode_number: int, 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), 'mu': vec([mu[i][slices].transpose(order) for i in order]), - 'dx_prop': dx_prop, } fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d) From 278790864088c3a55d9306ed5688c0f7ab5040ee Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:21:39 -0700 Subject: [PATCH 123/437] Add E variants of waveguide equations rename 2d vector from v to e_xy or h_xy --- meanas/fdfd/waveguide.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py index 91b023c..611cb57 100644 --- a/meanas/fdfd/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -17,6 +17,7 @@ As the z-dependence is known, all the functions in this file assume a 2D grid (ie. dxes = [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dx_h_0, ...], [dy_h_0, ...]]]) with propagation along the z axis. """ +# TODO update module docs from typing import List, Tuple import numpy From 41bec05d4efebb82f2ab268fa27066cbb9eb8833 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:24:17 -0700 Subject: [PATCH 124/437] Remove unwanted return --- meanas/fdfd/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 13b2691..c2df8ab 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -545,4 +545,4 @@ def e_boundary_source(mask: vfield_t, r3 = sparse.block_diag((r, r, r)) jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask)) - return sparse.diags(jmask.astype(int)) @ full, jmask + return sparse.diags(jmask.astype(int)) @ full From af8efd00eb410542dbdbd2bf3cc58a7828864cb4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:25:36 -0700 Subject: [PATCH 125/437] Add E-field versions of waveguide mode operators, rename v->e_xy or h_xy, and add ability to specify mode margin in solve_waveguide_mode_2d --- meanas/fdfd/waveguide.py | 205 ++++++++++++++++++++++++++-------- meanas/fdfd/waveguide_mode.py | 16 ++- 2 files changed, 170 insertions(+), 51 deletions(-) diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py index 611cb57..63ecb98 100644 --- a/meanas/fdfd/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -31,11 +31,36 @@ from . import operators __author__ = 'Jan Petykiewicz' -def operator(omega: complex, +def operator_e(omega: complex, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, ) -> sparse.spmatrix: + if numpy.any(numpy.equal(mu, None)): + mu = numpy.ones_like(epsilon) + + Dfx, Dfy = operators.deriv_forward(dxes[0]) + Dbx, Dby = operators.deriv_back(dxes[1]) + + eps_parts = numpy.split(epsilon, 3) + eps_xy = sparse.diags(numpy.hstack((eps_parts[0], eps_parts[1]))) + eps_z_inv = sparse.diags(1 / eps_parts[2]) + + mu_parts = numpy.split(mu, 3) + mu_yx = sparse.diags(numpy.hstack((mu_parts[1], mu_parts[0]))) + mu_z_inv = sparse.diags(1 / mu_parts[2]) + + op = omega * omega * mu_yx @ eps_xy + \ + mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx)) + \ + sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy + return op + + +def operator_h(omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + ) -> sparse.spmatrix: """ Waveguide operator of the form @@ -71,27 +96,27 @@ def operator(omega: complex, mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) mu_z_inv = sparse.diags(1 / mu_parts[2]) - op = omega ** 2 * eps_yx @ mu_xy + \ + op = omega * omega * eps_yx @ mu_xy + \ eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx)) + \ sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy return op -def normalized_fields(v: numpy.ndarray, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - dx_prop: float = 0, - ) -> Tuple[vfield_t, vfield_t]: +def normalized_fields_e(e_xy: numpy.ndarray, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + dx_prop: float = 0, + ) -> Tuple[vfield_t, vfield_t]: """ - Given a vector v containing the vectorized H_x and H_y fields, + Given a vector e_xy containing the vectorized E_x and E_y fields, returns normalized, vectorized E and H fields for the system. - :param v: Vector containing H_x and H_y fields - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param e_xy: Vector containing E_x and E_y fields + :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` :param omega: The angular frequency of the system :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid @@ -99,9 +124,51 @@ def normalized_fields(v: numpy.ndarray, :param dxes_prop: Grid cell width in the propagation direction. Default 0 (continuous). :return: Normalized, vectorized (e, h) containing all vector components. """ - e = v2e(v, wavenumber, omega, dxes, epsilon, mu=mu) - h = v2h(v, wavenumber, dxes, mu=mu) + e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy + h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy + e_norm, h_norm = _normalized_fields(e=e, h=h, wavenumber=wavenumber, omega=omega, + dxes=dxes, epsilon=epsilon, mu=mu, dx_prop=dx_prop) + return e_norm, h_norm + +def normalized_fields_h(h_xy: numpy.ndarray, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + dx_prop: float = 0, + ) -> Tuple[vfield_t, vfield_t]: + """ + Given a vector e_xy containing the vectorized E_x and E_y fields, + returns normalized, vectorized E and H fields for the system. + + :param e_xy: Vector containing E_x and E_y fields + :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :param dxes_prop: Grid cell width in the propagation direction. Default 0 (continuous). + :return: Normalized, vectorized (e, h) containing all vector components. + """ + e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy + h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy + e_norm, h_norm = _normalized_fields(e=e, h=h, wavenumber=wavenumber, omega=omega, + dxes=dxes, epsilon=epsilon, mu=mu, dx_prop=dx_prop) + return e_norm, h_norm + + +def _normalized_fields(e: numpy.ndarray, + h: numpy.ndarray, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + dx_prop: float = 0, + ) -> Tuple[vfield_t, vfield_t]: + # TODO documentation shape = [s.size for s in dxes[0]] dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)] @@ -131,56 +198,104 @@ def normalized_fields(v: numpy.ndarray, return e, h -def v2h(v: numpy.ndarray, - wavenumber: complex, - dxes: dx_lists_t, - mu: vfield_t = None - ) -> vfield_t: +def exy2h(wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> sparse.spmatrix: """ - Given a vector v containing the vectorized H_x and H_y fields, - returns a vectorized H including all three H components. + Operator which transforms the vector e_xy containing the vectorized E_x and E_y fields, + into a vectorized H containing all three H components - :param v: Vector containing H_x and H_y fields - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Sparse matrix representing the operator + """ + e2hop = e2h(wavenumber=wavenumber, omega=omega, dxes=dxes, mu=mu) + return e2hop @ exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) + + +def hxy2e(wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Operator which transforms the vector h_xy containing the vectorized H_x and H_y fields, + into a vectorized E containing all three E components + + :param wavenumber: Wavenumber satisfying `operator_h(...) @ h_xy == wavenumber**2 * h_xy` + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Sparse matrix representing the operator + """ + h2eop = h2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon) + return h2eop @ hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) + + +def hxy2h(wavenumber: complex, + dxes: dx_lists_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Operator which transforms the vector h_xy containing the vectorized H_x and H_y fields, + into a vectorized H containing all three H components + + :param wavenumber: Wavenumber satisfying `operator_h(...) @ h_xy == wavenumber**2 * h_xy` :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Vectorized H field with all vector components + :return: Sparse matrix representing the operator """ Dfx, Dfy = operators.deriv_forward(dxes[0]) - op = sparse.hstack((Dfx, Dfy)) + hxy2hz = sparse.hstack((Dfx, Dfy)) / (1j * wavenumber) if not numpy.any(numpy.equal(mu, None)): mu_parts = numpy.split(mu, 3) mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) mu_z_inv = sparse.diags(1 / mu_parts[2]) - op = mu_z_inv @ op @ mu_xy + hxy2hz = mu_z_inv @ hxy2hz @ mu_xy - w = op @ v / (1j * wavenumber) - return numpy.hstack((v, w)).flatten() + n_pts = dxes[1][0].size * dxes[1][1].size + op = sparse.vstack((sparse.eye(2 * n_pts), + hxy2hz)) + return op -def v2e(v: numpy.ndarray, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None - ) -> vfield_t: +def exy2e(wavenumber: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + ) -> sparse.spmatrix: """ - Given a vector v containing the vectorized H_x and H_y fields, - returns a vectorized E containing all three E components + Operator which transforms the vector e_xy containing the vectorized E_x and E_y fields, + into a vectorized E containing all three E components - :param v: Vector containing H_x and H_y fields - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param omega: The angular frequency of the system + :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Vectorized E field with all vector components. + :return: Sparse matrix representing the operator """ - h2eop = h2e(wavenumber, omega, dxes, epsilon) - return h2eop @ v2h(v, wavenumber, dxes, mu) + Dbx, Dby = operators.deriv_back(dxes[1]) + exy2ez = sparse.hstack((Dbx, Dby)) / (1j * wavenumber) + + if not numpy.any(numpy.equal(epsilon, None)): + epsilon_parts = numpy.split(epsilon, 3) + epsilon_xy = sparse.diags(numpy.hstack((epsilon_parts[0], epsilon_parts[1]))) + epsilon_z_inv = sparse.diags(1 / epsilon_parts[2]) + + exy2ez = epsilon_z_inv @ exy2ez @ epsilon_xy + + n_pts = dxes[0][0].size * dxes[0][1].size + op = sparse.vstack((sparse.eye(2 * n_pts), + exy2ez)) + return op def e2h(wavenumber: complex, diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 3dbdfd8..300d3e6 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -12,6 +12,7 @@ def solve_waveguide_mode_2d(mode_number: int, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, + mode_margin: int = 2, ) -> Dict[str, complex or field_t]: """ Given a 2d region, attempts to solve for the eigenmode with the specified mode number. @@ -21,6 +22,9 @@ def solve_waveguide_mode_2d(mode_number: int, :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) + :param mode_margin: The eigensolver will actually solve for (mode_number + mode_margin) + modes, but only return the target mode. Increasing this value can improve the solver's + ability to find the correct mode. Default 2. :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ @@ -28,23 +32,23 @@ def solve_waveguide_mode_2d(mode_number: int, Solve for the largest-magnitude eigenvalue of the real operator ''' dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] - A_r = waveguide.operator(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) + A_r = waveguide.operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) - eigvals, eigvecs = signed_eigensolve(A_r, mode_number+3) - v = eigvecs[:, -(mode_number + 1)] + eigvals, eigvecs = signed_eigensolve(A_r, mode_number + mode_margin) + exy = eigvecs[:, -(mode_number + 1)] ''' Now solve for the eigenvector of the full operator, using the real operator's eigenvector as an initial guess for Rayleigh quotient iteration. ''' - A = waveguide.operator(omega, dxes, epsilon, mu) - eigval, v = rayleigh_quotient_iteration(A, v) + A = waveguide.operator_e(omega, dxes, epsilon, mu) + eigval, exy = rayleigh_quotient_iteration(A, exy) # Calculate the wave-vector (force the real part to be positive) wavenumber = numpy.sqrt(eigval) wavenumber *= numpy.sign(numpy.real(wavenumber)) - e, h = waveguide.normalized_fields(v, wavenumber, omega, dxes, epsilon, mu) + e, h = waveguide.normalized_fields_e(exy, wavenumber, omega, dxes, epsilon, mu) shape = [d.size for d in dxes[0]] fields = { From c306bb1f46c40e7f4aad32d4cd6591cd8345b0d5 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:26:54 -0700 Subject: [PATCH 126/437] Correct for numerical dispersion at 3d solve_waveguide_mode level --- meanas/fdfd/waveguide_mode.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 300d3e6..2f32c89 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -111,6 +111,9 @@ def solve_waveguide_mode(mode_number: int, ''' Apply corrections and expand to 3D ''' + # Correct wavenumber to account for numerical dispersion. + fields_2d['wavenumber'] = 2/dx_prop * numpy.arcsin(fields_2d['wavenumber'] * dx_prop/2) + # Adjust for propagation direction fields_2d['E'][2] *= polarity fields_2d['H'][2] *= polarity From 7006b5e6e4c73c81284bf73009968ac53cc1df0b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:27:05 -0700 Subject: [PATCH 127/437] Flip propagation direction by flipping H --- meanas/fdfd/waveguide_mode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 2f32c89..7776ce2 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -115,8 +115,7 @@ def solve_waveguide_mode(mode_number: int, fields_2d['wavenumber'] = 2/dx_prop * numpy.arcsin(fields_2d['wavenumber'] * dx_prop/2) # Adjust for propagation direction - fields_2d['E'][2] *= polarity - fields_2d['H'][2] *= polarity + fields_2d['H'] *= polarity # Apply phase shift to H-field fields_2d['H'] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * dx_prop) From 1860d754cda26da183bebb814c5fe23748787111 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:27:32 -0700 Subject: [PATCH 128/437] Fix shifts applied to E and H fields Only some components need shifting --- meanas/fdfd/waveguide_mode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 7776ce2..b9524e5 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -118,7 +118,8 @@ def solve_waveguide_mode(mode_number: int, fields_2d['H'] *= polarity # Apply phase shift to H-field - fields_2d['H'] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * dx_prop) + fields_2d['H'][:2] *= numpy.exp(-1j * polarity * 0.5 * fields_2d['wavenumber'] * dx_prop) + fields_2d['E'][2] *= numpy.exp(-1j * polarity * 0.5 * fields_2d['wavenumber'] * dx_prop) # Expand E, H to full epsilon space we were given E = numpy.zeros_like(epsilon, dtype=complex) From d6a34b280eb22e5ff06ff1785fa356ab24ef93ff Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:28:06 -0700 Subject: [PATCH 129/437] Simplify compute_source and fix propagation direction --- meanas/fdfd/waveguide_mode.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index b9524e5..cd59b5f 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -178,10 +178,10 @@ def compute_source(E: field_t, s2[axis] = slice(slices[axis].start, slices[axis].stop) s2 = (src_order, *s2) - rollby = 1 if polarity > 0 else 0 - exp_iphi = numpy.exp(-1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) - J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H.conj()), -rollby, axis=axis+1)[s2] * polarity * exp_iphi - M[s2] = -numpy.roll(functional.curl_e(dxes=dxes)(E.conj()), rollby, axis=axis+1)[s2] + rollby = 1 if polarity < 0 else 0 + exp_iphi = numpy.exp(-1j * -rollby * wavenumber * dxes[1][axis][slices[axis]]) + J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H), -rollby, axis=axis+1)[s2] * exp_iphi * -polarity + M[s2] = numpy.roll(functional.curl_e(dxes=dxes)(E), rollby, axis=axis+1)[s2] m2j = functional.m2j(omega, dxes, mu) Jm = m2j(M) From 3887a8804f7767214fe2244f3ade32f60c9b9e75 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:28:19 -0700 Subject: [PATCH 130/437] fix phase in expand_wgmode --- meanas/fdfd/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index cd59b5f..bda7fcf 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -458,7 +458,7 @@ def expand_wgmode_e(E: field_t, a_shape = numpy.roll([1, -1, 1, 1], axis) a_E = numpy.real(dxes[0][axis]).cumsum() r_E = a_E - a_E[slices[axis]] - iphi = polarity * 1j * wavenumber + iphi = polarity * -1j * wavenumber phase_E = numpy.exp(iphi * r_E).reshape(a_shape) # Expand our slice to the entire grid using the phase factors From 7b56aa363f506759a5133e87b4cf8c60372c25d5 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 01:02:54 -0700 Subject: [PATCH 131/437] Use non-vectorized fields for waveguide_mode functions --- meanas/fdfd/waveguide_mode.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index bda7fcf..fc25c70 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -10,8 +10,8 @@ from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration def solve_waveguide_mode_2d(mode_number: int, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, + epsilon: field_t, + mu: field_t = None, mode_margin: int = 2, ) -> Dict[str, complex or field_t]: """ @@ -25,14 +25,14 @@ def solve_waveguide_mode_2d(mode_number: int, :param mode_margin: The eigensolver will actually solve for (mode_number + mode_margin) modes, but only return the target mode. Increasing this value can improve the solver's ability to find the correct mode. Default 2. - :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} + :return: {'E': numpy.ndarray, 'H': numpy.ndarray, 'wavenumber': complex} """ ''' Solve for the largest-magnitude eigenvalue of the real operator ''' dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] - A_r = waveguide.operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) + A_r = waveguide.operator_e(numpy.real(omega), dxes_real, vec(numpy.real(epsilon)), vec(numpy.real(mu))) eigvals, eigvecs = signed_eigensolve(A_r, mode_number + mode_margin) exy = eigvecs[:, -(mode_number + 1)] @@ -41,14 +41,14 @@ def solve_waveguide_mode_2d(mode_number: int, Now solve for the eigenvector of the full operator, using the real operator's eigenvector as an initial guess for Rayleigh quotient iteration. ''' - A = waveguide.operator_e(omega, dxes, epsilon, mu) + A = waveguide.operator_e(omega, dxes, vec(epsilon), vec(mu)) eigval, exy = rayleigh_quotient_iteration(A, exy) # Calculate the wave-vector (force the real part to be positive) wavenumber = numpy.sqrt(eigval) wavenumber *= numpy.sign(numpy.real(wavenumber)) - e, h = waveguide.normalized_fields_e(exy, wavenumber, omega, dxes, epsilon, mu) + e, h = waveguide.normalized_fields_e(exy, wavenumber, omega, dxes, vec(epsilon), vec(mu)) shape = [d.size for d in dxes[0]] fields = { @@ -103,8 +103,8 @@ def solve_waveguide_mode(mode_number: int, # Reduce to 2D and solve the 2D problem args_2d = { 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], - 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), - 'mu': vec([mu[i][slices].transpose(order) for i in order]), + 'epsilon': [epsilon[i][slices].transpose(order) for i in order], + 'mu': [mu[i][slices].transpose(order) for i in order], } fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d) From 5f96474497d22c5ef8263f634cbfd43e300a84d7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 01:03:13 -0700 Subject: [PATCH 132/437] Use e_boundary_source for compute_source --- meanas/fdfd/waveguide_mode.py | 38 ++++++++++++----------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index fc25c70..410ee02 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -137,13 +137,13 @@ def solve_waveguide_mode(mode_number: int, def compute_source(E: field_t, - H: field_t, wavenumber: complex, omega: complex, dxes: dx_lists_t, axis: int, polarity: int, slices: List[slice], + epsilon: field_t, mu: field_t = None, ) -> field_t: """ @@ -151,7 +151,6 @@ def compute_source(E: field_t, necessary to position a unidirectional source at the slice location. :param E: E-field of the mode - :param H: H-field of the mode (advanced by half of a Yee cell from E) :param wavenumber: Wavenumber of the mode :param omega: Angular frequency of the simulation :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types @@ -162,32 +161,21 @@ def compute_source(E: field_t, :param mu: Magnetic permeability (default 1 everywhere) :return: J distribution for the unidirectional source """ - J = numpy.zeros_like(E, dtype=complex) - M = numpy.zeros_like(E, dtype=complex) + E_expanded = expand_wgmode_e(E=E, dxes=dxes, wavenumber=wavenumber, axis=axis, + polarity=polarity, slices=slices) - src_order = numpy.roll(range(3), -axis) + smask = [slice(None)] * 4 + if polarity > 0: + smask[axis + 1] = slice(slices[axis].start, None) + else: + smask[axis + 1] = slice(None, slices[axis].stop) -# exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) -# rollby = -1 if polarity > 0 else 0 -# J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity / dxes[1][axis][slices[axis]] -# J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity / dxes[1][axis][slices[axis]] -# M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) / dxes[0][axis][slices[axis]] -# M[src_order[2]] = -numpy.roll(E[src_order[1]], rollby, axis=axis) / dxes[0][axis][slices[axis]] + mask = numpy.zeros_like(E_expanded, dtype=int) + mask[tuple(smask)] = 1 - s2 = [slice(None), slice(None), slice(None)] - s2[axis] = slice(slices[axis].start, slices[axis].stop) - s2 = (src_order, *s2) - - rollby = 1 if polarity < 0 else 0 - exp_iphi = numpy.exp(-1j * -rollby * wavenumber * dxes[1][axis][slices[axis]]) - J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H), -rollby, axis=axis+1)[s2] * exp_iphi * -polarity - M[s2] = numpy.roll(functional.curl_e(dxes=dxes)(E), rollby, axis=axis+1)[s2] - - m2j = functional.m2j(omega, dxes, mu) - Jm = m2j(M) - - Jtot = J + Jm - return Jtot + masked_e2j = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon), mu=vec(mu)) + J = unvec(masked_e2j @ vec(E_expanded), E.shape[1:]) + return J def compute_overlap_e(E: field_t, From 337cee801803d3975b08437a3543402208491bfb Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 01:10:54 -0700 Subject: [PATCH 133/437] Add epsilon arg to compute_overlap_e currently unused but useful for reusing solve_wgmode arguments --- meanas/fdfd/waveguide_mode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 410ee02..35439a0 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -186,6 +186,7 @@ def compute_overlap_e(E: field_t, axis: int, polarity: int, slices: List[slice], + epsilon: field_t, # TODO unused?? mu: field_t = None, ) -> field_t: """ From d2d4220313c2413b83692fbc9b64b1c8ed2556a8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 01:12:36 -0700 Subject: [PATCH 134/437] update example code --- examples/fdfd.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/examples/fdfd.py b/examples/fdfd.py index c20fc3e..f5130be 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -3,7 +3,7 @@ import numpy from numpy.linalg import norm import meanas -from meanas import vec, unvec +from meanas import vec, unvec, fdtd from meanas.fdfd import waveguide_mode, functional, scpml, operators from meanas.fdfd.solvers import generic as generic_solver @@ -125,16 +125,18 @@ def test1(solver=generic_solver): dims[1][0] = dims[0][0] ind_dims = (grid.pos2ind(dims[0], which_shifts=None).astype(int), grid.pos2ind(dims[1], which_shifts=None).astype(int)) + src_axis = 0 wg_args = { 'omega': omega, 'slices': [slice(i, f+1) for i, f in zip(*ind_dims)], 'dxes': dxes, - 'axis': 0, + 'axis': src_axis, 'polarity': +1, + 'epsilon': grid.grids, } - wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, **wg_args, epsilon=grid.grids) - J = waveguide_mode.compute_source(**wg_args, **wg_results) + wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, **wg_args) + J = waveguide_mode.compute_source(**wg_args, E=wg_results['E'], wavenumber=wg_results['wavenumber']) H_overlap = waveguide_mode.compute_overlap_e(**wg_args, **wg_results) pecg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) @@ -145,6 +147,12 @@ def test1(solver=generic_solver): # pmcg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) # pmcg.visualize_isosurface() + def pcolor(v): + vmax = numpy.max(numpy.abs(v)) + pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax) + pyplot.axis('equal') + pyplot.colorbar() + ''' Solve! ''' @@ -167,20 +175,14 @@ def test1(solver=generic_solver): ''' Plot results ''' - def pcolor(v): - vmax = numpy.max(numpy.abs(v)) - pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax) - pyplot.axis('equal') - pyplot.colorbar() - center = grid.pos2ind([0, 0, 0], None).astype(int) pyplot.figure() pyplot.subplot(2, 2, 1) - pcolor(numpy.real(E[1][center[0], :, :])) + pcolor(numpy.real(E[1][center[0], :, :]).T) pyplot.subplot(2, 2, 2) pyplot.plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10)) pyplot.subplot(2, 2, 3) - pcolor(numpy.real(E[1][:, :, center[2]])) + pcolor(numpy.real(E[1][:, :, center[2]]).T) pyplot.subplot(2, 2, 4) def poyntings(E): @@ -213,7 +215,7 @@ def module_available(name): if __name__ == '__main__': - test0() + #test0() if module_available('opencl_fdfd'): from opencl_fdfd import cg_solver as opencl_solver From f4bac9598d78043a73b6a60dbc70441ccb2ad531 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 01:18:44 -0700 Subject: [PATCH 135/437] Remove unused waveguide_mode functions --- meanas/fdfd/waveguide_mode.py | 101 ---------------------------------- 1 file changed, 101 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 35439a0..8f90d3e 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -307,107 +307,6 @@ def solve_waveguide_mode_cylindrical(mode_number: int, return fields -def compute_source_q(E: field_t, - H: field_t, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - mu: field_t = None, - ) -> field_t: - A1f = functional.curl_h(dxes) - A2f = functional.curl_e(dxes) - - J = A1f(H) - M = A2f(-E) - - m2j = functional.m2j(omega, dxes, mu) - Jm = m2j(M) - - Jtot = J + Jm - return Jtot, J, M - - -def compute_source_e(QE: field_t, - omega: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - epsilon: field_t, - mu: field_t = None, - ) -> field_t: - """ - Want AQE = -iwJ, where Q is mask and normally AE = -iwJ - ## Want (AQ-QA) E = -iwJ, where Q is a mask - ## If E is an eigenmode, AE = 0 so just AQE = -iwJ - Really only need E in 4 cells along axis (0, 0, Emode1, Emode2), find AE (1 iteration), then use center 2 cells as src - Maybe better to use (0, Emode1, Emode2, Emode3), find AE (1 iteration), then use left 2 cells as src? - """ - slices = tuple(slices) - - # Trim a cell from each end of the propagation axis - slices_reduced = list(slices) - for aa in range(3): - if aa == axis: - if polarity > 0: - slices_reduced[axis] = slice(slices[axis].start, slices[axis].start+2) - else: - slices_reduced[axis] = slice(slices[axis].stop-2, slices[axis].stop) - else: - start = slices[aa].start - stop = slices[aa].stop -# if start is not None or stop is not None: -# if start is None: -# start = 1 -# stop -= 1 -# elif stop is None: -# stop = E.shape[aa + 1] - 1 -# start += 1 -# else: -# start += 1 -# stop -= 1 -# slices_reduced[aa] = slice(start, stop) - slices_reduced = (slice(None), *slices_reduced) - - # Don't actually need to mask out E here since it needs to be pre-masked (QE) - - A = functional.e_full(omega, dxes, epsilon, mu) - J4 = A(QE) / (-1j * omega) #J4 is 4-cell result of -iwJ = A QE - - J = numpy.zeros_like(J4) - J[slices_reduced] = J4[slices_reduced] - return J - - -def compute_source_wg(E: field_t, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - epsilon: field_t, - mu: field_t = None, - ) -> field_t: - slices = tuple(slices) - Etgt, _slices2 = compute_overlap_ce(E=E, wavenumber=wavenumber, - dxes=dxes, axis=axis, polarity=polarity, - slices=slices) - - slices4 = list(slices) - slices4[axis] = slice(slices[axis].start - 4 * polarity, slices[axis].start) - slices4 = tuple(slices4) - - J = compute_source_e(QE=Etgt, - omega=omega, dxes=dxes, axis=axis, - polarity=polarity, slices=slices4, - epsilon=epsilon, mu=mu) - return J - - def compute_overlap_ce(E: field_t, wavenumber: complex, dxes: dx_lists_t, From e99019b37f2dd533bfcf15fa95b48170a7e1304c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 27 Aug 2019 00:40:49 -0700 Subject: [PATCH 136/437] v -> e_xy for cylindrical mode result --- meanas/fdfd/waveguide_mode.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 8f90d3e..3321c4f 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -280,14 +280,14 @@ def solve_waveguide_mode_cylindrical(mode_number: int, A_r = waveguide.cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0) eigvals, eigvecs = signed_eigensolve(A_r, mode_number + 3) - v = eigvecs[:, -(mode_number+1)] + e_xy = eigvecs[:, -(mode_number+1)] ''' Now solve for the eigenvector of the full operator, using the real operator's eigenvector as an initial guess for Rayleigh quotient iteration. ''' A = waveguide.cylindrical_operator(omega, dxes, epsilon, r0) - eigval, v = rayleigh_quotient_iteration(A, v) + eigval, e_xy = rayleigh_quotient_iteration(A, e_xy) # Calculate the wave-vector (force the real part to be positive) wavenumber = numpy.sqrt(eigval) @@ -296,10 +296,10 @@ def solve_waveguide_mode_cylindrical(mode_number: int, # TODO: Perform correction on wavenumber to account for numerical dispersion. shape = [d.size for d in dxes[0]] - v = numpy.hstack((v, numpy.zeros(shape[0] * shape[1]))) + e_xy = numpy.hstack((e_xy, numpy.zeros(shape[0] * shape[1]))) fields = { 'wavenumber': wavenumber, - 'E': unvec(v, shape), + 'E': unvec(e_xy, shape), # 'E': unvec(e, shape), # 'H': unvec(h, shape), } From a4c2239ad9b05e4f0db0ed7c9f14ca978649330a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 27 Aug 2019 00:40:59 -0700 Subject: [PATCH 137/437] formatting --- meanas/fdfd/waveguide_mode.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 3321c4f..5c02655 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -316,9 +316,8 @@ def compute_overlap_ce(E: field_t, ) -> field_t: slices = tuple(slices) - Ee = expand_wgmode_e(E=E, wavenumber=wavenumber, - dxes=dxes, axis=axis, polarity=polarity, - slices=slices) + Ee = expand_wgmode_e(E=E, wavenumber=wavenumber, dxes=dxes, + axis=axis, polarity=polarity, slices=slices) start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity)) From 8eac9df76e78c2c36eea3c18115848de96e8f8f6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 30 Aug 2019 22:03:54 -0700 Subject: [PATCH 138/437] in progress --- examples/fdfd.py | 25 ++++++++--- meanas/fdfd/operators.py | 20 ++++++--- meanas/fdfd/waveguide.py | 2 +- meanas/fdfd/waveguide_mode.py | 80 ++++++----------------------------- 4 files changed, 46 insertions(+), 81 deletions(-) diff --git a/examples/fdfd.py b/examples/fdfd.py index f5130be..cf3d206 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -137,7 +137,9 @@ def test1(solver=generic_solver): wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, **wg_args) J = waveguide_mode.compute_source(**wg_args, E=wg_results['E'], wavenumber=wg_results['wavenumber']) - H_overlap = waveguide_mode.compute_overlap_e(**wg_args, **wg_results) + H_overlap, slices = waveguide_mode.compute_overlap_ce(E=wg_results['E'], wavenumber=wg_results['wavenumber'], + dxes=dxes, axis=src_axis, polarity=wg_args['polarity'], + slices=wg_args['slices']) pecg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) # pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) @@ -153,6 +155,13 @@ def test1(solver=generic_solver): pyplot.axis('equal') pyplot.colorbar() + ss = (1, slice(None), J.shape[2]//2+6, slice(None)) +# pyplot.figure() +# pcolor(J3[ss].T.imag) +# pyplot.figure() +# pcolor((numpy.abs(J3).sum(axis=2).sum(axis=0) > 0).astype(float).T) + pyplot.show(block=True) + ''' Solve! ''' @@ -186,12 +195,14 @@ def test1(solver=generic_solver): pyplot.subplot(2, 2, 4) def poyntings(E): - e = vec(E) - h = operators.e2h(omega, dxes) @ e - cross1 = operators.poynting_e_cross(e, dxes) @ h.conj() - cross2 = operators.poynting_h_cross(h.conj(), dxes) @ e + H = functional.e2h(omega, dxes)(E) + poynting = 0.5 * fdtd.poynting(e=E, h=H.conj()) * dx * dx + cross1 = operators.poynting_e_cross(vec(E), dxes) @ vec(H).conj() +# cross2 = operators.poynting_h_cross(h.conj(), dxes) @ e s1 = unvec(0.5 * numpy.real(cross1), grid.shape) - s2 = unvec(0.5 * numpy.real(-cross2), grid.shape) +# s2 = unvec(0.5 * numpy.real(-cross2), grid.shape) + s2 = poynting.real +# s2 = poynting.imag return s1, s2 s1x, s2x = poyntings(E) @@ -202,7 +213,7 @@ def test1(solver=generic_solver): q = [] for i in range(-5, 30): H_rolled = [numpy.roll(h, i, axis=0) for h in H_overlap] - q += [numpy.abs(vec(E) @ vec(H_rolled))] + q += [numpy.abs(vec(E) @ vec(H_rolled).conj())] pyplot.figure() pyplot.plot(q) pyplot.title('Overlap with mode') diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index c2df8ab..47cdf14 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -453,8 +453,7 @@ def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ shape = [len(dx) for dx in dxes[0]] - fx, fy, fz = [avgf(i, shape) for i in range(3)] - bx, by, bz = [avgb(i, shape) for i in range(3)] + bx, by, bz = [rotation(i, shape, -1) for i in range(3)] dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] dbgx, dbgy, dbgz = [sparse.diags(dx.ravel(order='C')) @@ -463,12 +462,11 @@ def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: Ex, Ey, Ez = [sparse.diags(ei * da) for ei, da in zip(numpy.split(e, 3), dxag)] n = numpy.prod(shape) - zero = sparse.csr_matrix((n, n)) P = sparse.bmat( - [[ zero, -fx @ Ez @ bz @ dbgy, fx @ Ey @ by @ dbgz], - [ fy @ Ez @ bz @ dbgx, zero, -fy @ Ex @ bx @ dbgz], - [-fz @ Ey @ by @ dbgx, fz @ Ex @ bx @ dbgy, zero]]) + [[ None, -bx @ Ez @ dbgy, bx @ Ey @ dbgz], + [ by @ Ez @ dbgx, None, -by @ Ex @ dbgz], + [-bz @ Ey @ dbgx, bz @ Ex @ dbgy, None]]) return P @@ -482,7 +480,7 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ shape = [len(dx) for dx in dxes[0]] - fx, fy, fz = [avgf(i, shape) for i in range(3)] + fx, fy, fz = [avgf(i, shape) for i in range(3)] #TODO bx, by, bz = [avgb(i, shape) for i in range(3)] dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] @@ -545,4 +543,12 @@ def e_boundary_source(mask: vfield_t, r3 = sparse.block_diag((r, r, r)) jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask)) +# jmask = ((numpy.roll(mask, -1, axis=0) != mask) | +# (numpy.roll(mask, +1, axis=0) != mask) | +# (numpy.roll(mask, -1, axis=1) != mask) | +# (numpy.roll(mask, +1, axis=1) != mask) | +# (numpy.roll(mask, -1, axis=2) != mask) | +# (numpy.roll(mask, +1, axis=2) != mask)) + return sparse.diags(jmask.astype(int)) @ full + diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py index 63ecb98..f84bbfe 100644 --- a/meanas/fdfd/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -186,7 +186,7 @@ def _normalized_fields(e: numpy.ndarray, norm_amplitude = 1 / numpy.sqrt(P) norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric - # Try to break symmetry to assign a consistent sign [experimental] + # Try to break symmetry to assign a consistent sign [experimental TODO] E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape) sign = numpy.sign(E_weighted[:, :max(shape[0]//2, 1), :max(shape[1]//2, 1)].real.sum()) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 5c02655..cfa888a 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -112,6 +112,8 @@ def solve_waveguide_mode(mode_number: int, Apply corrections and expand to 3D ''' # Correct wavenumber to account for numerical dispersion. + print(fields_2d['wavenumber'] / (2/dx_prop * numpy.arcsin(fields_2d['wavenumber'] * dx_prop/2))) + print(fields_2d['wavenumber'].real / (2/dx_prop * numpy.arcsin(fields_2d['wavenumber'].real * dx_prop/2))) fields_2d['wavenumber'] = 2/dx_prop * numpy.arcsin(fields_2d['wavenumber'] * dx_prop/2) # Adjust for propagation direction @@ -179,20 +181,16 @@ def compute_source(E: field_t, def compute_overlap_e(E: field_t, - H: field_t, wavenumber: complex, - omega: complex, dxes: dx_lists_t, axis: int, polarity: int, slices: List[slice], - epsilon: field_t, # TODO unused?? - mu: field_t = None, - ) -> field_t: + ) -> field_t: # TODO DOCS """ Given an eigenmode obtained by solve_waveguide_mode, calculates overlap_e for the mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) - [assumes reflection symmetry]. + [assumes reflection symmetry].i overlap_e makes use of the e2h operator to collapse the above expression into (vec(E) @ vec(overlap_e)), allowing for simple calculation of the mode overlap. @@ -211,45 +209,20 @@ def compute_overlap_e(E: field_t, """ slices = tuple(slices) - cross_plane = [slice(None)] * 4 - cross_plane[axis + 1] = slices[axis] - cross_plane = tuple(cross_plane) + Ee = expand_wgmode_e(E=E, wavenumber=wavenumber, dxes=dxes, + axis=axis, polarity=polarity, slices=slices) - # Determine phase factors for parallel slices - a_shape = numpy.roll([-1, 1, 1], axis) - a_E = numpy.real(dxes[0][axis]).cumsum() - a_H = numpy.real(dxes[1][axis]).cumsum() - iphi = -polarity * 1j * wavenumber - phase_E = numpy.exp(iphi * (a_E - a_E[slices[axis]])).reshape(a_shape) - phase_H = numpy.exp(iphi * (a_H - a_H[slices[axis]])).reshape(a_shape) + start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity)) - # Expand our slice to the entire grid using the calculated phase factors - Ee = phase_E * E[cross_plane] - He = phase_H * H[cross_plane] + slices2 = list(slices) + slices2[axis] = slice(start, stop) + slices2 = (slice(None), *slices2) + Etgt = numpy.zeros_like(Ee) + Etgt[slices2] = Ee[slices2] - # Write out the operator product for the mode orthogonality integral - domain = numpy.zeros_like(E[0], dtype=int) - domain[slices] = 1 - - npts = E[0].size - dn = numpy.zeros(npts * 3, dtype=int) - dn[0:npts] = 1 - dn = numpy.roll(dn, npts * axis) - - e2h = operators.e2h(omega, dxes, mu) - ds = sparse.diags(vec([domain]*3)) - h_cross_ = operators.poynting_h_cross(vec(He), dxes) - e_cross_ = operators.poynting_e_cross(vec(Ee), dxes) - - overlap_e = dn @ ds @ (-h_cross_ + e_cross_ @ e2h) - - # Normalize - dx_forward = dxes[0][axis][slices[axis]] - norm_factor = numpy.abs(overlap_e @ vec(Ee)) - overlap_e /= norm_factor * dx_forward - - return unvec(overlap_e, E[0].shape) + Etgt /= (Etgt.conj() * Etgt).sum() + return Etgt.conj() def solve_waveguide_mode_cylindrical(mode_number: int, @@ -307,31 +280,6 @@ def solve_waveguide_mode_cylindrical(mode_number: int, return fields -def compute_overlap_ce(E: field_t, - wavenumber: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - ) -> field_t: - slices = tuple(slices) - - Ee = expand_wgmode_e(E=E, wavenumber=wavenumber, dxes=dxes, - axis=axis, polarity=polarity, slices=slices) - - start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity)) - - slices2 = list(slices) - slices2[axis] = slice(start, stop) - slices2 = (slice(None), *slices2) - - Etgt = numpy.zeros_like(Ee) - Etgt[slices2] = Ee[slices2] - - Etgt /= (Etgt.conj() * Etgt).sum() - return Etgt, slices2 - - def expand_wgmode_e(E: field_t, wavenumber: complex, dxes: dx_lists_t, From b5ad284966866b8e092693b22f588a67f6abcf07 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 5 Sep 2019 22:35:23 +0200 Subject: [PATCH 139/437] dx_prop -> prop_phase propagation direction wavenumber might be different from operator-derived (2D) wavenumber due to numerical dispersion, so lump it in with dx_prop --- meanas/fdfd/waveguide.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py index f84bbfe..5fc490c 100644 --- a/meanas/fdfd/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -109,7 +109,7 @@ def normalized_fields_e(e_xy: numpy.ndarray, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, - dx_prop: float = 0, + prop_phase: float = 0, ) -> Tuple[vfield_t, vfield_t]: """ Given a vector e_xy containing the vectorized E_x and E_y fields, @@ -121,13 +121,14 @@ def normalized_fields_e(e_xy: numpy.ndarray, :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :param dxes_prop: Grid cell width in the propagation direction. Default 0 (continuous). + :param prop_phase: Phase shift (dz * corrected_wavenumber) over 1 cell in propagation direction. + Default 0 (continuous propagation direction, i.e. dz->0). :return: Normalized, vectorized (e, h) containing all vector components. """ e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy - e_norm, h_norm = _normalized_fields(e=e, h=h, wavenumber=wavenumber, omega=omega, - dxes=dxes, epsilon=epsilon, mu=mu, dx_prop=dx_prop) + e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, + mu=mu, prop_phase=prop_phase) return e_norm, h_norm @@ -137,7 +138,7 @@ def normalized_fields_h(h_xy: numpy.ndarray, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, - dx_prop: float = 0, + prop_phase: float = 0, ) -> Tuple[vfield_t, vfield_t]: """ Given a vector e_xy containing the vectorized E_x and E_y fields, @@ -154,19 +155,18 @@ def normalized_fields_h(h_xy: numpy.ndarray, """ e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy - e_norm, h_norm = _normalized_fields(e=e, h=h, wavenumber=wavenumber, omega=omega, - dxes=dxes, epsilon=epsilon, mu=mu, dx_prop=dx_prop) + e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, + mu=mu, prop_phase=prop_phase) return e_norm, h_norm def _normalized_fields(e: numpy.ndarray, h: numpy.ndarray, - wavenumber: complex, omega: complex, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, - dx_prop: float = 0, + prop_phase: float = 0, ) -> Tuple[vfield_t, vfield_t]: # TODO documentation shape = [s.size for s in dxes[0]] @@ -175,7 +175,7 @@ def _normalized_fields(e: numpy.ndarray, E = unvec(e, shape) H = unvec(h, shape) - phase = numpy.exp(-1j * wavenumber * dx_prop / 2) + phase = numpy.exp(-1j * prop_phase / 2) S1 = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0] S2 = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1] P = numpy.real(S1.sum() - S2.sum()) From f04c0daf287722eb31b00dd3c9c4d09d8103fa04 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 5 Sep 2019 22:38:29 +0200 Subject: [PATCH 140/437] solve_waveguide_mode_2d -> vsolve_* - return (e_xy. wavenumber) - vectorized inputs since we returned a vectorized output - exy -> e_xy --- meanas/fdfd/waveguide_mode.py | 66 +++++++++++++++++------------------ 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index cfa888a..6bdbc73 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Tuple import numpy import scipy.sparse as sparse @@ -7,13 +7,13 @@ from . import operators, waveguide, functional from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration -def solve_waveguide_mode_2d(mode_number: int, - omega: complex, - dxes: dx_lists_t, - epsilon: field_t, - mu: field_t = None, - mode_margin: int = 2, - ) -> Dict[str, complex or field_t]: +def vsolve_waveguide_mode_2d(mode_number: int, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + mode_margin: int = 2, + ) -> Tuple[vfield_t, complex]: """ Given a 2d region, attempts to solve for the eigenmode with the specified mode number. @@ -25,39 +25,31 @@ def solve_waveguide_mode_2d(mode_number: int, :param mode_margin: The eigensolver will actually solve for (mode_number + mode_margin) modes, but only return the target mode. Increasing this value can improve the solver's ability to find the correct mode. Default 2. - :return: {'E': numpy.ndarray, 'H': numpy.ndarray, 'wavenumber': complex} + :return: (e_xy, wavenumber) """ ''' Solve for the largest-magnitude eigenvalue of the real operator ''' dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] - A_r = waveguide.operator_e(numpy.real(omega), dxes_real, vec(numpy.real(epsilon)), vec(numpy.real(mu))) + A_r = waveguide.operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) eigvals, eigvecs = signed_eigensolve(A_r, mode_number + mode_margin) - exy = eigvecs[:, -(mode_number + 1)] + e_xy = eigvecs[:, -(mode_number + 1)] ''' Now solve for the eigenvector of the full operator, using the real operator's eigenvector as an initial guess for Rayleigh quotient iteration. ''' - A = waveguide.operator_e(omega, dxes, vec(epsilon), vec(mu)) - eigval, exy = rayleigh_quotient_iteration(A, exy) + A = waveguide.operator_e(omega, dxes, epsilon, mu) + eigval, e_xy = rayleigh_quotient_iteration(A, e_xy) # Calculate the wave-vector (force the real part to be positive) wavenumber = numpy.sqrt(eigval) wavenumber *= numpy.sign(numpy.real(wavenumber)) - e, h = waveguide.normalized_fields_e(exy, wavenumber, omega, dxes, vec(epsilon), vec(mu)) + return e_xy, wavenumber - shape = [d.size for d in dxes[0]] - fields = { - 'wavenumber': wavenumber, - 'E': unvec(e, shape), - 'H': unvec(h, shape), - } - - return fields def solve_waveguide_mode(mode_number: int, @@ -102,36 +94,42 @@ def solve_waveguide_mode(mode_number: int, # Reduce to 2D and solve the 2D problem args_2d = { + 'omega': omega, 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], - 'epsilon': [epsilon[i][slices].transpose(order) for i in order], - 'mu': [mu[i][slices].transpose(order) for i in order], + 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), + 'mu': vec([mu[i][slices].transpose(order) for i in order]), } - fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d) + e_xy, wavenumber_2d = vsolve_waveguide_mode_2d(mode_number, **args_2d) ''' Apply corrections and expand to 3D ''' # Correct wavenumber to account for numerical dispersion. - print(fields_2d['wavenumber'] / (2/dx_prop * numpy.arcsin(fields_2d['wavenumber'] * dx_prop/2))) - print(fields_2d['wavenumber'].real / (2/dx_prop * numpy.arcsin(fields_2d['wavenumber'].real * dx_prop/2))) - fields_2d['wavenumber'] = 2/dx_prop * numpy.arcsin(fields_2d['wavenumber'] * dx_prop/2) + wavenumber = 2/dx_prop * numpy.arcsin(wavenumber_2d * dx_prop/2) + print(wavenumber_2d / wavenumber) + + shape = [d.size for d in args_2d['dxes'][0]] + ve, vh = waveguide.normalized_fields_e(e_xy, wavenumber=wavenumber_2d, **args_2d, prop_phase=dx_prop * wavenumber) + e = unvec(ve, shape) + h = unvec(vh, shape) # Adjust for propagation direction - fields_2d['H'] *= polarity + h *= polarity # Apply phase shift to H-field - fields_2d['H'][:2] *= numpy.exp(-1j * polarity * 0.5 * fields_2d['wavenumber'] * dx_prop) - fields_2d['E'][2] *= numpy.exp(-1j * polarity * 0.5 * fields_2d['wavenumber'] * dx_prop) + h[:2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop) + e[2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop) # Expand E, H to full epsilon space we were given E = numpy.zeros_like(epsilon, dtype=complex) H = numpy.zeros_like(epsilon, dtype=complex) for a, o in enumerate(reverse_order): - E[(a, *slices)] = fields_2d['E'][o][:, :, None].transpose(reverse_order) - H[(a, *slices)] = fields_2d['H'][o][:, :, None].transpose(reverse_order) + E[(a, *slices)] = e[o][:, :, None].transpose(reverse_order) + H[(a, *slices)] = h[o][:, :, None].transpose(reverse_order) results = { - 'wavenumber': fields_2d['wavenumber'], + 'wavenumber': wavenumber, + 'wavenumber_2d': wavenumber_2d, 'H': H, 'E': E, } From bedec489aad2049961f693e55341f800c4cca3d2 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 5 Sep 2019 22:41:34 +0200 Subject: [PATCH 141/437] Don't pre-conjugate e_overlap --- meanas/fdfd/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 6bdbc73..e94429f 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -220,7 +220,7 @@ def compute_overlap_e(E: field_t, Etgt[slices2] = Ee[slices2] Etgt /= (Etgt.conj() * Etgt).sum() - return Etgt.conj() + return Etgt def solve_waveguide_mode_cylindrical(mode_number: int, From 2289c6d11615ac3dc0a5976208f804ff20657a3c Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 5 Sep 2019 22:42:20 +0200 Subject: [PATCH 142/437] dx_prop should be a scalar? --- meanas/fdfd/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index e94429f..0838188 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -90,7 +90,7 @@ def solve_waveguide_mode(mode_number: int, # Find dx in propagation direction dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes]) - dx_prop = 0.5 * sum(dxab_forward) + dx_prop = 0.5 * sum(dxab_forward)[0] # Reduce to 2D and solve the 2D problem args_2d = { From 10e275611d0ee11f0ce6ab5de97148dd768d5ca7 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 5 Sep 2019 22:42:39 +0200 Subject: [PATCH 143/437] Use overlap_e --- examples/fdfd.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/fdfd.py b/examples/fdfd.py index cf3d206..668ea67 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -137,9 +137,9 @@ def test1(solver=generic_solver): wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, **wg_args) J = waveguide_mode.compute_source(**wg_args, E=wg_results['E'], wavenumber=wg_results['wavenumber']) - H_overlap, slices = waveguide_mode.compute_overlap_ce(E=wg_results['E'], wavenumber=wg_results['wavenumber'], - dxes=dxes, axis=src_axis, polarity=wg_args['polarity'], - slices=wg_args['slices']) + e_overlap = waveguide_mode.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], + dxes=dxes, axis=src_axis, polarity=wg_args['polarity'], + slices=wg_args['slices']) pecg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) # pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) @@ -212,8 +212,8 @@ def test1(solver=generic_solver): q = [] for i in range(-5, 30): - H_rolled = [numpy.roll(h, i, axis=0) for h in H_overlap] - q += [numpy.abs(vec(E) @ vec(H_rolled).conj())] + e_ovl_rolled = numpy.roll(e_overlap, i, axis=1) + q += [numpy.abs(vec(E) @ vec(e_ovl_rolled).conj())] pyplot.figure() pyplot.plot(q) pyplot.title('Overlap with mode') From c92656bed881338db0844bae812171aa84e3ca92 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 5 Sep 2019 22:53:43 +0200 Subject: [PATCH 144/437] Compactify args --- examples/fdfd.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/examples/fdfd.py b/examples/fdfd.py index 668ea67..c158c3c 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -127,19 +127,16 @@ def test1(solver=generic_solver): grid.pos2ind(dims[1], which_shifts=None).astype(int)) src_axis = 0 wg_args = { - 'omega': omega, 'slices': [slice(i, f+1) for i, f in zip(*ind_dims)], 'dxes': dxes, 'axis': src_axis, 'polarity': +1, - 'epsilon': grid.grids, } - wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, **wg_args) - J = waveguide_mode.compute_source(**wg_args, E=wg_results['E'], wavenumber=wg_results['wavenumber']) - e_overlap = waveguide_mode.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], - dxes=dxes, axis=src_axis, polarity=wg_args['polarity'], - slices=wg_args['slices']) + wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, omega=omega, epsilon=grid.grids, **wg_args) + J = waveguide_mode.compute_source(E=wg_results['E'], wavenumber=wg_results['wavenumber'], + omega=omega, epsilon=grid.grids, **wg_args) + e_overlap = waveguide_mode.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], **wg_args) pecg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) # pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) From 9cd2dd131b2e2501a40a47df2011ca893029e20f Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 12 Sep 2019 12:21:09 +0200 Subject: [PATCH 145/437] Move version into setup.py and read it back with pkg_resources --- meanas/__init__.py | 4 +++- setup.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/meanas/__init__.py b/meanas/__init__.py index d4288a5..131646b 100644 --- a/meanas/__init__.py +++ b/meanas/__init__.py @@ -41,8 +41,10 @@ Dependencies: """ +import pkg_resources + from .types import dx_lists_t, field_t, vfield_t, field_updater from .vectorization import vec, unvec __author__ = 'Jan Petykiewicz' -version = '0.5' +__version__ = pkg_resources.get_distribution('meanas').version diff --git a/setup.py b/setup.py index 8e817cb..7093b57 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,12 @@ #!/usr/bin/env python3 from setuptools import setup, find_packages -import meanas with open('README.md', 'r') as f: long_description = f.read() setup(name='meanas', - version=meanas.version, + version='0.5', description='Electromagnetic simulation tools', long_description=long_description, long_description_content_type='text/markdown', From a1a7aa55116f7e80a1009bc71f57c4340bac8da2 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Sep 2019 19:59:08 +0200 Subject: [PATCH 146/437] Alternate approach to poynting matrices --- meanas/fdfd/operators.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 47cdf14..c5c0f87 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -456,9 +456,8 @@ def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: bx, by, bz = [rotation(i, shape, -1) for i in range(3)] dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] - dbgx, dbgy, dbgz = [sparse.diags(dx.ravel(order='C')) - for dx in numpy.meshgrid(*dxes[1], indexing='ij')] - + dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] + dbgx, dbgy, dbgz = [sparse.diags(dx) for dx in dxbg] Ex, Ey, Ez = [sparse.diags(ei * da) for ei, da in zip(numpy.split(e, 3), dxag)] n = numpy.prod(shape) @@ -467,6 +466,9 @@ def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: [[ None, -bx @ Ez @ dbgy, bx @ Ey @ dbgz], [ by @ Ez @ dbgx, None, -by @ Ex @ dbgz], [-bz @ Ey @ dbgx, bz @ Ex @ dbgy, None]]) + #TODO + P2 = sparse.block_diag((bx, by, bz)) @ cross([Ex, Ey, Ez]) @ sparse.diags(numpy.concatenate(dxbg)) + print(sparse.linalg.norm((P-P2)), sparse.linalg.norm(P), sparse.linalg.norm(P2)) return P @@ -490,12 +492,11 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: Hx, Hy, Hz = [sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg)] n = numpy.prod(shape) - zero = sparse.csr_matrix((n, n)) P = sparse.bmat( - [[ zero, -by @ Hz @ fx @ dagy, bz @ Hy @ fx @ dagz], - [ bx @ Hz @ fy @ dagx, zero, -bz @ Hx @ fy @ dagz], - [-bx @ Hy @ fz @ dagx, by @ Hx @ fz @ dagy, zero]]) + [[ None, Hz @ bx @ dagy, Hy @ bx @ dagz], + [ Hz @ by @ dagx, None, -Hx @ by @ dagz], + [-Hy @ bz @ dagx, Hx @ bz @ dagy, None]]) return P From 487bdd61ec86425dd0bd5d3c8c552621efe3094b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 27 Sep 2019 20:43:32 -0700 Subject: [PATCH 147/437] Fixup poynting operators for new approach --- meanas/fdfd/operators.py | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index c5c0f87..7052715 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -453,22 +453,18 @@ def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ shape = [len(dx) for dx in dxes[0]] - bx, by, bz = [rotation(i, shape, -1) for i in range(3)] + fx, fy, fz = [rotation(i, shape, 1) for i in range(3)] dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] - dbgx, dbgy, dbgz = [sparse.diags(dx) for dx in dxbg] - Ex, Ey, Ez = [sparse.diags(ei * da) for ei, da in zip(numpy.split(e, 3), dxag)] + Ex, Ey, Ez = [ei * da for ei, da in zip(numpy.split(e, 3), dxag)] - n = numpy.prod(shape) - - P = sparse.bmat( - [[ None, -bx @ Ez @ dbgy, bx @ Ey @ dbgz], - [ by @ Ez @ dbgx, None, -by @ Ex @ dbgz], - [-bz @ Ey @ dbgx, bz @ Ex @ dbgy, None]]) - #TODO - P2 = sparse.block_diag((bx, by, bz)) @ cross([Ex, Ey, Ez]) @ sparse.diags(numpy.concatenate(dxbg)) - print(sparse.linalg.norm((P-P2)), sparse.linalg.norm(P), sparse.linalg.norm(P2)) + block_diags = [[ None, fx @ -Ez, fx @ Ey], + [ fy @ Ez, None, fy @ -Ex], + [ fz @ -Ey, fz @ Ex, None]] + block_matrix = sparse.bmat([[sparse.diags(x) if x is not None else None for x in row] + for row in block_diags]) + P = block_matrix @ sparse.diags(numpy.concatenate(dxag)) return P @@ -482,21 +478,17 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ shape = [len(dx) for dx in dxes[0]] - fx, fy, fz = [avgf(i, shape) for i in range(3)] #TODO - bx, by, bz = [avgb(i, shape) for i in range(3)] + fx, fy, fz = [rotation(i, shape, 1) for i in range(3)] + dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] - dagx, dagy, dagz = [sparse.diags(dx.ravel(order='C')) - for dx in numpy.meshgrid(*dxes[0], indexing='ij')] - Hx, Hy, Hz = [sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg)] - n = numpy.prod(shape) - - P = sparse.bmat( - [[ None, Hz @ bx @ dagy, Hy @ bx @ dagz], - [ Hz @ by @ dagx, None, -Hx @ by @ dagz], - [-Hy @ bz @ dagx, Hx @ bz @ dagy, None]]) + P = (sparse.bmat( + [[ None, -Hz @ fx, Hy @ fx], + [ Hz @ fy, None, -Hx @ fy], + [-Hy @ fz, Hx @ fz, None]]) + @ sparse.diags(numpy.concatenate(dxag))) return P From 0ad289e5ac733001187e43e042cd95ba00e9d8e8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 27 Sep 2019 20:44:31 -0700 Subject: [PATCH 148/437] Switch to file-based version number --- MANIFEST.in | 3 +++ meanas/VERSION | 1 + meanas/__init__.py | 6 ++++-- setup.py | 8 +++++++- 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 MANIFEST.in create mode 100644 meanas/VERSION diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..41ad357 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md +include LICENSE.md +include meanas/VERSION diff --git a/meanas/VERSION b/meanas/VERSION new file mode 100644 index 0000000..2eb3c4f --- /dev/null +++ b/meanas/VERSION @@ -0,0 +1 @@ +0.5 diff --git a/meanas/__init__.py b/meanas/__init__.py index 131646b..76388dc 100644 --- a/meanas/__init__.py +++ b/meanas/__init__.py @@ -41,10 +41,12 @@ Dependencies: """ -import pkg_resources +import pathlib from .types import dx_lists_t, field_t, vfield_t, field_updater from .vectorization import vec, unvec __author__ = 'Jan Petykiewicz' -__version__ = pkg_resources.get_distribution('meanas').version + +with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f: + __version__ = f.read().strip() diff --git a/setup.py b/setup.py index 7093b57..f5e0a77 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,11 @@ from setuptools import setup, find_packages with open('README.md', 'r') as f: long_description = f.read() +with open('meanas/VERSION', 'r') as f: + version = f.read().strip() + setup(name='meanas', - version='0.5', + version=version, description='Electromagnetic simulation tools', long_description=long_description, long_description_content_type='text/markdown', @@ -14,6 +17,9 @@ setup(name='meanas', author_email='anewusername@gmail.com', url='https://mpxd.net/code/jan/fdfd_tools', packages=find_packages(), + package_data={ + 'meanas': ['VERSION'] + }, install_requires=[ 'numpy', 'scipy', From 6333dbd110b31af9876552b17c38874981fe58d9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 27 Sep 2019 20:47:22 -0700 Subject: [PATCH 149/437] add missing __init__.py's --- meanas/fdfd/__init__.py | 0 meanas/test/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 meanas/fdfd/__init__.py create mode 100644 meanas/test/__init__.py diff --git a/meanas/fdfd/__init__.py b/meanas/fdfd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/meanas/test/__init__.py b/meanas/test/__init__.py new file mode 100644 index 0000000..e69de29 From 6850fe532f388aac98e61bce011a7e8a5a30a7ad Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 27 Sep 2019 20:48:03 -0700 Subject: [PATCH 150/437] poynting calculation comparison --- examples/fdfd.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/fdfd.py b/examples/fdfd.py index c158c3c..6c8577c 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -62,6 +62,7 @@ def test0(solver=generic_solver): J = [numpy.zeros_like(grid.grids[0], dtype=complex) for _ in range(3)] J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1 + ''' Solve! ''' @@ -195,16 +196,18 @@ def test1(solver=generic_solver): H = functional.e2h(omega, dxes)(E) poynting = 0.5 * fdtd.poynting(e=E, h=H.conj()) * dx * dx cross1 = operators.poynting_e_cross(vec(E), dxes) @ vec(H).conj() -# cross2 = operators.poynting_h_cross(h.conj(), dxes) @ e + cross2 = operators.poynting_h_cross(vec(H), dxes) @ vec(E).conj() * -1 s1 = unvec(0.5 * numpy.real(cross1), grid.shape) -# s2 = unvec(0.5 * numpy.real(-cross2), grid.shape) - s2 = poynting.real + s2 = unvec(0.5 * numpy.real(cross2), grid.shape) + s0 = poynting.real # s2 = poynting.imag - return s1, s2 + return s0, s1, s2 - s1x, s2x = poyntings(E) - pyplot.plot(s1x[0].sum(axis=2).sum(axis=1)) - pyplot.plot(s2x[0].sum(axis=2).sum(axis=1)) + s0x, s1x, s2x = poyntings(E) + pyplot.plot(s0x[0].sum(axis=2).sum(axis=1), label='s0') + pyplot.plot(s1x[0].sum(axis=2).sum(axis=1), label='s1') + pyplot.plot(s2x[0].sum(axis=2).sum(axis=1), label='s2') + pyplot.legend() pyplot.show() q = [] From 7f22f83c686701828b6b692f6cfe7d305f4f3021 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 8 Oct 2019 23:56:33 -0700 Subject: [PATCH 151/437] Update README with install instructions --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3ead0ca..71bdf92 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ -# meanas +# meanas README **meanas** is a python package for electromagnetic simulations +** UNSTABLE / WORK IN PROGRESS ** + +Formerly known as [fdfd_tools](https://mpxd.net/code/jan/fdfd_tools). + This package is intended for building simulation inputs, analyzing simulation outputs, and running short simulations on unspecialized hardware. It is designed to provide tooling and a baseline for other, high-performance @@ -31,10 +35,13 @@ solver uses scipy's eigenvalue solver, with reasonable results. For solving large (or 3D) FDFD problems, I recommend a GPU-based iterative solver, such as [opencl_fdfd](https://mpxd.net/code/jan/opencl_fdfd) or -those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html)). Your +those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html). Your solver will need the ability to solve complex symmetric (non-Hermitian) linear systems, ideally with double precision. +- [Source repository](https://mpxd.net/code/jan/meanas) +- PyPI *TBD* + ## Installation @@ -44,11 +51,47 @@ linear systems, ideally with double precision. * scipy -Install with pip, via git: +Install from PyPI with pip: ```bash -pip install git+https://mpxd.net/code/jan/meanas.git@release +pip3 install 'meanas[test,examples]' ``` +### Development install +Install python3.7 and virtualenv: +```bash +# This is for Debian/Ubuntu/other-apt-based systems; you may need an alternative command +sudo apt install python3.7 virtualenv build-essential python3.7-dev +``` + +If python 3.7 is not your default python3 version, create a virtualenv: +```bash +# Check python3 version: +python3 --version +# output: Python 3.7.5rc1 + +# Create a virtual environment using python3.7 and place it in the directory `venv/` +virtualenv -p python3.7 venv +``` + +In-place development install: +```bash +# Download using git +git clone --branch ongoing https://mpxd.net/code/jan/fdfd_tools.git meanas/ + +# NOTE: In the future this will become +#git clone https://mpxd.net/code/jan/meanas.git + +# If you are using a virtualenv, activate it +source venv/bin/activate + +# Install in-place (-e, editable) from ./meanas, including testing and example dependencies ([test, examples]) +pip3 install --user -e './meanas[test,examples]' + +# Run tests +python3 -m pytest +``` + + ## Use See `examples/` for some simple examples; you may need additional From 74e212f5602eab791df8d9ae55d2727f0e6d1cd3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 8 Oct 2019 23:56:49 -0700 Subject: [PATCH 152/437] examples require gridlock --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index f5e0a77..197b9e8 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,9 @@ setup(name='meanas', 'pytest', 'dataclasses', ], + 'examples': [ + 'gridlock', + ], }, classifiers=[ 'Programming Language :: Python :: 3', From 6c032fbdc3357cc859a39e189abdfd8caf247d82 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 8 Oct 2019 23:59:03 -0700 Subject: [PATCH 153/437] Note temporary source location --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 71bdf92..c5ce9a8 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,8 @@ those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html). Your solver will need the ability to solve complex symmetric (non-Hermitian) linear systems, ideally with double precision. -- [Source repository](https://mpxd.net/code/jan/meanas) +- [WORKING Source repository](https://mpxd.net/code/jan/fdfd_tools/src/branch/ongoing) +- *TODO* [Source repository](https://mpxd.net/code/jan/meanas) - PyPI *TBD* From c6e463209fb5a220c7b438f02b0bd7781fe6f91a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 8 Oct 2019 23:59:22 -0700 Subject: [PATCH 154/437] Update repo location --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 197b9e8..8438f8f 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup(name='meanas', long_description_content_type='text/markdown', author='Jan Petykiewicz', author_email='anewusername@gmail.com', - url='https://mpxd.net/code/jan/fdfd_tools', + url='https://mpxd.net/code/jan/meanas', packages=find_packages(), package_data={ 'meanas': ['VERSION'] From 2ba75a418959004765052c79bf61aa4bc27863e9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 9 Oct 2019 00:00:24 -0700 Subject: [PATCH 155/437] branch name changes to --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c5ce9a8..fccc717 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html). Your solver will need the ability to solve complex symmetric (non-Hermitian) linear systems, ideally with double precision. -- [WORKING Source repository](https://mpxd.net/code/jan/fdfd_tools/src/branch/ongoing) +- [WORKING Source repository](https://mpxd.net/code/jan/fdfd_tools/src/branch/wip) - *TODO* [Source repository](https://mpxd.net/code/jan/meanas) - PyPI *TBD* @@ -77,7 +77,7 @@ virtualenv -p python3.7 venv In-place development install: ```bash # Download using git -git clone --branch ongoing https://mpxd.net/code/jan/fdfd_tools.git meanas/ +git clone --branch wip https://mpxd.net/code/jan/fdfd_tools.git meanas/ # NOTE: In the future this will become #git clone https://mpxd.net/code/jan/meanas.git From ca7bc717880276d0da549e9d975939b6d7d44db8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 9 Oct 2019 00:03:31 -0700 Subject: [PATCH 156/437] add git to necessary programs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fccc717..76295af 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,10 @@ pip3 install 'meanas[test,examples]' ``` ### Development install -Install python3.7 and virtualenv: +Install python3.7, virtualenv, and git: ```bash # This is for Debian/Ubuntu/other-apt-based systems; you may need an alternative command -sudo apt install python3.7 virtualenv build-essential python3.7-dev +sudo apt install python3.7 virtualenv build-essential python3.7-dev git ``` If python 3.7 is not your default python3 version, create a virtualenv: From cde8537f140878b0f7f42f0c8ae27ddf9b9cc5ae Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 9 Oct 2019 00:10:46 -0700 Subject: [PATCH 157/437] add see-also links --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 76295af..2c89a4d 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,12 @@ pip3 install --user -e './meanas[test,examples]' python3 -m pytest ``` +### See also: +- [git book](https://git-scm.com/book/en/v2) +- [virtualenv documentation](https://virtualenv.pypa.io/en/stable/userguide/) +- [python language reference](https://docs.python.org/3/reference/index.html) +- [python standard library](https://docs.python.org/3/library/index.html) + ## Use From 9709a0a41591da0ad75538052f1d60654f61dfd1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 9 Oct 2019 00:11:27 -0700 Subject: [PATCH 158/437] make subheading smaller --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c89a4d..2f2b3d6 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ pip3 install --user -e './meanas[test,examples]' python3 -m pytest ``` -### See also: +#### See also: - [git book](https://git-scm.com/book/en/v2) - [virtualenv documentation](https://virtualenv.pypa.io/en/stable/userguide/) - [python language reference](https://docs.python.org/3/reference/index.html) From 88123342be3987b93f925beeadec47af9d7854cf Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 10 Oct 2019 00:01:57 -0700 Subject: [PATCH 159/437] note that venv is optional --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f2b3d6..04aeae7 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,9 @@ If python 3.7 is not your default python3 version, create a virtualenv: ```bash # Check python3 version: python3 --version -# output: Python 3.7.5rc1 +# output on my system: Python 3.7.5rc1 +# If this indicates a version >= 3.7, you can skip all +# the steps involving virtualenv or referencing the venv/ directory # Create a virtual environment using python3.7 and place it in the directory `venv/` virtualenv -p python3.7 venv From 62572545795b4f1e8694ec9ca2f5c76c1b7b3dbb Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 12:41:08 -0700 Subject: [PATCH 160/437] add fdfd.poynting_e_cross_h() --- meanas/fdfd/functional.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index fe11e51..326f38b 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -204,3 +204,19 @@ def e_tfsf_source(TF_region: field_t, neg_iwj = A(TF_region * e) - TF_region * A(e) return neg_iwj / (-1j * omega) + +def poynting_e_cross_h(dxes: dx_lists_t): + def exh(e: field_t, h: field_t): + s = numpy.empty_like(e) + ex = e[0] * dxes[0][0][:, None, None] + ey = e[1] * dxes[0][1][None, :, None] + ez = e[2] * dxes[0][2][None, None, :] + hx = h[0] * dxes[1][0][:, None, None] + hy = h[1] * dxes[1][1][None, :, None] + hz = h[2] * dxes[1][2][None, None, :] + s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy + s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz + s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx + return s + return exh + From c0f14cd07b55abe46f35e067b8def23c9feaff39 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 12:42:21 -0700 Subject: [PATCH 161/437] Modify phase factor for 1-cell shift (poynting). Also clarify some variable names --- meanas/fdfd/waveguide.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py index 5fc490c..14e3502 100644 --- a/meanas/fdfd/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -175,15 +175,17 @@ def _normalized_fields(e: numpy.ndarray, E = unvec(e, shape) H = unvec(h, shape) - phase = numpy.exp(-1j * prop_phase / 2) - S1 = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0] - S2 = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1] - P = numpy.real(S1.sum() - S2.sum()) - assert P > 0, 'Found a mode propagating in the wrong direction! P={}'.format(P) + # Find time-averaged Sz and normalize to it + # H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting + phase = numpy.exp(-1j * -prop_phase / 2) + Sz_a = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0] + Sz_b = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1] + Sz_tavg = numpy.real(Sz_a.sum() - Sz_b.sum()) * 0.5 # 0.5 since E, H are assumed to be peak (not RMS) amplitudes + assert Sz_tavg > 0, 'Found a mode propagating in the wrong direction! Sz_tavg={}'.format(Sz_tavg) energy = epsilon * e.conj() * e - norm_amplitude = 1 / numpy.sqrt(P) + norm_amplitude = 1 / numpy.sqrt(Sz_tavg) norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric # Try to break symmetry to assign a consistent sign [experimental TODO] @@ -488,5 +490,3 @@ def cylindrical_operator(omega: complex, return op - - From 2a1fa1df7bdf79c285fe82d655bd58b01d24c3d4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 12:43:06 -0700 Subject: [PATCH 162/437] Take care of dxes at the poynting() level rather than poynting_divergence --- meanas/fdtd/energy.py | 34 +++++++++++++++++++++------------- meanas/test/test_fdtd.py | 18 +++++------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index 26fb036..ffbbf80 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -4,23 +4,31 @@ import numpy from .. import dx_lists_t, field_t, field_updater -def poynting(e, h): - s = (numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], - numpy.roll(e[2], -1, axis=1) * h[0] - numpy.roll(e[0], -1, axis=1) * h[2], - numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]) - return numpy.array(s) - - -def poynting_divergence(s=None, *, e=None, h=None, dxes=None): # TODO dxes +def poynting(e, h, dxes=None): if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) - if s is None: - s = poynting(e, h) + ex = e[0] * dxes[0][0][:, None, None] + ey = e[1] * dxes[0][1][None, :, None] + ez = e[2] * dxes[0][2][None, None, :] + hx = h[0] * dxes[1][0][:, None, None] + hy = h[1] * dxes[1][1][None, :, None] + hz = h[2] * dxes[1][2][None, None, :] - ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) / numpy.sqrt(dxes[0][0] * dxes[1][0])[:, None, None] + - (s[1] - numpy.roll(s[1], 1, axis=1)) / numpy.sqrt(dxes[0][1] * dxes[1][1])[None, :, None] + - (s[2] - numpy.roll(s[2], 1, axis=2)) / numpy.sqrt(dxes[0][2] * dxes[1][2])[None, None, :] ) + s = numpy.empty_like(e) + s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy + s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz + s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx + return s + + +def poynting_divergence(s=None, *, e=None, h=None, dxes=None): # TODO dxes + if s is None: + s = poynting(e, h, dxes=dxes) + + ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) + + (s[1] - numpy.roll(s[1], 1, axis=1)) + + (s[2] - numpy.roll(s[2], 1, axis=2)) ) return ds diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 08b678e..1b185f5 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -77,16 +77,14 @@ def test_poynting_divergence(sim): args = {'dxes': sim.dxes, 'epsilon': sim.epsilon} - dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0) - u_eprev = None for ii in range(1, 8): u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) - delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) + delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) du_half_h2e = u_estep - u_hstep - delta_j_B - div_s_h2e = sim.dt * fdtd.poynting_divergence(e=sim.es[ii], h=sim.hs[ii], dxes=sim.dxes) * dV + div_s_h2e = sim.dt * fdtd.poynting_divergence(e=sim.es[ii], h=sim.hs[ii], dxes=sim.dxes) assert_fields_close(du_half_h2e, -div_s_h2e) if u_eprev is None: @@ -97,7 +95,7 @@ def test_poynting_divergence(sim): delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii-1], dxes=sim.dxes) du_half_e2h = u_hstep - u_eprev - delta_j_A - div_s_e2h = sim.dt * fdtd.poynting_divergence(e=sim.es[ii-1], h=sim.hs[ii], dxes=sim.dxes) * dV + div_s_e2h = sim.dt * fdtd.poynting_divergence(e=sim.es[ii-1], h=sim.hs[ii], dxes=sim.dxes) assert_fields_close(du_half_e2h, -div_s_e2h) u_eprev = u_estep @@ -123,10 +121,7 @@ def test_poynting_planes(sim): u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) - s_h2e = -fdtd.poynting(e=sim.es[ii], h=sim.hs[ii]) * sim.dt - s_h2e[0] *= sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] - s_h2e[1] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][2][None, None, :] - s_h2e[2] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] + s_h2e = -fdtd.poynting(e=sim.es[ii], h=sim.hs[ii], dxes=sim.dxes) * sim.dt planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), s_h2e[py].sum(), -s_h2e[my].sum(), s_h2e[pz].sum(), -s_h2e[mz].sum()] @@ -135,10 +130,7 @@ def test_poynting_planes(sim): u_eprev = u_estep continue - s_e2h = -fdtd.poynting(e=sim.es[ii - 1], h=sim.hs[ii]) * sim.dt - s_e2h[0] *= sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] - s_e2h[1] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][2][None, None, :] - s_e2h[2] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] + s_e2h = -fdtd.poynting(e=sim.es[ii - 1], h=sim.hs[ii], dxes=sim.dxes) * sim.dt planes = [s_e2h[px].sum(), -s_e2h[mx].sum(), s_e2h[py].sum(), -s_e2h[my].sum(), s_e2h[pz].sum(), -s_e2h[mz].sum()] From 589d658853e2d7b74e5555d5433c53f371d75727 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 12:46:12 -0700 Subject: [PATCH 163/437] prettyify example plots --- examples/fdfd.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/fdfd.py b/examples/fdfd.py index 6c8577c..aa684e8 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -188,25 +188,29 @@ def test1(solver=generic_solver): pcolor(numpy.real(E[1][center[0], :, :]).T) pyplot.subplot(2, 2, 2) pyplot.plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10)) + pyplot.grid(alpha=0.6) + pyplot.ylabel('log10 of field') pyplot.subplot(2, 2, 3) pcolor(numpy.real(E[1][:, :, center[2]]).T) pyplot.subplot(2, 2, 4) def poyntings(E): H = functional.e2h(omega, dxes)(E) - poynting = 0.5 * fdtd.poynting(e=E, h=H.conj()) * dx * dx + poynting = fdtd.poynting(e=E, h=H.conj(), dxes=dxes) cross1 = operators.poynting_e_cross(vec(E), dxes) @ vec(H).conj() cross2 = operators.poynting_h_cross(vec(H), dxes) @ vec(E).conj() * -1 - s1 = unvec(0.5 * numpy.real(cross1), grid.shape) - s2 = unvec(0.5 * numpy.real(cross2), grid.shape) - s0 = poynting.real + s1 = 0.5 * unvec(numpy.real(cross1), grid.shape) + s2 = 0.5 * unvec(numpy.real(cross2), grid.shape) + s0 = 0.5 * poynting.real # s2 = poynting.imag return s0, s1, s2 s0x, s1x, s2x = poyntings(E) - pyplot.plot(s0x[0].sum(axis=2).sum(axis=1), label='s0') - pyplot.plot(s1x[0].sum(axis=2).sum(axis=1), label='s1') - pyplot.plot(s2x[0].sum(axis=2).sum(axis=1), label='s2') + pyplot.plot(s0x[0].sum(axis=2).sum(axis=1), label='s0', marker='.') + pyplot.plot(s1x[0].sum(axis=2).sum(axis=1), label='s1', marker='.') + pyplot.plot(s2x[0].sum(axis=2).sum(axis=1), label='s2', marker='.') + pyplot.plot(E[1][:, center[1], center[2]].real.T, label='Ey', marker='x') + pyplot.grid(alpha=0.6) pyplot.legend() pyplot.show() @@ -215,7 +219,8 @@ def test1(solver=generic_solver): e_ovl_rolled = numpy.roll(e_overlap, i, axis=1) q += [numpy.abs(vec(E) @ vec(e_ovl_rolled).conj())] pyplot.figure() - pyplot.plot(q) + pyplot.plot(q, marker='.') + pyplot.grid(alpha=0.6) pyplot.title('Overlap with mode') pyplot.show() print('Average overlap with mode:', sum(q)/len(q)) @@ -227,6 +232,7 @@ def module_available(name): if __name__ == '__main__': #test0() +# test1() if module_available('opencl_fdfd'): from opencl_fdfd import cg_solver as opencl_solver From 9f207f76e421c04e65646d91fc2224929e4e5c05 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 12:46:46 -0700 Subject: [PATCH 164/437] delete extra lines --- meanas/fdfd/waveguide_mode.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 0838188..4318cc0 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -305,5 +305,3 @@ def expand_wgmode_e(E: field_t, E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in] return E_expanded - - From 7d36be96859182e360a22940dd371ef2ca872737 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 12:47:38 -0700 Subject: [PATCH 165/437] remove TODO --- meanas/fdtd/energy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index ffbbf80..7dbf363 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -22,7 +22,7 @@ def poynting(e, h, dxes=None): return s -def poynting_divergence(s=None, *, e=None, h=None, dxes=None): # TODO dxes +def poynting_divergence(s=None, *, e=None, h=None, dxes=None): if s is None: s = poynting(e, h, dxes=dxes) From 9f4a515eca76b92f741ba8b8c343f50c2288d97b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 15:44:28 -0700 Subject: [PATCH 166/437] import fdfd submodules --- meanas/fdfd/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meanas/fdfd/__init__.py b/meanas/fdfd/__init__.py index e69de29..80ba55e 100644 --- a/meanas/fdfd/__init__.py +++ b/meanas/fdfd/__init__.py @@ -0,0 +1,2 @@ +from . import solvers, operators, functional, scpml, waveguide, waveguide_mode +# from . import farfield, bloch TODO From 8fc96c13abb52f497755815677168790a7dfc72f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 16:12:30 -0700 Subject: [PATCH 167/437] test updates - move generalizable fixtures out into conftest.py - move some other functions out to utils - fix test_poynting_planes() for fdtd --- meanas/test/conftest.py | 92 +++++++++++++++++++++++++ meanas/test/test_fdtd.py | 145 ++++++++------------------------------- meanas/test/utils.py | 11 +++ 3 files changed, 131 insertions(+), 117 deletions(-) create mode 100644 meanas/test/conftest.py create mode 100644 meanas/test/utils.py diff --git a/meanas/test/conftest.py b/meanas/test/conftest.py new file mode 100644 index 0000000..c2adeb9 --- /dev/null +++ b/meanas/test/conftest.py @@ -0,0 +1,92 @@ +from typing import List, Tuple +import numpy +import pytest + + +PRNG = numpy.random.RandomState(12345) + + +##################################### +# Test fixtures +##################################### + +@pytest.fixture(scope='module', + params=[(5, 5, 1), + (5, 1, 5), + (5, 5, 5), + #(7, 7, 7), + ]) +def shape(request): + yield (3, *request.param) + + +@pytest.fixture(scope='module', params=[1.0, 1.5]) +def epsilon_bg(request): + yield request.param + + +@pytest.fixture(scope='module', params=[1.0, 2.5]) +def epsilon_fg(request): + yield request.param + + +@pytest.fixture(scope='module', params=['center', '000', 'random']) +def epsilon(request, shape, epsilon_bg, epsilon_fg): + is3d = (numpy.array(shape) == 1).sum() == 0 + if is3d: + if request.param == '000': + pytest.skip('Skipping 000 epsilon because test is 3D (for speed)') + if epsilon_bg != 1: + pytest.skip('Skipping epsilon_bg != 1 because test is 3D (for speed)') + if epsilon_fg not in (1.0, 2.0): + pytest.skip('Skipping epsilon_fg not in (1, 2) because test is 3D (for speed)') + + epsilon = numpy.full(shape, epsilon_bg, dtype=float) + if request.param == 'center': + epsilon[:, shape[1]//2, shape[2]//2, shape[3]//2] = epsilon_fg + elif request.param == '000': + epsilon[:, 0, 0, 0] = epsilon_fg + elif request.param == 'random': + epsilon[:] = PRNG.uniform(low=min(epsilon_bg, epsilon_fg), + high=max(epsilon_bg, epsilon_fg), + size=shape) + + yield epsilon + + +@pytest.fixture(scope='module', params=[1.0])#, 1.5]) +def j_mag(request): + yield request.param + + +@pytest.fixture(scope='module', params=['center', 'random']) +def j_distribution(request, shape, j_mag): + j = numpy.zeros(shape) + if request.param == 'center': + j[:, shape[1]//2, shape[2]//2, shape[3]//2] = j_mag + elif request.param == '000': + j[:, 0, 0, 0] = j_mag + elif request.param == 'random': + j[:] = PRNG.uniform(low=-j_mag, high=j_mag, size=shape) + yield j + + +@pytest.fixture(scope='module', params=[1.0, 1.5]) +def dx(request): + yield request.param + + +@pytest.fixture(scope='module', params=['uniform']) +def dxes(request, shape, dx): + if request.param == 'uniform': + dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] + yield dxes + + +@pytest.fixture(scope='module', + params=[(0, 4, 8), + #(0,), + ] + ) +def j_steps(request): + yield request.param diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 1b185f5..2fdcbea 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -1,21 +1,11 @@ -import numpy -import pytest -import dataclasses from typing import List, Tuple +import dataclasses +import pytest +import numpy from numpy.testing import assert_allclose, assert_array_equal -from meanas import fdtd - - -prng = numpy.random.RandomState(12345) - - -def assert_fields_close(a, b, *args, **kwargs): - numpy.testing.assert_allclose(a, b, verbose=False, err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(a, -1), - numpy.rollaxis(b, -1)), *args, **kwargs) - -def assert_close(a, b, *args, **kwargs): - numpy.testing.assert_allclose(a, b, *args, **kwargs) +from .. import fdtd +from .utils import assert_close, assert_fields_close def test_initial_fields(sim): @@ -101,40 +91,43 @@ def test_poynting_divergence(sim): def test_poynting_planes(sim): - mask = (sim.js[0] != 0) + mask = (sim.js[0] != 0).any(axis=0) if mask.sum() > 1: - pytest.skip('test_poynting_planes can only test single point sources') + pytest.skip('test_poynting_planes can only test single point sources, got {}'.format(mask.sum())) args = {'dxes': sim.dxes, 'epsilon': sim.epsilon} - dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0) - mx = numpy.roll(mask, (-1, -1), axis=(0, 1)) - my = numpy.roll(mask, -1, axis=2) - mz = numpy.roll(mask, (+1, -1), axis=(0, 3)) - px = numpy.roll(mask, -1, axis=0) - py = mask.copy() - pz = numpy.roll(mask, +1, axis=0) + mx = numpy.roll(mask, -1, axis=0) + my = numpy.roll(mask, -1, axis=1) + mz = numpy.roll(mask, -1, axis=2) u_eprev = None for ii in range(1, 8): u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) + du_half_h2e = u_estep - u_hstep - delta_j_B s_h2e = -fdtd.poynting(e=sim.es[ii], h=sim.hs[ii], dxes=sim.dxes) * sim.dt - planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), - s_h2e[py].sum(), -s_h2e[my].sum(), - s_h2e[pz].sum(), -s_h2e[mz].sum()] - assert_close(sum(planes), (u_estep - u_hstep).sum()) + planes = [s_h2e[0, mask].sum(), -s_h2e[0, mx].sum(), + s_h2e[1, mask].sum(), -s_h2e[1, my].sum(), + s_h2e[2, mask].sum(), -s_h2e[2, mz].sum()] + + assert_close(sum(planes), du_half_h2e[mask]) + if u_eprev is None: u_eprev = u_estep continue + delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii-1], dxes=sim.dxes) + du_half_e2h = u_hstep - u_eprev - delta_j_A + s_e2h = -fdtd.poynting(e=sim.es[ii - 1], h=sim.hs[ii], dxes=sim.dxes) * sim.dt - planes = [s_e2h[px].sum(), -s_e2h[mx].sum(), - s_e2h[py].sum(), -s_e2h[my].sum(), - s_e2h[pz].sum(), -s_e2h[mz].sum()] - assert_close(sum(planes), (u_hstep - u_eprev).sum()) + planes = [s_e2h[0, mask].sum(), -s_e2h[0, mx].sum(), + s_e2h[1, mask].sum(), -s_e2h[1, my].sum(), + s_e2h[2, mask].sum(), -s_e2h[2, mz].sum()] + assert_close(sum(planes), du_half_e2h[mask]) # previous half-step u_eprev = u_estep @@ -143,94 +136,14 @@ def test_poynting_planes(sim): ##################################### # Test fixtures ##################################### - -@pytest.fixture(scope='module', - params=[(5, 5, 1), - (5, 1, 5), - (5, 5, 5), -# (7, 7, 7), - ]) -def shape(request): - yield (3, *request.param) +# Also see conftest.py -@pytest.fixture(scope='module', params=[0.3]) +@pytest.fixture(params=[0.3]) def dt(request): yield request.param -@pytest.fixture(scope='module', params=[1.0, 1.5]) -def epsilon_bg(request): - yield request.param - - -@pytest.fixture(scope='module', params=[1.0, 2.5]) -def epsilon_fg(request): - yield request.param - - -@pytest.fixture(scope='module', params=['center', '000', 'random']) -def epsilon(request, shape, epsilon_bg, epsilon_fg): - is3d = (numpy.array(shape) == 1).sum() == 0 - if is3d: - if request.param == '000': - pytest.skip('Skipping 000 epsilon because test is 3D (for speed)') - if epsilon_bg != 1: - pytest.skip('Skipping epsilon_bg != 1 because test is 3D (for speed)') - if epsilon_fg not in (1.0, 2.0): - pytest.skip('Skipping epsilon_fg not in (1, 2) because test is 3D (for speed)') - - epsilon = numpy.full(shape, epsilon_bg, dtype=float) - if request.param == 'center': - epsilon[:, shape[1]//2, shape[2]//2, shape[3]//2] = epsilon_fg - elif request.param == '000': - epsilon[:, 0, 0, 0] = epsilon_fg - elif request.param == 'random': - epsilon[:] = prng.uniform(low=min(epsilon_bg, epsilon_fg), - high=max(epsilon_bg, epsilon_fg), - size=shape) - - yield epsilon - - -@pytest.fixture(scope='module', params=[1.0])#, 1.5]) -def j_mag(request): - yield request.param - - -@pytest.fixture(scope='module', params=['center', 'random']) -def j_distribution(request, shape, j_mag): - j = numpy.zeros(shape) - if request.param == 'center': - j[:, shape[1]//2, shape[2]//2, shape[3]//2] = j_mag - elif request.param == '000': - j[:, 0, 0, 0] = j_mag - elif request.param == 'random': - j[:] = prng.uniform(low=-j_mag, high=j_mag, size=shape) - yield j - - -@pytest.fixture(scope='module', params=[1.0, 1.5]) -def dx(request): - yield request.param - - -@pytest.fixture(scope='module', params=['uniform']) -def dxes(request, shape, dx): - if request.param == 'uniform': - dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] - yield dxes - - -@pytest.fixture(scope='module', - params=[(0,), - (0, 4, 8), - ] - ) -def j_steps(request): - yield request.param - - @dataclasses.dataclass() class SimResult: shape: Tuple[int] @@ -244,7 +157,7 @@ class SimResult: js: List[numpy.ndarray] = dataclasses.field(default_factory=list) -@pytest.fixture(scope='module') +@pytest.fixture() def sim(request, shape, epsilon, dxes, dt, j_distribution, j_steps): is3d = (numpy.array(shape) == 1).sum() == 0 if is3d: @@ -281,5 +194,3 @@ def sim(request, shape, epsilon, dxes, dt, j_distribution, j_steps): sim.es.append(e) sim.hs.append(h) return sim - - diff --git a/meanas/test/utils.py b/meanas/test/utils.py new file mode 100644 index 0000000..53c25e6 --- /dev/null +++ b/meanas/test/utils.py @@ -0,0 +1,11 @@ +import numpy + + +def assert_fields_close(x, y, *args, **kwargs): + numpy.testing.assert_allclose(x, y, verbose=False, + err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(x, -1), + numpy.rollaxis(y, -1)), *args, **kwargs) + +def assert_close(x, y, *args, **kwargs): + numpy.testing.assert_allclose(x, y, *args, **kwargs) + From 061dbf59d163f7089ad76c30adccba3dc81f1e35 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 16:12:48 -0700 Subject: [PATCH 168/437] formatting fixes --- meanas/fdfd/functional.py | 1 - meanas/fdfd/waveguide.py | 8 ++++---- meanas/fdtd/boundaries.py | 7 ++----- meanas/fdtd/pml.py | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index 326f38b..bd31192 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -219,4 +219,3 @@ def poynting_e_cross_h(dxes: dx_lists_t): s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx return s return exh - diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py index 14e3502..c24a471 100644 --- a/meanas/fdfd/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -32,10 +32,10 @@ __author__ = 'Jan Petykiewicz' def operator_e(omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - ) -> sparse.spmatrix: + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + ) -> sparse.spmatrix: if numpy.any(numpy.equal(mu, None)): mu = numpy.ones_like(epsilon) diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py index 34a8d4a..cba1797 100644 --- a/meanas/fdtd/boundaries.py +++ b/meanas/fdtd/boundaries.py @@ -37,7 +37,7 @@ def conducting_boundary(direction: int, return en, hn - elif polarity > 0: + if polarity > 0: boundary_slice = [slice(None)] * 3 shifted1_slice = [slice(None)] * 3 shifted2_slice = [slice(None)] * 3 @@ -62,7 +62,4 @@ def conducting_boundary(direction: int, return ep, hp - else: - raise Exception('Bad polarity: {}'.format(polarity)) - - + raise Exception('Bad polarity: {}'.format(polarity)) diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py index 3e10aa6..7d73e38 100644 --- a/meanas/fdtd/pml.py +++ b/meanas/fdtd/pml.py @@ -13,7 +13,7 @@ from .. import dx_lists_t, field_t, field_updater __author__ = 'Jan Petykiewicz' -def cpml(direction:int, +def cpml(direction: int, polarity: int, dt: float, epsilon: field_t, From b0b295d1ac6a23c0d98d0f238863327c3a63990d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 16:13:25 -0700 Subject: [PATCH 169/437] add test_fdfd --- meanas/test/test_fdfd.py | 90 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 meanas/test/test_fdfd.py diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py new file mode 100644 index 0000000..05f710d --- /dev/null +++ b/meanas/test/test_fdfd.py @@ -0,0 +1,90 @@ +from typing import List, Tuple +import dataclasses +import pytest +import numpy +from numpy.testing import assert_allclose, assert_array_equal + +from .. import fdfd, vec, unvec +from .utils import assert_close, assert_fields_close + + +def test_poynting_planes(sim): + mask = (sim.j != 0).any(axis=0) + if mask.sum() > 1: + pytest.skip(f'test_poynting_planes can only test single point sources, got {mask.sum()}') + + mx = numpy.roll(mask, -1, axis=0) + my = numpy.roll(mask, -1, axis=1) + mz = numpy.roll(mask, -1, axis=2) + + e2h = fdfd.operators.e2h(omega=sim.omega, dxes=sim.dxes, pmc=sim.pmc) + ev = vec(sim.e) + hv = e2h @ ev + + exh = fdfd.operators.poynting_e_cross(e=ev, dxes=sim.dxes) @ hv.conj() + s = unvec(exh.real / 2, sim.shape[1:]) + planes = [s[0, mask].sum(), -s[0, mx].sum(), + s[0, mask].sum(), -s[1, my].sum(), + s[0, mask].sum(), -s[2, mz].sum()] + + e_dot_j = sim.e * sim.j + src_energy = -e_dot_j.real / 2 + + assert_close(sum(planes), (src_energy).sum()) + + +##################################### +# Test fixtures +##################################### +# Also see conftest.py + +@pytest.fixture(params=[1/1500]) +def omega(request): + yield request.param + + +@pytest.fixture(params=[None]) +def pec(request): + yield request.param + + +@pytest.fixture(params=[None]) +def pmc(request): + yield request.param + + +@dataclasses.dataclass() +class SimResult: + shape: Tuple[int] + dxes: List[List[numpy.ndarray]] + epsilon: numpy.ndarray + omega: complex + j: numpy.ndarray + e: numpy.ndarray + pmc: numpy.ndarray + pec: numpy.ndarray + +@pytest.fixture() +def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): +# is3d = (numpy.array(shape) == 1).sum() == 0 +# if is3d: +# pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)') + + j_vec = vec(j_distribution) + eps_vec = vec(epsilon) + e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec) + e = unvec(e_vec, shape[1:]) + + sim = SimResult( + shape=shape, + dxes=dxes, + epsilon=epsilon, + j=j_distribution, + e=e, + pec=pec, + pmc=pmc, + omega=omega, + ) + + return sim + From 7da80c3e64c1d17bd8e99ecd93330248e82298fe Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 16:16:43 -0700 Subject: [PATCH 170/437] more args to pytest --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 04aeae7..a3f6993 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ source venv/bin/activate pip3 install --user -e './meanas[test,examples]' # Run tests -python3 -m pytest +python3 -m pytest -rsxX | tee test_results.txt ``` #### See also: From 60961db2d3a115458411b3a42df60b9f6a4e0f2b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 27 Oct 2019 16:17:15 -0700 Subject: [PATCH 171/437] add atol to shut up solver --- meanas/test/test_fdfd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 05f710d..a00c5e9 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -72,7 +72,7 @@ def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): j_vec = vec(j_distribution) eps_vec = vec(epsilon) - e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec) + e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec, matrix_solver_opts={'atol': 1e-12}) e = unvec(e_vec, shape[1:]) sim = SimResult( From 9253200eaf4591126675253f60510a3e09432f4e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 4 Nov 2019 20:27:22 -0800 Subject: [PATCH 172/437] Move j_steps and j_distribution out of conftest - also move PRNG back into utils --- meanas/test/conftest.py | 24 +----------------------- meanas/test/test_fdfd.py | 17 ++++++++++++++++- meanas/test/test_fdtd.py | 19 ++++++++++++++++++- meanas/test/utils.py | 1 + 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/meanas/test/conftest.py b/meanas/test/conftest.py index c2adeb9..d25103b 100644 --- a/meanas/test/conftest.py +++ b/meanas/test/conftest.py @@ -2,9 +2,7 @@ from typing import List, Tuple import numpy import pytest - -PRNG = numpy.random.RandomState(12345) - +from .utils import PRNG ##################################### # Test fixtures @@ -59,18 +57,6 @@ def j_mag(request): yield request.param -@pytest.fixture(scope='module', params=['center', 'random']) -def j_distribution(request, shape, j_mag): - j = numpy.zeros(shape) - if request.param == 'center': - j[:, shape[1]//2, shape[2]//2, shape[3]//2] = j_mag - elif request.param == '000': - j[:, 0, 0, 0] = j_mag - elif request.param == 'random': - j[:] = PRNG.uniform(low=-j_mag, high=j_mag, size=shape) - yield j - - @pytest.fixture(scope='module', params=[1.0, 1.5]) def dx(request): yield request.param @@ -82,11 +68,3 @@ def dxes(request, shape, dx): dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] yield dxes - -@pytest.fixture(scope='module', - params=[(0, 4, 8), - #(0,), - ] - ) -def j_steps(request): - yield request.param diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index a00c5e9..8325d63 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -5,7 +5,7 @@ import numpy from numpy.testing import assert_allclose, assert_array_equal from .. import fdfd, vec, unvec -from .utils import assert_close, assert_fields_close +from .utils import assert_close, assert_fields_close, PRNG def test_poynting_planes(sim): @@ -53,6 +53,20 @@ def pmc(request): yield request.param +@pytest.fixture(params=['center', 'diag']) +def j_distribution(request, shape, j_mag): + j = numpy.zeros(shape, dtype=complex) + center_mask = numpy.zeros(shape, dtype=bool) + center_mask[:, shape[1]//2, shape[2]//2, shape[3]//2] = True + + if request.param == 'center': + j[center_mask] = j_mag + elif request.param == 'diag': + j[numpy.roll(center_mask, [1, 1, 1], axis=(1, 2, 3))] = j_mag + j[numpy.roll(center_mask, [-1, -1, -1], axis=(1, 2, 3))] = -1j * j_mag + yield j + + @dataclasses.dataclass() class SimResult: shape: Tuple[int] @@ -64,6 +78,7 @@ class SimResult: pmc: numpy.ndarray pec: numpy.ndarray + @pytest.fixture() def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): # is3d = (numpy.array(shape) == 1).sum() == 0 diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 2fdcbea..4861f25 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -5,7 +5,7 @@ import numpy from numpy.testing import assert_allclose, assert_array_equal from .. import fdtd -from .utils import assert_close, assert_fields_close +from .utils import assert_close, assert_fields_close, PRNG def test_initial_fields(sim): @@ -157,6 +157,23 @@ class SimResult: js: List[numpy.ndarray] = dataclasses.field(default_factory=list) +@pytest.fixture(params=[(0, 4, 8),]) #(0,)]) +def j_steps(request): + yield request.param + + +@pytest.fixture(params=['center', 'random']) +def j_distribution(request, shape, j_mag): + j = numpy.zeros(shape) + if request.param == 'center': + j[:, shape[1]//2, shape[2]//2, shape[3]//2] = j_mag + elif request.param == '000': + j[:, 0, 0, 0] = j_mag + elif request.param == 'random': + j[:] = PRNG.uniform(low=-j_mag, high=j_mag, size=shape) + yield j + + @pytest.fixture() def sim(request, shape, epsilon, dxes, dt, j_distribution, j_steps): is3d = (numpy.array(shape) == 1).sum() == 0 diff --git a/meanas/test/utils.py b/meanas/test/utils.py index 53c25e6..ac657a1 100644 --- a/meanas/test/utils.py +++ b/meanas/test/utils.py @@ -1,5 +1,6 @@ import numpy +PRNG = numpy.random.RandomState(12345) def assert_fields_close(x, y, *args, **kwargs): numpy.testing.assert_allclose(x, y, verbose=False, From 0bb4105e7aa23423cacb236a8e3b64142c76c0cd Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 4 Nov 2019 20:30:07 -0800 Subject: [PATCH 173/437] e dot j should take into account dxes --- meanas/test/test_fdfd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 8325d63..31e3553 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -27,7 +27,7 @@ def test_poynting_planes(sim): s[0, mask].sum(), -s[1, my].sum(), s[0, mask].sum(), -s[2, mz].sum()] - e_dot_j = sim.e * sim.j + e_dot_j = sim.e * sim.j * sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] src_energy = -e_dot_j.real / 2 assert_close(sum(planes), (src_energy).sum()) From 68f857e83e9cb195c5b7afdb45d1a7050fb4ecfc Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 4 Nov 2019 20:30:45 -0800 Subject: [PATCH 174/437] Use 2-point sources to test fdfd poynting --- meanas/test/test_fdfd.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 31e3553..ffd69ea 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -10,8 +10,10 @@ from .utils import assert_close, assert_fields_close, PRNG def test_poynting_planes(sim): mask = (sim.j != 0).any(axis=0) - if mask.sum() > 1: - pytest.skip(f'test_poynting_planes can only test single point sources, got {mask.sum()}') + if mask.sum() != 2: + pytest.skip(f'test_poynting_planes will only test 2-point sources, got {mask.sum()}') + points = numpy.where(mask) + mask[points[0][0], points[1][0], points[2][0]] = 0 mx = numpy.roll(mask, -1, axis=0) my = numpy.roll(mask, -1, axis=1) From ca70d6e6c8d1f703a2efb24d15c76c388a81ff62 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 4 Nov 2019 20:31:02 -0800 Subject: [PATCH 175/437] Sample all three S components... --- meanas/test/test_fdfd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index ffd69ea..8397a45 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -26,8 +26,8 @@ def test_poynting_planes(sim): exh = fdfd.operators.poynting_e_cross(e=ev, dxes=sim.dxes) @ hv.conj() s = unvec(exh.real / 2, sim.shape[1:]) planes = [s[0, mask].sum(), -s[0, mx].sum(), - s[0, mask].sum(), -s[1, my].sum(), - s[0, mask].sum(), -s[2, mz].sum()] + s[1, mask].sum(), -s[1, my].sum(), + s[2, mask].sum(), -s[2, mz].sum()] e_dot_j = sim.e * sim.j * sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] src_energy = -e_dot_j.real / 2 From 8794cbd7c232d481bec4a352cd901028783d0ba3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 4 Nov 2019 20:48:14 -0800 Subject: [PATCH 176/437] e dot j should be masked as well --- meanas/test/test_fdfd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 8397a45..676848a 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -30,7 +30,7 @@ def test_poynting_planes(sim): s[2, mask].sum(), -s[2, mz].sum()] e_dot_j = sim.e * sim.j * sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] - src_energy = -e_dot_j.real / 2 + src_energy = -e_dot_j[:, mask].real / 2 assert_close(sum(planes), (src_energy).sum()) From 2300a8b47e51cdd0772c178e9d9c6fc3a772f10f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 4 Nov 2019 20:49:04 -0800 Subject: [PATCH 177/437] poynting equality needs bigger tolerance not sure why yet --- meanas/test/test_fdfd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 676848a..d126cd7 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -32,7 +32,7 @@ def test_poynting_planes(sim): e_dot_j = sim.e * sim.j * sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] src_energy = -e_dot_j[:, mask].real / 2 - assert_close(sum(planes), (src_energy).sum()) + assert_close(sum(planes), src_energy.sum(), rtol=2e-5) # TODO why is the tolerance so bad? ##################################### From 507417b90e3f38eaeb1edd4e63e8692ef05ba871 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 5 Nov 2019 19:02:40 -0800 Subject: [PATCH 178/437] add missing cd --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a3f6993..79490e0 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ source venv/bin/activate pip3 install --user -e './meanas[test,examples]' # Run tests +cd meanas python3 -m pytest -rsxX | tee test_results.txt ``` From 31e8dde8ce1ec3d9da3b31b5a7afedc13de4d096 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 12 Nov 2019 00:57:26 -0800 Subject: [PATCH 179/437] Add type hints --- meanas/fdtd/energy.py | 59 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index 7dbf363..9f5c99b 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -4,7 +4,10 @@ import numpy from .. import dx_lists_t, field_t, field_updater -def poynting(e, h, dxes=None): +def poynting(e: field_t, + h: field_t, + dxes: dx_lists_t = None, + ) -> field_t: if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) @@ -22,27 +25,52 @@ def poynting(e, h, dxes=None): return s -def poynting_divergence(s=None, *, e=None, h=None, dxes=None): +def poynting_divergence(s: field_t = None, + *, + e: field_t = None, + h: field_t = None, + dxes: dx_lists_t = None, + ) -> field_t: if s is None: s = poynting(e, h, dxes=dxes) ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) + (s[1] - numpy.roll(s[1], 1, axis=1)) + - (s[2] - numpy.roll(s[2], 1, axis=2)) ) + (s[2] - numpy.roll(s[2], 1, axis=2))) return ds -def energy_hstep(e0, h1, e2, epsilon=None, mu=None, dxes=None): +def energy_hstep(e0: field_t, + h1: field_t, + e2: field_t, + epsilon: field_t = None, + mu: field_t = None, + dxes: dx_lists_t = None, + ) -> field_t: u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) return u -def energy_estep(h0, e1, h2, epsilon=None, mu=None, dxes=None): +def energy_estep(h0: field_t, + e1: field_t, + h2: field_t, + epsilon: field_t = None, + mu: field_t = None, + dxes: dx_lists_t = None, + ) -> field_t: u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) return u -def delta_energy_h2e(dt, e0, h1, e2, h3, epsilon=None, mu=None, dxes=None): +def delta_energy_h2e(dt: float, + e0: field_t, + h1: field_t, + e2: field_t, + h3: field_t, + epsilon: field_t = None, + mu: field_t = None, + dxes: dx_lists_t = None, + ) -> field_t: """ This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) """ @@ -52,7 +80,15 @@ def delta_energy_h2e(dt, e0, h1, e2, h3, epsilon=None, mu=None, dxes=None): return du -def delta_energy_e2h(dt, h0, e1, h2, e3, epsilon=None, mu=None, dxes=None): +def delta_energy_e2h(dt: float, + h0: field_t, + e1: field_t, + h2: field_t, + e3: field_t, + epsilon: field_t = None, + mu: field_t = None, + dxes: dx_lists_t = None, + ) -> field_t: """ This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) """ @@ -62,7 +98,7 @@ def delta_energy_e2h(dt, h0, e1, h2, e3, epsilon=None, mu=None, dxes=None): return du -def delta_energy_j(j0, e1, dxes=None): +def delta_energy_j(j0: field_t, e1: field_t, dxes: dx_lists_t = None) -> field_t: if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) @@ -73,7 +109,12 @@ def delta_energy_j(j0, e1, dxes=None): return du -def dxmul(ee, hh, epsilon=None, mu=None, dxes=None): +def dxmul(ee: field_t, + hh: field_t, + epsilon: field_t = None, + mu: field_t = None, + dxes: dx_lists_t = None + ) -> field_t: if epsilon is None: epsilon = 1 if mu is None: From d16de3802fe1975ce115f7b026e77779cfe44821 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 12 Nov 2019 00:57:36 -0800 Subject: [PATCH 180/437] work around pylint bug --- meanas/fdtd/energy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index 9f5c99b..8644646 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -1,3 +1,4 @@ +# pylint: disable=unsupported-assignment-operation from typing import List, Callable, Tuple, Dict import numpy From 462a8c6dbc6ad25b09e92b21221264ab956b01ec Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 12 Nov 2019 01:09:26 -0800 Subject: [PATCH 181/437] add some nonuniform dxes 'random' causes some tests to fail, haven't thought about why yet --- meanas/test/conftest.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/meanas/test/conftest.py b/meanas/test/conftest.py index d25103b..2522e0c 100644 --- a/meanas/test/conftest.py +++ b/meanas/test/conftest.py @@ -62,9 +62,18 @@ def dx(request): yield request.param -@pytest.fixture(scope='module', params=['uniform']) +@pytest.fixture(scope='module', params=['uniform', 'centerbig']) def dxes(request, shape, dx): if request.param == 'uniform': dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] + elif request.param == 'centerbig': + dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] + for eh in (0, 1): + for ax in (0, 1, 2): + dxes[eh][ax][dxes[eh][ax].size // 2] *= 1.1 + elif request.param == 'random': + dxe = [PRNG.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]] + dxh = [(d + numpy.roll(d, -1)) / 2 for d in dxe] + dxes = [dxe, dxh] yield dxes From 0fd6df441e8da8af768b16963def4ae9817de946 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 12 Nov 2019 01:09:42 -0800 Subject: [PATCH 182/437] linter options --- meanas/test/test_fdtd.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 4861f25..533064a 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -1,3 +1,4 @@ +# pylint: disable=redefined-outer-name, no-member from typing import List, Tuple import dataclasses import pytest @@ -52,10 +53,10 @@ def test_energy_conservation(sim): 'epsilon': sim.epsilon} for ii in range(1, 8): - u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) - u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) # pylint: disable=bad-whitespace + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) # pylint: disable=bad-whitespace delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii-1], dxes=sim.dxes) - delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) + delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) # pylint: disable=bad-whitespace u += delta_j_A.sum() assert_close(u_hstep.sum(), u) @@ -69,8 +70,8 @@ def test_poynting_divergence(sim): u_eprev = None for ii in range(1, 8): - u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) - u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) # pylint: disable=bad-whitespace + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) # pylint: disable=bad-whitespace delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) du_half_h2e = u_estep - u_hstep - delta_j_B @@ -104,8 +105,8 @@ def test_poynting_planes(sim): u_eprev = None for ii in range(1, 8): - u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) - u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) # pylint: disable=bad-whitespace + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) # pylint: disable=bad-whitespace delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) du_half_h2e = u_estep - u_hstep - delta_j_B From b24b5d169f3980d7b5768e461dd75e29fb5d3be6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 12 Nov 2019 01:10:01 -0800 Subject: [PATCH 183/437] Add residual testing --- meanas/test/test_fdfd.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index d126cd7..e9f3a21 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -8,6 +8,13 @@ from .. import fdfd, vec, unvec from .utils import assert_close, assert_fields_close, PRNG +def test_residual(sim): + A = fdfd.operators.e_full(sim.omega, sim.dxes, vec(sim.epsilon)).tocsr() + b = -1j * sim.omega * vec(sim.j) + residual = A @ vec(sim.e) - b + assert numpy.linalg.norm(residual) < 1e-10 + + def test_poynting_planes(sim): mask = (sim.j != 0).any(axis=0) if mask.sum() != 2: From d4bb3e83e1fa52cd14f99252daeb524468bfacdd Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 12 Nov 2019 01:10:21 -0800 Subject: [PATCH 184/437] improve solution accuracy --- meanas/test/test_fdfd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index e9f3a21..533ef91 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -96,7 +96,8 @@ def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): j_vec = vec(j_distribution) eps_vec = vec(epsilon) - e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec, matrix_solver_opts={'atol': 1e-12}) + e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec, + matrix_solver_opts={'atol': 1e-15, 'tol': 1e-10}) e = unvec(e_vec, shape[1:]) sim = SimResult( From 318f361f6123c3e1813ff86bffcb203bcd909b3b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 12 Nov 2019 01:10:58 -0800 Subject: [PATCH 185/437] some cleanup --- meanas/test/test_fdfd.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 533ef91..43eba25 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -1,11 +1,12 @@ +# pylint: disable=redefined-outer-name from typing import List, Tuple import dataclasses import pytest import numpy -from numpy.testing import assert_allclose, assert_array_equal +#from numpy.testing import assert_allclose, assert_array_equal from .. import fdfd, vec, unvec -from .utils import assert_close, assert_fields_close, PRNG +from .utils import assert_close, assert_fields_close def test_residual(sim): @@ -39,7 +40,8 @@ def test_poynting_planes(sim): e_dot_j = sim.e * sim.j * sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] src_energy = -e_dot_j[:, mask].real / 2 - assert_close(sum(planes), src_energy.sum(), rtol=2e-5) # TODO why is the tolerance so bad? + assert_close(sum(planes), src_energy.sum()) + ##################################### From 41be4d2ab86833fe6f087db74c0dbf176e7c5d50 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 12 Nov 2019 01:11:13 -0800 Subject: [PATCH 186/437] more cleanup --- meanas/test/test_fdfd.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 43eba25..d3c174c 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -64,7 +64,13 @@ def pmc(request): yield request.param -@pytest.fixture(params=['center', 'diag']) +#@pytest.fixture(scope='module', +# params=[(25, 5, 5)]) +#def shape(request): +# yield (3, *request.param) + + +@pytest.fixture(params=['diag']) #'center' def j_distribution(request, shape, j_mag): j = numpy.zeros(shape, dtype=complex) center_mask = numpy.zeros(shape, dtype=bool) From 0f07fd15da1f35bf372a543a7e11873a1623dc19 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 12 Nov 2019 01:11:58 -0800 Subject: [PATCH 187/437] whitespace --- meanas/test/test_fdfd.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index d3c174c..5c6acc7 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -120,4 +120,3 @@ def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): ) return sim - From 63ad67800a40873c29fd8ac1ce06bc6ae10e0359 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 21 Nov 2019 20:13:53 -0800 Subject: [PATCH 188/437] further improve solve precision --- meanas/test/test_fdfd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 5c6acc7..3347049 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -105,7 +105,7 @@ def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): j_vec = vec(j_distribution) eps_vec = vec(epsilon) e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec, - matrix_solver_opts={'atol': 1e-15, 'tol': 1e-10}) + matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11}) e = unvec(e_vec, shape[1:]) sim = SimResult( From 6f2faca7dcaaa7ad6946a2da643ece80552b0c16 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 22 Nov 2019 00:56:03 -0800 Subject: [PATCH 189/437] Ignore size-1 axes when shifting --- meanas/fdfd/operators.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 7052715..905c23e 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -531,6 +531,8 @@ def e_boundary_source(mask: vfield_t, shift = lambda axis, polarity: shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity) for axis in (0, 1, 2): + if shape[axis] == 1: + continue for polarity in (-1, +1): r = shift(axis, polarity) - sparse.eye(numpy.prod(shape)) # shifted minus original r3 = sparse.block_diag((r, r, r)) From caa6f995c79d8959f67843826b73b3313f198f26 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 22 Nov 2019 00:56:35 -0800 Subject: [PATCH 190/437] rename SimResult classes to TDResult and FDResult --- meanas/test/test_fdfd.py | 4 ++-- meanas/test/test_fdtd.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 3347049..ea6f417 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -85,7 +85,7 @@ def j_distribution(request, shape, j_mag): @dataclasses.dataclass() -class SimResult: +class FDResult: shape: Tuple[int] dxes: List[List[numpy.ndarray]] epsilon: numpy.ndarray @@ -108,7 +108,7 @@ def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11}) e = unvec(e_vec, shape[1:]) - sim = SimResult( + sim = FDResult( shape=shape, dxes=dxes, epsilon=epsilon, diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 533064a..95d6817 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -146,7 +146,7 @@ def dt(request): @dataclasses.dataclass() -class SimResult: +class TDResult: shape: Tuple[int] dt: float dxes: List[List[numpy.ndarray]] @@ -182,7 +182,7 @@ def sim(request, shape, epsilon, dxes, dt, j_distribution, j_steps): if dt != 0.3: pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)') - sim = SimResult( + sim = TDResult( shape=shape, dt=dt, dxes=dxes, From 1048dfc21c0ca3b2b5b0871998b5823a40b8c391 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 22 Nov 2019 00:56:42 -0800 Subject: [PATCH 191/437] Add test_fdfd_pml --- meanas/test/test_fdfd_pml.py | 148 +++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 meanas/test/test_fdfd_pml.py diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py new file mode 100644 index 0000000..112193d --- /dev/null +++ b/meanas/test/test_fdfd_pml.py @@ -0,0 +1,148 @@ +##################################### +# pylint: disable=redefined-outer-name +from typing import List, Tuple +import dataclasses +import pytest +import numpy +from numpy.testing import assert_allclose, assert_array_equal + +from .. import fdfd, vec, unvec +from .utils import assert_close, assert_fields_close +from .test_fdfd import FDResult + + +def test_pml(sim, src_polarity): + dim = numpy.where(numpy.array(sim.shape[1:]) > 1)[0][0] # Propagation axis + + e_sqr = numpy.squeeze((sim.e.conj() * sim.e).sum(axis=0)) + +# from matplotlib import pyplot +# pyplot.figure() +# pyplot.plot(numpy.squeeze(e_sqr)) +# pyplot.show(block=True) + + e_sqr_tgt = e_sqr[16:19] + e_sqr_rev = e_sqr[10:13] + if src_polarity < 0: + e_sqr_tgt, e_sqr_rev = e_sqr_rev, e_sqr_tgt + + assert_allclose(e_sqr_rev, 0, atol=1e-12) + assert_allclose(e_sqr_tgt, 1, rtol=3e-6) + + +# pyplot.figure() +# pyplot.plot(numpy.squeeze(sim.e[0].real), label='Ex_real') +# pyplot.plot(numpy.squeeze(sim.e[0].imag), label='Ex_imag') +# pyplot.plot(numpy.squeeze(sim.e[1].real), label='Ey_real') +# pyplot.plot(numpy.squeeze(sim.e[1].imag), label='Ey_imag') +# pyplot.plot(numpy.squeeze(sim.e[2].real), label='Ez_real') +# pyplot.plot(numpy.squeeze(sim.e[2].imag), label='Ez_imag') +# pyplot.legend() +# pyplot.show(block=True) + + +# Test fixtures +##################################### +# Also see conftest.py + +@pytest.fixture(params=[1/1500]) +def omega(request): + yield request.param + + +@pytest.fixture(params=[None]) +def pec(request): + yield request.param + + +@pytest.fixture(params=[None]) +def pmc(request): + yield request.param + + + +@pytest.fixture(params=[(30, 1, 1), + (1, 30, 1), + (1, 1, 30)]) +def shape(request): + yield (3, *request.param) + + +@pytest.fixture(params=[+1, -1]) +def src_polarity(request): + yield request.param + + +@pytest.fixture() +def j_distribution(request, shape, epsilon, dxes, omega, src_polarity): + j = numpy.zeros(shape, dtype=complex) + + dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis + other_dims = [0, 1, 2] + other_dims.remove(dim) + + dx_prop = (dxes[0][dim][shape[dim + 1] // 2] + + dxes[1][dim][shape[dim + 1] // 2]) / 2 #TODO is this right for nonuniform dxes? + + # Mask only contains components orthogonal to propagation direction + center_mask = numpy.zeros(shape, dtype=bool) + center_mask[other_dims, shape[1]//2, shape[2]//2, shape[3]//2] = True + if (epsilon[center_mask] != epsilon[center_mask][0]).any(): + center_mask[other_dims[1]] = False # If epsilon is not isotropic, pick only one dimension + + + wavenumber = omega * numpy.sqrt(epsilon[center_mask].mean()) + wavenumber_corrected = 2 / dx_prop * numpy.arcsin(wavenumber * dx_prop / 2) + + e = numpy.zeros_like(epsilon, dtype=complex) + e[center_mask] = 1 / numpy.linalg.norm(center_mask[:]) + + slices = [slice(None), slice(None), slice(None)] + slices[dim] = slice(shape[dim + 1] // 2, + shape[dim + 1] // 2 + 1) + + j = fdfd.waveguide_mode.compute_source(E=e, wavenumber=wavenumber_corrected, omega=omega, dxes=dxes, + axis=dim, polarity=src_polarity, slices=slices, epsilon=epsilon) + yield j + + +@pytest.fixture() +def epsilon(request, shape, epsilon_bg, epsilon_fg): + epsilon = numpy.full(shape, epsilon_fg, dtype=float) + yield epsilon + + +@pytest.fixture(params=['uniform']) +def dxes(request, shape, dx, omega, epsilon_fg): + if request.param == 'uniform': + dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] + dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis + for axis in (dim,): + for polarity in (-1, 1): + dxes = fdfd.scpml.stretch_with_scpml(dxes, axis=axis, polarity=polarity, + omega=omega, epsilon_effective=epsilon_fg, + thickness=10) + yield dxes + + +@pytest.fixture() +def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): + j_vec = vec(j_distribution) + eps_vec = vec(epsilon) + e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec, + matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11}) + e = unvec(e_vec, shape[1:]) + + sim = FDResult( + shape=shape, + dxes=dxes, + epsilon=epsilon, + j=j_distribution, + e=e, + pec=pec, + pmc=pmc, + omega=omega, + ) + + return sim + From 8550125744ac72e5a2c5ba9dbba8ebc3602c4ee2 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 24 Nov 2019 22:46:36 -0800 Subject: [PATCH 192/437] Readme updates --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 79490e0..d127258 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# meanas README +# meanas **meanas** is a python package for electromagnetic simulations @@ -13,6 +13,7 @@ purpose- and hardware-specific solvers. **Contents** + - Finite difference frequency domain (FDFD) * Library of sparse matrices for representing the electromagnetic wave equation in 3D, as well as auxiliary matrices for conversion between fields @@ -39,7 +40,7 @@ those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html). Your solver will need the ability to solve complex symmetric (non-Hermitian) linear systems, ideally with double precision. -- [WORKING Source repository](https://mpxd.net/code/jan/fdfd_tools/src/branch/wip) +- [WIP Source repository](https://mpxd.net/code/jan/fdfd_tools/src/branch/wip) - *TODO* [Source repository](https://mpxd.net/code/jan/meanas) - PyPI *TBD* @@ -47,6 +48,7 @@ linear systems, ideally with double precision. ## Installation **Requirements:** + * python 3 (tests require 3.7) * numpy * scipy From 3b57619c2fa491bba4a846841630ca074a14d0e4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 24 Nov 2019 22:46:47 -0800 Subject: [PATCH 193/437] Use readme as module doc --- meanas/__init__.py | 43 +++++-------------------------------------- 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/meanas/__init__.py b/meanas/__init__.py index 76388dc..8d30d47 100644 --- a/meanas/__init__.py +++ b/meanas/__init__.py @@ -1,44 +1,7 @@ """ Electromagnetic simulation tools -This package is intended for building simulation inputs, analyzing -simulation outputs, and running short simulations on unspecialized hardware. -It is designed to provide tooling and a baseline for other, high-performance -purpose- and hardware-specific solvers. - - -**Contents** -- Finite difference frequency domain (FDFD) - * Library of sparse matrices for representing the electromagnetic wave - equation in 3D, as well as auxiliary matrices for conversion between fields - * Waveguide mode operators - * Waveguide mode eigensolver - * Stretched-coordinate PML boundaries (SCPML) - * Functional versions of most operators - * Anisotropic media (limited to diagonal elements eps_xx, eps_yy, eps_zz, mu_xx, ...) - * Arbitrary distributions of perfect electric and magnetic conductors (PEC / PMC) -- Finite difference time domain (FDTD) - * Basic Maxwell time-steps - * Poynting vector and energy calculation - * Convolutional PMLs - -This package does *not* provide a fast matrix solver, though by default -```meanas.fdfd.solvers.generic(...)``` will call -```scipy.sparse.linalg.qmr(...)``` to perform a solve. -For 2D FDFD problems this should be fine; likewise, the waveguide mode -solver uses scipy's eigenvalue solver, with reasonable results. - -For solving large (or 3D) FDFD problems, I recommend a GPU-based iterative -solver, such as [opencl_fdfd](https://mpxd.net/code/jan/opencl_fdfd) or -those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html)). Your -solver will need the ability to solve complex symmetric (non-Hermitian) -linear systems, ideally with double precision. - - -Dependencies: -- numpy -- scipy - +See the readme or `import meanas; help(meanas)` for more info. """ import pathlib @@ -50,3 +13,7 @@ __author__ = 'Jan Petykiewicz' with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f: __version__ = f.read().strip() + +with open(pathlib.Path(__file__).parent.parent / 'README.md', 'r') as f: + __doc__ = f.read() + From f0ef31c25d7d01b2aa7f8c873a17c9852436c9ee Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 24 Nov 2019 22:50:03 -0800 Subject: [PATCH 194/437] enable multiple vector rayleigh_quotient_iteration --- meanas/eigensolvers.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py index d0ee541..5ca9962 100644 --- a/meanas/eigensolvers.py +++ b/meanas/eigensolvers.py @@ -34,7 +34,7 @@ def power_iteration(operator: sparse.spmatrix, def rayleigh_quotient_iteration(operator: sparse.spmatrix or spalg.LinearOperator, - guess_vector: numpy.ndarray, + guess_vectors: numpy.ndarray, iterations: int = 40, tolerance: float = 1e-13, solver=None, @@ -42,15 +42,21 @@ def rayleigh_quotient_iteration(operator: sparse.spmatrix or spalg.LinearOperato """ Use Rayleigh quotient iteration to refine an eigenvector guess. - :param operator: Matrix to analyze. - :param guess_vector: Eigenvector to refine. - :param iterations: Maximum number of iterations to perform. Default 40. - :param tolerance: Stop iteration if (A - I*eigenvalue) @ v < tolerance. - Default 1e-13. - :param solver: Solver function of the form x = solver(A, b). - By default, use scipy.sparse.spsolve for sparse matrices and - scipy.sparse.bicgstab for general LinearOperator instances. - :return: (eigenvalue, eigenvector) + TODO: + Need to test this for more than one guess_vectors. + + Args: + operator: Matrix to analyze. + guess_vectors: Eigenvectors to refine. + iterations: Maximum number of iterations to perform. Default 40. + tolerance: Stop iteration if `(A - I*eigenvalue) @ v < num_vectors * tolerance`, + Default 1e-13. + solver: Solver function of the form `x = solver(A, b)`. + By default, use scipy.sparse.spsolve for sparse matrices and + scipy.sparse.bicgstab for general LinearOperator instances. + + Returns: + (eigenvalues, eigenvectors) """ try: _test = operator - sparse.eye(operator.shape[0]) @@ -64,16 +70,16 @@ def rayleigh_quotient_iteration(operator: sparse.spmatrix or spalg.LinearOperato if solver is None: solver = lambda A, b: spalg.bicgstab(A, b)[0] - v = guess_vector + v = numpy.atleast_2d(guess_vectors) v /= norm(v) for _ in range(iterations): eigval = v.conj() @ (operator @ v) - if norm(operator @ v - eigval * v) < tolerance: + if norm(operator @ v - eigval * v) < v.shape[1] * tolerance: break shifted_operator = operator - shift(eigval) v = solver(shifted_operator, v) - v /= norm(v) + v /= norm(v, axis=0) return eigval, v @@ -99,7 +105,7 @@ def signed_eigensolve(operator: sparse.spmatrix or spalg.LinearOperator, Shift by the absolute value of the largest eigenvalue, then find a few of the largest-magnitude (shifted) eigenvalues. A positive shift ensures that we find the largest _positive_ eigenvalues, since any negative eigenvalues will be shifted to the - range 0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval) + range `0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval)` ''' shift = numpy.abs(lm_eigval) if negative: From d6e7e3dee166b17d1d28b03a82f87aa032199f5a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 24 Nov 2019 23:47:31 -0800 Subject: [PATCH 195/437] Big documentation and structure updates - Split math into fdmath package - Rename waveguide into _2d _3d and _cyl variants - pdoc-based documentation --- make_docs.sh | 3 + meanas/eigensolvers.py | 26 +- meanas/fdfd/__init__.py | 17 +- meanas/fdfd/bloch.py | 2 +- meanas/fdfd/farfield.py | 6 +- meanas/fdfd/functional.py | 192 +++++------ meanas/fdfd/operators.py | 444 ++++++++++--------------- meanas/fdfd/scpml.py | 102 +++--- meanas/fdfd/solvers.py | 57 ++-- meanas/fdfd/waveguide.py | 492 --------------------------- meanas/fdfd/waveguide_2d.py | 607 ++++++++++++++++++++++++++++++++++ meanas/fdfd/waveguide_3d.py | 236 +++++++++++++ meanas/fdfd/waveguide_cyl.py | 138 ++++++++ meanas/fdfd/waveguide_mode.py | 307 ----------------- meanas/fdmath/functional.py | 109 ++++++ meanas/fdmath/operators.py | 231 +++++++++++++ meanas/fdtd/__init__.py | 2 +- meanas/fdtd/base.py | 64 +--- meanas/fdtd/energy.py | 1 + meanas/test/__init__.py | 3 + meanas/test/test_fdfd_pml.py | 5 +- meanas/types.py | 17 +- pdoc_templates/config.mako | 46 +++ pdoc_templates/css.mako | 389 ++++++++++++++++++++++ pdoc_templates/html.mako | 421 +++++++++++++++++++++++ 25 files changed, 2579 insertions(+), 1338 deletions(-) create mode 100755 make_docs.sh delete mode 100644 meanas/fdfd/waveguide.py create mode 100644 meanas/fdfd/waveguide_2d.py create mode 100644 meanas/fdfd/waveguide_3d.py create mode 100644 meanas/fdfd/waveguide_cyl.py delete mode 100644 meanas/fdfd/waveguide_mode.py create mode 100644 meanas/fdmath/functional.py create mode 100644 meanas/fdmath/operators.py create mode 100644 pdoc_templates/config.mako create mode 100644 pdoc_templates/css.mako create mode 100644 pdoc_templates/html.mako diff --git a/make_docs.sh b/make_docs.sh new file mode 100755 index 0000000..f2f2fe2 --- /dev/null +++ b/make_docs.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd ~/projects/meanas +pdoc3 --html --force --template-dir pdoc_templates -o doc . diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py index 5ca9962..960df60 100644 --- a/meanas/eigensolvers.py +++ b/meanas/eigensolvers.py @@ -15,10 +15,13 @@ def power_iteration(operator: sparse.spmatrix, """ Use power iteration to estimate the dominant eigenvector of a matrix. - :param operator: Matrix to analyze. - :param guess_vector: Starting point for the eigenvector. Default is a randomly chosen vector. - :param iterations: Number of iterations to perform. Default 20. - :return: (Largest-magnitude eigenvalue, Corresponding eigenvector estimate) + Args: + operator: Matrix to analyze. + guess_vector: Starting point for the eigenvector. Default is a randomly chosen vector. + iterations: Number of iterations to perform. Default 20. + + Returns: + (Largest-magnitude eigenvalue, Corresponding eigenvector estimate) """ if numpy.any(numpy.equal(guess_vector, None)): v = numpy.random.rand(operator.shape[0]) @@ -91,12 +94,15 @@ def signed_eigensolve(operator: sparse.spmatrix or spalg.LinearOperator, Find the largest-magnitude positive-only (or negative-only) eigenvalues and eigenvectors of the provided matrix. - :param operator: Matrix to analyze. - :param how_many: How many eigenvalues to find. - :param negative: Whether to find negative-only eigenvalues. - Default False (positive only). - :return: (sorted list of eigenvalues, 2D ndarray of corresponding eigenvectors) - eigenvectors[:, k] corresponds to the k-th eigenvalue + Args: + operator: Matrix to analyze. + how_many: How many eigenvalues to find. + negative: Whether to find negative-only eigenvalues. + Default False (positive only). + + Returns: + (sorted list of eigenvalues, 2D ndarray of corresponding eigenvectors) + `eigenvectors[:, k]` corresponds to the k-th eigenvalue """ # Use power iteration to estimate the dominant eigenvector lm_eigval, _ = power_iteration(operator) diff --git a/meanas/fdfd/__init__.py b/meanas/fdfd/__init__.py index 80ba55e..d0d58a9 100644 --- a/meanas/fdfd/__init__.py +++ b/meanas/fdfd/__init__.py @@ -1,2 +1,17 @@ -from . import solvers, operators, functional, scpml, waveguide, waveguide_mode +""" +Tools for finite difference frequency-domain (FDFD) simulations and calculations. + +These mostly involve picking a single frequency, then setting up and solving a +matrix equation (Ax=b) or eigenvalue problem. + + +Submodules: + +- `operators`, `functional`: General FDFD problem setup. +- `solvers`: Solver interface and reference implementation. +- `scpml`: Stretched-coordinate perfectly matched layer (scpml) boundary conditions +- `waveguide_2d`: Operators and mode-solver for waveguides with constant cross-section. +- `waveguide_3d`: Functions for transforming `waveguide_2d` results into 3D. +""" +from . import solvers, operators, functional, scpml, waveguide_2d, waveguide_3d # from . import farfield, bloch TODO diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index 9c252e7..0ca64a7 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -83,7 +83,7 @@ import scipy.optimize from scipy.linalg import norm import scipy.sparse.linalg as spalg -from . import field_t +from .. import field_t logger = logging.getLogger(__name__) diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index faa25b5..665e70f 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -1,7 +1,7 @@ """ Functions for performing near-to-farfield transformation (and the reverse). """ -from typing import Dict, List +from typing import Dict, List, Any import numpy from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift from numpy import pi @@ -14,7 +14,7 @@ def near_to_farfield(E_near: field_t, dx: float, dy: float, padded_size: List[int] = None - ) -> Dict[str]: + ) -> Dict[str, Any]: """ Compute the farfield, i.e. the distribution of the fields after propagation through several wavelengths of uniform medium. @@ -122,7 +122,7 @@ def far_to_nearfield(E_far: field_t, dkx: float, dky: float, padded_size: List[int] = None - ) -> Dict[str]: + ) -> Dict[str, Any]: """ Compute the farfield, i.e. the distribution of the fields after propagation through several wavelengths of uniform medium. diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index bd31192..74b66da 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -2,88 +2,41 @@ Functional versions of many FDFD operators. These can be useful for performing FDFD calculations without needing to construct large matrices in memory. -The functions generated here expect field inputs with shape (3, X, Y, Z), +The functions generated here expect `field_t` inputs with shape (3, X, Y, Z), e.g. E = [E_x, E_y, E_z] where each component has shape (X, Y, Z) """ -from typing import List, Callable +from typing import List, Callable, Tuple import numpy from .. import dx_lists_t, field_t +from ..fdmath.functional import curl_forward, curl_back __author__ = 'Jan Petykiewicz' -functional_matrix = Callable[[field_t], field_t] - - -def curl_h(dxes: dx_lists_t) -> functional_matrix: - """ - Curl operator for use with the H field. - - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :return: Function for taking the discretized curl of the H-field, F(H) -> curlH - """ - dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') - - def dh(f, ax): - return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] - - def ch_fun(h: field_t) -> field_t: - e = numpy.empty_like(h) - e[0] = dh(h[2], 1) - e[0] -= dh(h[1], 2) - e[1] = dh(h[0], 2) - e[1] -= dh(h[2], 0) - e[2] = dh(h[1], 0) - e[2] -= dh(h[0], 1) - return e - - return ch_fun - - -def curl_e(dxes: dx_lists_t) -> functional_matrix: - """ - Curl operator for use with the E field. - - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :return: Function for taking the discretized curl of the E-field, F(E) -> curlE - """ - dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') - - def de(f, ax): - return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] - - def ce_fun(e: field_t) -> field_t: - h = numpy.empty_like(e) - h[0] = de(e[2], 1) - h[0] -= de(e[1], 2) - h[1] = de(e[0], 2) - h[1] -= de(e[2], 0) - h[2] = de(e[1], 0) - h[2] -= de(e[0], 1) - return h - - return ce_fun +field_transform_t = Callable[[field_t], field_t] def e_full(omega: complex, dxes: dx_lists_t, epsilon: field_t, mu: field_t = None - ) -> functional_matrix: + ) -> field_transform_t: """ - Wave operator del x (1/mu * del x) - omega**2 * epsilon, for use with E-field, - with wave equation - (del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J + Wave operator for use with E-field. See `operators.e_full` for details. - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param epsilon: Dielectric constant - :param mu: Magnetic permeability (default 1 everywhere) - :return: Function implementing the wave operator A(E) -> E + Args: + omega: Angular frequency of the simulation + dxes: Grid parameters [dx_e, dx_h] as described in meanas.types + epsilon: Dielectric constant + mu: Magnetic permeability (default 1 everywhere) + + Return: + Function `f` implementing the wave operator + `f(E)` -> `-i * omega * J` """ - ch = curl_h(dxes) - ce = curl_e(dxes) + ch = curl_back(dxes[1]) + ce = curl_forward(dxes[0]) def op_1(e): curls = ch(ce(e)) @@ -103,18 +56,23 @@ def eh_full(omega: complex, dxes: dx_lists_t, epsilon: field_t, mu: field_t = None - ) -> functional_matrix: + ) -> Callable[[field_t, field_t], Tuple[field_t, field_t]]: """ Wave operator for full (both E and H) field representation. + See `operators.eh_full`. - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param epsilon: Dielectric constant - :param mu: Magnetic permeability (default 1 everywhere) - :return: Function implementing the wave operator A(E, H) -> (E, H) + Args: + omega: Angular frequency of the simulation + dxes: Grid parameters [dx_e, dx_h] as described in meanas.types + epsilon: Dielectric constant + mu: Magnetic permeability (default 1 everywhere) + + Returns: + Function `f` implementing the wave operator + `f(E, H)` -> `(J, -M)` """ - ch = curl_h(dxes) - ce = curl_e(dxes) + ch = curl_back(dxes[1]) + ce = curl_forward(dxes[0]) def op_1(e, h): return (ch(h) - 1j * omega * epsilon * e, @@ -133,23 +91,27 @@ def eh_full(omega: complex, def e2h(omega: complex, dxes: dx_lists_t, mu: field_t = None, - ) -> functional_matrix: + ) -> field_transform_t: """ - Utility operator for converting the E field into the H field. - For use with e_full -- assumes that there is no magnetic current M. + Utility operator for converting the `E` field into the `H` field. + For use with `e_full` -- assumes that there is no magnetic current `M`. - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param mu: Magnetic permeability (default 1 everywhere) - :return: Function for converting E to H - """ - A2 = curl_e(dxes) + Args: + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + mu: Magnetic permeability (default 1 everywhere) + + Return: + Function `f` for converting `E` to `H`, + `f(E)` -> `H` + """ + ce = curl_forward(dxes[0]) def e2h_1_1(e): - return A2(e) / (-1j * omega) + return ce(e) / (-1j * omega) def e2h_mu(e): - return A2(e) / (-1j * omega * mu) + return ce(e) / (-1j * omega * mu) if numpy.any(numpy.equal(mu, None)): return e2h_1_1 @@ -160,18 +122,22 @@ def e2h(omega: complex, def m2j(omega: complex, dxes: dx_lists_t, mu: field_t = None, - ) -> functional_matrix: + ) -> field_transform_t: """ - Utility operator for converting magnetic current (M) distribution - into equivalent electric current distribution (J). - For use with e.g. e_full(). + Utility operator for converting magnetic current `M` distribution + into equivalent electric current distribution `J`. + For use with e.g. `e_full`. - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param mu: Magnetic permeability (default 1 everywhere) - :return: Function for converting M to J - """ - ch = curl_h(dxes) + Args: + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + mu: Magnetic permeability (default 1 everywhere) + + Returns: + Function `f` for converting `M` to `J`, + `f(M)` -> `J` + """ + ch = curl_back(dxes[1]) def m2j_mu(m): J = ch(m / mu) / (-1j * omega) @@ -192,10 +158,23 @@ def e_tfsf_source(TF_region: field_t, dxes: dx_lists_t, epsilon: field_t, mu: field_t = None, - ) -> functional_matrix: + ) -> field_transform_t: """ - Operator that turuns an E-field distribution into a total-field/scattered-field + Operator that turns an E-field distribution into a total-field/scattered-field (TFSF) source. + + Args: + TF_region: mask which is set to 1 in the total-field region, and 0 elsewhere + (i.e. in the scattered-field region). + Should have the same shape as the simulation grid, e.g. `epsilon[0].shape`. + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + epsilon: Dielectric constant distribution + mu: Magnetic permeability (default 1 everywhere) + + Returns: + Function `f` which takes an E field and returns a current distribution, + `f(E)` -> `J` """ # TODO documentation A = e_full(omega, dxes, epsilon, mu) @@ -205,7 +184,28 @@ def e_tfsf_source(TF_region: field_t, return neg_iwj / (-1j * omega) -def poynting_e_cross_h(dxes: dx_lists_t): +def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[field_t, field_t], field_t]: + """ + Generates a function that takes the single-frequency `E` and `H` fields + and calculates the cross product `E` x `H` = \\( E \\times H \\) as required + for the Poynting vector, \\( S = E \\times H \\) + + Note: + This function also shifts the input `E` field by one cell as required + for computing the Poynting cross product (see `meanas.fdfd` module docs). + + Note: + If `E` and `H` are peak amplitudes as assumed elsewhere in this code, + the time-average of the poynting vector is ` = Re(S)/2 = Re(E x H) / 2`. + The factor of `1/2` can be omitted if root-mean-square quantities are used + instead. + + Args: + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + + Returns: + Function `f` that returns E x H as required for the poynting vector. + """ def exh(e: field_t, h: field_t): s = numpy.empty_like(e) ex = e[0] * dxes[0][0][:, None, None] diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 905c23e..20cf96a 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -1,18 +1,19 @@ """ Sparse matrix operators for use with electromagnetic wave equations. -These functions return sparse-matrix (scipy.sparse.spmatrix) representations of +These functions return sparse-matrix (`scipy.sparse.spmatrix`) representations of a variety of operators, intended for use with E and H fields vectorized using the - meanas.vec() and .unvec() functions (column-major/Fortran ordering). + `meanas.vec()` and `meanas.unvec()` functions. -E- and H-field values are defined on a Yee cell; epsilon values should be calculated for - cells centered at each E component (mu at each H component). +E- and H-field values are defined on a Yee cell; `epsilon` values should be calculated for + cells centered at each E component (`mu` at each H component). -Many of these functions require a 'dxes' parameter, of type meanas.dx_lists_type; see -the meanas.types submodule for details. +Many of these functions require a `dxes` parameter, of type `dx_lists_t`; see +the `meanas.types` submodule for details. The following operators are included: + - E-only wave operator - H-only wave operator - EH wave operator @@ -20,8 +21,6 @@ The following operators are included: - E to H conversion - M to J conversion - Poynting cross products - -Also available: - Circular shifts - Discrete derivatives - Averaging operators @@ -33,6 +32,7 @@ import numpy import scipy.sparse as sparse from .. import vec, dx_lists_t, vfield_t +from ..fdmath.operators import shift_with_mirror, rotation, curl_forward, curl_back __author__ = 'Jan Petykiewicz' @@ -46,26 +46,35 @@ def e_full(omega: complex, pmc: vfield_t = None, ) -> sparse.spmatrix: """ - Wave operator del x (1/mu * del x) - omega**2 * epsilon, for use with E-field, - with wave equation - (del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J + Wave operator + $$ \\nabla \\times (\\frac{1}{\\mu} \\nabla \\times) - \\omega^2 \\epsilon $$ - To make this matrix symmetric, use the preconditions from e_full_preconditioners(). + del x (1/mu * del x) - omega**2 * epsilon - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param epsilon: Vectorized dielectric constant - :param mu: Vectorized magnetic permeability (default 1 everywhere). - :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted - as containing a perfect electrical conductor (PEC). - The PEC is applied per-field-component (ie, pec.size == epsilon.size) - :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted - as containing a perfect magnetic conductor (PMC). - The PMC is applied per-field-component (ie, pmc.size == epsilon.size) - :return: Sparse matrix containing the wave operator + for use with the E-field, with wave equation + $$ (\\nabla \\times (\\frac{1}{\\mu} \\nabla \\times) - \\omega^2 \\epsilon) E = -\\imath \\omega J $$ + + (del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J + + To make this matrix symmetric, use the preconditioners from `e_full_preconditioners()`. + + Args: + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + epsilon: Vectorized dielectric constant + mu: Vectorized magnetic permeability (default 1 everywhere). + pec: Vectorized mask specifying PEC cells. Any cells where `pec != 0` are interpreted + as containing a perfect electrical conductor (PEC). + The PEC is applied per-field-component (i.e. `pec.size == epsilon.size`) + pmc: Vectorized mask specifying PMC cells. Any cells where `pmc != 0` are interpreted + as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (i.e. `pmc.size == epsilon.size`) + + Returns: + Sparse matrix containing the wave operator. """ - ce = curl_e(dxes) - ch = curl_h(dxes) + ch = curl_back(dxes[1]) + ce = curl_forward(dxes[0]) if numpy.any(numpy.equal(pec, None)): pe = sparse.eye(epsilon.size) @@ -90,15 +99,18 @@ def e_full(omega: complex, def e_full_preconditioners(dxes: dx_lists_t ) -> Tuple[sparse.spmatrix, sparse.spmatrix]: """ - Left and right preconditioners (Pl, Pr) for symmetrizing the e_full wave operator. + Left and right preconditioners `(Pl, Pr)` for symmetrizing the `e_full` wave operator. - The preconditioned matrix A_symm = (Pl @ A @ Pr) is complex-symmetric + The preconditioned matrix `A_symm = (Pl @ A @ Pr)` is complex-symmetric (non-Hermitian unless there is no loss or PMLs). - The preconditioner matrices are diagonal and complex, with Pr = 1 / Pl + The preconditioner matrices are diagonal and complex, with `Pr = 1 / Pl` - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :return: Preconditioner matrices (Pl, Pr) + Args: + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + + Returns: + Preconditioner matrices `(Pl, Pr)`. """ p_squared = [dxes[0][0][:, None, None] * dxes[1][1][None, :, None] * dxes[1][2][None, None, :], dxes[1][0][:, None, None] * dxes[0][1][None, :, None] * dxes[1][2][None, None, :], @@ -118,24 +130,33 @@ def h_full(omega: complex, pmc: vfield_t = None, ) -> sparse.spmatrix: """ - Wave operator del x (1/epsilon * del x) - omega**2 * mu, for use with H-field, - with wave equation - (del x (1/epsilon * del x) - omega**2 * mu) H = i * omega * M + Wave operator + $$ \\nabla \\times (\\frac{1}{\\epsilon} \\nabla \\times) - \\omega^2 \\mu $$ - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param epsilon: Vectorized dielectric constant - :param mu: Vectorized magnetic permeability (default 1 everywhere) - :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted - as containing a perfect electrical conductor (PEC). - The PEC is applied per-field-component (ie, pec.size == epsilon.size) - :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted - as containing a perfect magnetic conductor (PMC). - The PMC is applied per-field-component (ie, pmc.size == epsilon.size) - :return: Sparse matrix containing the wave operator + del x (1/epsilon * del x) - omega**2 * mu + + for use with the H-field, with wave equation + $$ (\\nabla \\times (\\frac{1}{\\epsilon} \\nabla \\times) - \\omega^2 \\mu) E = \\imath \\omega M $$ + + (del x (1/epsilon * del x) - omega**2 * mu) E = i * omega * M + + Args: + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + epsilon: Vectorized dielectric constant + mu: Vectorized magnetic permeability (default 1 everywhere) + pec: Vectorized mask specifying PEC cells. Any cells where `pec != 0` are interpreted + as containing a perfect electrical conductor (PEC). + The PEC is applied per-field-component (i.e. `pec.size == epsilon.size`) + pmc: Vectorized mask specifying PMC cells. Any cells where `pmc != 0` are interpreted + as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (i.e. `pmc.size == epsilon.size`) + + Returns: + Sparse matrix containing the wave operator. """ - ec = curl_e(dxes) - hc = curl_h(dxes) + ch = curl_back(dxes[1]) + ce = curl_forward(dxes[0]) if numpy.any(numpy.equal(pec, None)): pe = sparse.eye(epsilon.size) @@ -153,7 +174,7 @@ def h_full(omega: complex, else: m = sparse.diags(mu) - A = pm @ (ec @ pe @ e_div @ hc - omega**2 * m) @ pm + A = pm @ (ce @ pe @ e_div @ ch - omega**2 * m) @ pm return A @@ -165,24 +186,42 @@ def eh_full(omega: complex, pmc: vfield_t = None ) -> sparse.spmatrix: """ - Wave operator for [E, H] field representation. This operator implements Maxwell's + Wave operator for `[E, H]` field representation. This operator implements Maxwell's equations without cancelling out either E or H. The operator is - [[-i * omega * epsilon, del x], - [del x, i * omega * mu]] + $$ \\begin{bmatrix} + -\\imath \\omega \\epsilon & \\nabla \\times \\\\ + \\nabla \\times & \\imath \\omega \\mu + \\end{bmatrix} $$ - for use with a field vector of the form hstack(vec(E), vec(H)). + [[-i * omega * epsilon, del x ], + [del x, i * omega * mu]] - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param epsilon: Vectorized dielectric constant - :param mu: Vectorized magnetic permeability (default 1 everywhere) - :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted - as containing a perfect electrical conductor (PEC). - The PEC is applied per-field-component (i.e., pec.size == epsilon.size) - :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted - as containing a perfect magnetic conductor (PMC). - The PMC is applied per-field-component (i.e., pmc.size == epsilon.size) - :return: Sparse matrix containing the wave operator + for use with a field vector of the form `cat(vec(E), vec(H))`: + $$ \\begin{bmatrix} + -\\imath \\omega \\epsilon & \\nabla \\times \\\\ + \\nabla \\times & \\imath \\omega \\mu + \\end{bmatrix} + \\begin{bmatrix} E \\\\ + H + \\end{bmatrix} + = \\begin{bmatrix} J \\\\ + -M + \\end{bmatrix} $$ + + Args: + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + epsilon: Vectorized dielectric constant + mu: Vectorized magnetic permeability (default 1 everywhere) + pec: Vectorized mask specifying PEC cells. Any cells where `pec != 0` are interpreted + as containing a perfect electrical conductor (PEC). + The PEC is applied per-field-component (i.e. `pec.size == epsilon.size`) + pmc: Vectorized mask specifying PMC cells. Any cells where `pmc != 0` are interpreted + as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (i.e. `pmc.size == epsilon.size`) + + Returns: + Sparse matrix containing the wave operator. """ if numpy.any(numpy.equal(pec, None)): pe = sparse.eye(epsilon.size) @@ -200,34 +239,14 @@ def eh_full(omega: complex, iwm *= sparse.diags(mu) iwm = pm @ iwm @ pm - A1 = pe @ curl_h(dxes) @ pm - A2 = pm @ curl_e(dxes) @ pe + A1 = pe @ curl_back(dxes[1]) @ pm + A2 = pm @ curl_forward(dxes[0]) @ pe A = sparse.bmat([[-iwe, A1], [A2, iwm]]) return A -def curl_h(dxes: dx_lists_t) -> sparse.spmatrix: - """ - Curl operator for use with the H field. - - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :return: Sparse matrix for taking the discretized curl of the H-field - """ - return cross(deriv_back(dxes[1])) - - -def curl_e(dxes: dx_lists_t) -> sparse.spmatrix: - """ - Curl operator for use with the E field. - - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :return: Sparse matrix for taking the discretized curl of the E-field - """ - return cross(deriv_forward(dxes[0])) - - def e2h(omega: complex, dxes: dx_lists_t, mu: vfield_t = None, @@ -235,17 +254,20 @@ def e2h(omega: complex, ) -> sparse.spmatrix: """ Utility operator for converting the E field into the H field. - For use with e_full -- assumes that there is no magnetic current M. + For use with `e_full()` -- assumes that there is no magnetic current M. - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param mu: Vectorized magnetic permeability (default 1 everywhere) - :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted - as containing a perfect magnetic conductor (PMC). - The PMC is applied per-field-component (ie, pmc.size == epsilon.size) - :return: Sparse matrix for converting E to H + Args: + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + mu: Vectorized magnetic permeability (default 1 everywhere) + pmc: Vectorized mask specifying PMC cells. Any cells where `pmc != 0` are interpreted + as containing a perfect magnetic conductor (PMC). + The PMC is applied per-field-component (i.e. `pmc.size == epsilon.size`) + + Returns: + Sparse matrix for converting E to H. """ - op = curl_e(dxes) / (-1j * omega) + op = curl_forward(dxes[0]) / (-1j * omega) if not numpy.any(numpy.equal(mu, None)): op = sparse.diags(1 / mu) @ op @@ -261,16 +283,18 @@ def m2j(omega: complex, mu: vfield_t = None ) -> sparse.spmatrix: """ - Utility operator for converting M field into J. - Converts a magnetic current M into an electric current J. - For use with eg. e_full. + Operator for converting a magnetic current M into an electric current J. + For use with eg. `e_full()`. - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param mu: Vectorized magnetic permeability (default 1 everywhere) - :return: Sparse matrix for converting E to H + Args: + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + mu: Vectorized magnetic permeability (default 1 everywhere) + + Returns: + Sparse matrix for converting M to J. """ - op = curl_h(dxes) / (1j * omega) + op = curl_back(dxes[1]) / (1j * omega) if not numpy.any(numpy.equal(mu, None)): op = op @ sparse.diags(1 / mu) @@ -278,178 +302,17 @@ def m2j(omega: complex, return op -def rotation(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmatrix: - """ - Utility operator for performing a circular shift along a specified axis by a - specified number of elements. - - :param axis: Axis to shift along. x=0, y=1, z=2 - :param shape: Shape of the grid being shifted - :param shift_distance: Number of cells to shift by. May be negative. Default 1. - :return: Sparse matrix for performing the circular shift - """ - if len(shape) not in (2, 3): - raise Exception('Invalid shape: {}'.format(shape)) - if axis not in range(len(shape)): - raise Exception('Invalid direction: {}, shape is {}'.format(axis, shape)) - - shifts = [abs(shift_distance) if a == axis else 0 for a in range(3)] - shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts)] - ijk = numpy.meshgrid(*shifted_diags, indexing='ij') - - n = numpy.prod(shape) - i_ind = numpy.arange(n) - j_ind = numpy.ravel_multi_index(ijk, shape, order='C') - - vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) - - d = sparse.csr_matrix(vij, shape=(n, n)) - - if shift_distance < 0: - d = d.T - - return d - - -def shift_with_mirror(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmatrix: - """ - Utility operator for performing an n-element shift along a specified axis, with mirror - boundary conditions applied to the cells beyond the receding edge. - - :param axis: Axis to shift along. x=0, y=1, z=2 - :param shape: Shape of the grid being shifted - :param shift_distance: Number of cells to shift by. May be negative. Default 1. - :return: Sparse matrix for performing the circular shift - """ - if len(shape) not in (2, 3): - raise Exception('Invalid shape: {}'.format(shape)) - if axis not in range(len(shape)): - raise Exception('Invalid direction: {}, shape is {}'.format(axis, shape)) - if shift_distance >= shape[axis]: - raise Exception('Shift ({}) is too large for axis {} of size {}'.format( - shift_distance, axis, shape[axis])) - - def mirrored_range(n, s): - v = numpy.arange(n) + s - v = numpy.where(v >= n, 2 * n - v - 1, v) - v = numpy.where(v < 0, - 1 - v, v) - return v - - shifts = [shift_distance if a == axis else 0 for a in range(3)] - shifted_diags = [mirrored_range(n, s) for n, s in zip(shape, shifts)] - ijk = numpy.meshgrid(*shifted_diags, indexing='ij') - - n = numpy.prod(shape) - i_ind = numpy.arange(n) - j_ind = numpy.ravel_multi_index(ijk, shape, order='C') - - vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) - - d = sparse.csr_matrix(vij, shape=(n, n)) - return d - - -def deriv_forward(dx_e: List[numpy.ndarray]) -> List[sparse.spmatrix]: - """ - Utility operators for taking discretized derivatives (forward variant). - - :param dx_e: Lists of cell sizes for all axes [[dx_0, dx_1, ...], ...]. - :return: List of operators for taking forward derivatives along each axis. - """ - shape = [s.size for s in dx_e] - n = numpy.prod(shape) - - dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij') - - def deriv(axis): - return rotation(axis, shape, 1) - sparse.eye(n) - - Ds = [sparse.diags(+1 / dx.ravel(order='C')) @ deriv(a) - for a, dx in enumerate(dx_e_expanded)] - - return Ds - - -def deriv_back(dx_h: List[numpy.ndarray]) -> List[sparse.spmatrix]: - """ - Utility operators for taking discretized derivatives (backward variant). - - :param dx_h: Lists of cell sizes for all axes [[dx_0, dx_1, ...], ...]. - :return: List of operators for taking forward derivatives along each axis. - """ - shape = [s.size for s in dx_h] - n = numpy.prod(shape) - - dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij') - - def deriv(axis): - return rotation(axis, shape, -1) - sparse.eye(n) - - Ds = [sparse.diags(-1 / dx.ravel(order='C')) @ deriv(a) - for a, dx in enumerate(dx_h_expanded)] - - return Ds - - -def cross(B: List[sparse.spmatrix]) -> sparse.spmatrix: - """ - Cross product operator - - :param B: List [Bx, By, Bz] of sparse matrices corresponding to the x, y, z - portions of the operator on the left side of the cross product. - :return: Sparse matrix corresponding to (B x), where x is the cross product - """ - n = B[0].shape[0] - zero = sparse.csr_matrix((n, n)) - return sparse.bmat([[zero, -B[2], B[1]], - [B[2], zero, -B[0]], - [-B[1], B[0], zero]]) - - -def vec_cross(b: vfield_t) -> sparse.spmatrix: - """ - Vector cross product operator - - :param b: Vector on the left side of the cross product - :return: Sparse matrix corresponding to (b x), where x is the cross product - """ - B = [sparse.diags(c) for c in numpy.split(b, 3)] - return cross(B) - - -def avgf(axis: int, shape: List[int]) -> sparse.spmatrix: - """ - Forward average operator (x4 = (x4 + x5) / 2) - - :param axis: Axis to average along (x=0, y=1, z=2) - :param shape: Shape of the grid to average - :return: Sparse matrix for forward average operation - """ - if len(shape) not in (2, 3): - raise Exception('Invalid shape: {}'.format(shape)) - - n = numpy.prod(shape) - return 0.5 * (sparse.eye(n) + rotation(axis, shape)) - - -def avgb(axis: int, shape: List[int]) -> sparse.spmatrix: - """ - Backward average operator (x4 = (x4 + x3) / 2) - - :param axis: Axis to average along (x=0, y=1, z=2) - :param shape: Shape of the grid to average - :return: Sparse matrix for backward average operation - """ - return avgf(axis, shape).T - - def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ - Operator for computing the Poynting vector, containing the (E x) portion of the Poynting vector. + Operator for computing the Poynting vector, containing the + (E x) portion of the Poynting vector. - :param e: Vectorized E-field for the ExH cross product - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :return: Sparse matrix containing (E x) portion of Poynting cross product + Args: + e: Vectorized E-field for the ExH cross product + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + + Returns: + Sparse matrix containing (E x) portion of Poynting cross product. """ shape = [len(dx) for dx in dxes[0]] @@ -472,9 +335,12 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector. - :param h: Vectorized H-field for the HxE cross product - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :return: Sparse matrix containing (H x) portion of Poynting cross product + Args: + h: Vectorized H-field for the HxE cross product + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + + Returns: + Sparse matrix containing (H x) portion of Poynting cross product. """ shape = [len(dx) for dx in dxes[0]] @@ -499,8 +365,22 @@ def e_tfsf_source(TF_region: vfield_t, mu: vfield_t = None, ) -> sparse.spmatrix: """ - Operator that turns an E-field distribution into a total-field/scattered-field - (TFSF) source. + Operator that turns a desired E-field distribution into a + total-field/scattered-field (TFSF) source. + + TODO: Reference Rumpf paper + + Args: + TF_region: Mask, which is set to 1 inside the total-field region and 0 in the + scattered-field region + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + epsilon: Vectorized dielectric constant + mu: Vectorized magnetic permeability (default 1 everywhere). + + Returns: + Sparse matrix that turns an E-field into a current (J) distribution. + """ # TODO documentation A = e_full(omega, dxes, epsilon, mu) @@ -518,7 +398,19 @@ def e_boundary_source(mask: vfield_t, """ Operator that turns an E-field distrubtion into a current (J) distribution along the edges (external and internal) of the provided mask. This is just an - e_tfsf_source with an additional masking step. + `e_tfsf_source()` with an additional masking step. + + Args: + mask: The current distribution is generated at the edges of the mask, + i.e. any points where shifting the mask by one cell in any direction + would change its value. + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + epsilon: Vectorized dielectric constant + mu: Vectorized magnetic permeability (default 1 everywhere). + + Returns: + Sparse matrix that turns an E-field into a current (J) distribution. """ full = e_tfsf_source(TF_region=mask, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py index 897d43a..c9a93e6 100644 --- a/meanas/fdfd/scpml.py +++ b/meanas/fdfd/scpml.py @@ -1,5 +1,5 @@ """ -Functions for creating stretched coordinate PMLs. +Functions for creating stretched coordinate perfectly matched layer (PML) absorbers. """ from typing import List, Callable @@ -7,24 +7,29 @@ import numpy from .. import dx_lists_t + __author__ = 'Jan Petykiewicz' -s_function_type = Callable[[float], float] +s_function_t = Callable[[float], float] +"""Typedef for s-functions""" def prepare_s_function(ln_R: float = -16, m: float = 4 - ) -> s_function_type: + ) -> s_function_t: """ Create an s_function to pass to the SCPML functions. This is used when you would like to customize the PML parameters. - :param ln_R: Natural logarithm of the desired reflectance - :param m: Polynomial order for the PML (imaginary part increases as distance ** m) - :return: An s_function, which takes an ndarray (distances) and returns an ndarray (complex part - of the cell width; needs to be divided by sqrt(epilon_effective) * real(omega)) - before use. + Args: + ln_R: Natural logarithm of the desired reflectance + m: Polynomial order for the PML (imaginary part increases as distance ** m) + + Returns: + An s_function, which takes an ndarray (distances) and returns an ndarray (complex part + of the cell width; needs to be divided by `sqrt(epilon_effective) * real(omega))` + before use. """ def s_factor(distance: numpy.ndarray) -> numpy.ndarray: s_max = (m + 1) * ln_R / 2 # / 2 because we assume periodic boundaries @@ -36,26 +41,29 @@ def uniform_grid_scpml(shape: numpy.ndarray or List[int], thicknesses: numpy.ndarray or List[int], omega: float, epsilon_effective: float = 1.0, - s_function: s_function_type = None, + s_function: s_function_t = None, ) -> dx_lists_t: """ Create dx arrays for a uniform grid with a cell width of 1 and a pml. - If you want something more fine-grained, check out stretch_with_scpml(...). + If you want something more fine-grained, check out `stretch_with_scpml(...)`. - :param shape: Shape of the grid, including the PMLs (which are 2*thicknesses thick) - :param thicknesses: [th_x, th_y, th_z] Thickness of the PML in each direction. - Both polarities are added. - Each th_ of pml is applied twice, once on each edge of the grid along the given axis. - th_* may be zero, in which case no pml is added. - :param omega: Angular frequency for the simulation - :param epsilon_effective: Effective epsilon of the PML. Match this to the material - at the edge of your grid. - Default 1. - :param s_function: s_function created by prepare_s_function(...), allowing - customization of pml parameters. - Default uses prepare_s_function() with no parameters. - :return: Complex cell widths (dx_lists) + Args: + shape: Shape of the grid, including the PMLs (which are 2*thicknesses thick) + thicknesses: `[th_x, th_y, th_z]` + Thickness of the PML in each direction. + Both polarities are added. + Each th_ of pml is applied twice, once on each edge of the grid along the given axis. + `th_*` may be zero, in which case no pml is added. + omega: Angular frequency for the simulation + epsilon_effective: Effective epsilon of the PML. Match this to the material + at the edge of your grid. + Default 1. + s_function: created by `prepare_s_function(...)`, allowing customization of pml parameters. + Default uses `prepare_s_function()` with no parameters. + + Returns: + Complex cell widths (dx_lists_t) as discussed in `meanas.types`. """ if s_function is None: s_function = prepare_s_function() @@ -88,21 +96,25 @@ def stretch_with_scpml(dxes: dx_lists_t, omega: float, epsilon_effective: float = 1.0, thickness: int = 10, - s_function: s_function_type = None, + s_function: s_function_t = None, ) -> dx_lists_t: """ Stretch dxes to contain a stretched-coordinate PML (SCPML) in one direction along one axis. - :param dxes: dx_tuple with coordinates to stretch - :param axis: axis to stretch (0=x, 1=y, 2=z) - :param polarity: direction to stretch (-1 for -ve, +1 for +ve) - :param omega: Angular frequency for the simulation - :param epsilon_effective: Effective epsilon of the PML. Match this to the material at the - edge of your grid. Default 1. - :param thickness: number of cells to use for pml (default 10) - :param s_function: s_function created by prepare_s_function(...), allowing customization - of pml parameters. Default uses prepare_s_function() with no parameters. - :return: Complex cell widths + Args: + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + axis: axis to stretch (0=x, 1=y, 2=z) + polarity: direction to stretch (-1 for -ve, +1 for +ve) + omega: Angular frequency for the simulation + epsilon_effective: Effective epsilon of the PML. Match this to the material at the + edge of your grid. Default 1. + thickness: number of cells to use for pml (default 10) + s_function: Created by `prepare_s_function(...)`, allowing customization + of pml parameters. Default uses `prepare_s_function()` with no parameters. + + Returns: + Complex cell widths (dx_lists_t) as discussed in `meanas.types`. + Multiple calls to this function may be necessary if multiple absorpbing boundaries are needed. """ if s_function is None: s_function = prepare_s_function() @@ -147,25 +159,3 @@ def stretch_with_scpml(dxes: dx_lists_t, dxes[1][axis] = dx_bi return dxes - - -def generate_periodic_dx(pos: List[numpy.ndarray]) -> dx_lists_t: - """ - Given a list of 3 ndarrays cell centers, creates the cell width parameters for a periodic grid. - - :param pos: List of 3 ndarrays of cell centers - :return: (dx_a, dx_b) cell widths (no pml) - """ - if len(pos) != 3: - raise Exception('Must have len(pos) == 3') - - dx_a = [numpy.array(numpy.inf)] * 3 - dx_b = [numpy.array(numpy.inf)] * 3 - - for i, p_orig in enumerate(pos): - p = numpy.array(p_orig, dtype=float) - if p.size != 1: - p_shifted = numpy.hstack((p[1:], p[-1] + (p[1] - p[0]))) - dx_a[i] = numpy.diff(p) - dx_b[i] = numpy.diff((p + p_shifted) / 2) - return dx_a, dx_b diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py index a0ce403..aa9633a 100644 --- a/meanas/fdfd/solvers.py +++ b/meanas/fdfd/solvers.py @@ -1,5 +1,5 @@ """ -Solvers for FDFD problems. +Solvers and solver interface for FDFD problems. """ from typing import List, Callable, Dict, Any @@ -22,10 +22,13 @@ def _scipy_qmr(A: scipy.sparse.csr_matrix, """ Wrapper for scipy.sparse.linalg.qmr - :param A: Sparse matrix - :param b: Right-hand-side vector - :param kwargs: Passed as **kwargs to the wrapped function - :return: Guess for solution (returned even if didn't converge) + Args: + A: Sparse matrix + b: Right-hand-side vector + kwargs: Passed as **kwargs to the wrapped function + + Returns: + Guess for solution (returned even if didn't converge) """ ''' @@ -70,27 +73,31 @@ def generic(omega: complex, """ Conjugate gradient FDFD solver using CSR sparse matrices. - All ndarray arguments should be 1D array, as returned by meanas.vec(). + All ndarray arguments should be 1D arrays, as returned by `meanas.vec()`. - :param omega: Complex frequency to solve at. - :param dxes: [[dx_e, dy_e, dz_e], [dx_h, dy_h, dz_h]] (complex cell sizes) - :param J: Electric current distribution (at E-field locations) - :param epsilon: Dielectric constant distribution (at E-field locations) - :param mu: Magnetic permeability distribution (at H-field locations) - :param pec: Perfect electric conductor distribution - (at E-field locations; non-zero value indicates PEC is present) - :param pmc: Perfect magnetic conductor distribution - (at H-field locations; non-zero value indicates PMC is present) - :param adjoint: If true, solves the adjoint problem. - :param matrix_solver: Called as matrix_solver(A, b, **matrix_solver_opts) -> x - Where A: scipy.sparse.csr_matrix - b: numpy.ndarray - x: numpy.ndarray - Default is a wrapped version of scipy.sparse.linalg.qmr() - which doesn't return convergence info and logs the residual - every 100 iterations. - :param matrix_solver_opts: Passed as kwargs to matrix_solver(...) - :return: E-field which solves the system. + Args: + omega: Complex frequency to solve at. + dxes: `[[dx_e, dy_e, dz_e], [dx_h, dy_h, dz_h]]` (complex cell sizes) as + discussed in `meanas.types` + J: Electric current distribution (at E-field locations) + epsilon: Dielectric constant distribution (at E-field locations) + mu: Magnetic permeability distribution (at H-field locations) + pec: Perfect electric conductor distribution + (at E-field locations; non-zero value indicates PEC is present) + pmc: Perfect magnetic conductor distribution + (at H-field locations; non-zero value indicates PMC is present) + adjoint: If true, solves the adjoint problem. + matrix_solver: Called as `matrix_solver(A, b, **matrix_solver_opts) -> x`, + where `A`: `scipy.sparse.csr_matrix`; + `b`: `numpy.ndarray`; + `x`: `numpy.ndarray`; + Default is a wrapped version of `scipy.sparse.linalg.qmr()` + which doesn't return convergence info and logs the residual + every 100 iterations. + matrix_solver_opts: Passed as kwargs to `matrix_solver(...)` + + Returns: + E-field which solves the system. """ if matrix_solver_opts is None: diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py deleted file mode 100644 index c24a471..0000000 --- a/meanas/fdfd/waveguide.py +++ /dev/null @@ -1,492 +0,0 @@ -""" -Various operators and helper functions for solving for waveguide modes. - -Assuming a z-dependence of the from exp(-i * wavenumber * z), we can simplify Maxwell's - equations in the absence of sources to the form - -A @ [H_x, H_y] = wavenumber**2 * [H_x, H_y] - -with A = -omega**2 * epsilon * mu + -epsilon * [[-Dy], [Dx]] / epsilon * [-Dy, Dx] + -[[Dx], [Dy]] / mu * [Dx, Dy] * mu - -which is the form used in this file. - -As the z-dependence is known, all the functions in this file assume a 2D grid - (ie. dxes = [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dx_h_0, ...], [dy_h_0, ...]]]) - with propagation along the z axis. -""" -# TODO update module docs - -from typing import List, Tuple -import numpy -from numpy.linalg import norm -import scipy.sparse as sparse - -from .. import vec, unvec, dx_lists_t, field_t, vfield_t -from . import operators - - -__author__ = 'Jan Petykiewicz' - - -def operator_e(omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - ) -> sparse.spmatrix: - if numpy.any(numpy.equal(mu, None)): - mu = numpy.ones_like(epsilon) - - Dfx, Dfy = operators.deriv_forward(dxes[0]) - Dbx, Dby = operators.deriv_back(dxes[1]) - - eps_parts = numpy.split(epsilon, 3) - eps_xy = sparse.diags(numpy.hstack((eps_parts[0], eps_parts[1]))) - eps_z_inv = sparse.diags(1 / eps_parts[2]) - - mu_parts = numpy.split(mu, 3) - mu_yx = sparse.diags(numpy.hstack((mu_parts[1], mu_parts[0]))) - mu_z_inv = sparse.diags(1 / mu_parts[2]) - - op = omega * omega * mu_yx @ eps_xy + \ - mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx)) + \ - sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy - return op - - -def operator_h(omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - ) -> sparse.spmatrix: - """ - Waveguide operator of the form - - omega**2 * epsilon * mu + - epsilon * [[-Dy], [Dx]] / epsilon * [-Dy, Dx] + - [[Dx], [Dy]] / mu * [Dx, Dy] * mu - - for use with a field vector of the form [H_x, H_y]. - - This operator can be used to form an eigenvalue problem of the form - A @ [H_x, H_y] = wavenumber**2 * [H_x, H_y] - - which can then be solved for the eigenmodes of the system (an exp(-i * wavenumber * z) - z-dependence is assumed for the fields). - - :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param epsilon: Vectorized dielectric constant grid - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Sparse matrix representation of the operator - """ - if numpy.any(numpy.equal(mu, None)): - mu = numpy.ones_like(epsilon) - - Dfx, Dfy = operators.deriv_forward(dxes[0]) - Dbx, Dby = operators.deriv_back(dxes[1]) - - eps_parts = numpy.split(epsilon, 3) - eps_yx = sparse.diags(numpy.hstack((eps_parts[1], eps_parts[0]))) - eps_z_inv = sparse.diags(1 / eps_parts[2]) - - mu_parts = numpy.split(mu, 3) - mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) - mu_z_inv = sparse.diags(1 / mu_parts[2]) - - op = omega * omega * eps_yx @ mu_xy + \ - eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx)) + \ - sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy - - return op - - -def normalized_fields_e(e_xy: numpy.ndarray, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - prop_phase: float = 0, - ) -> Tuple[vfield_t, vfield_t]: - """ - Given a vector e_xy containing the vectorized E_x and E_y fields, - returns normalized, vectorized E and H fields for the system. - - :param e_xy: Vector containing E_x and E_y fields - :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` - :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param epsilon: Vectorized dielectric constant grid - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :param prop_phase: Phase shift (dz * corrected_wavenumber) over 1 cell in propagation direction. - Default 0 (continuous propagation direction, i.e. dz->0). - :return: Normalized, vectorized (e, h) containing all vector components. - """ - e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy - h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy - e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, - mu=mu, prop_phase=prop_phase) - return e_norm, h_norm - - -def normalized_fields_h(h_xy: numpy.ndarray, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - prop_phase: float = 0, - ) -> Tuple[vfield_t, vfield_t]: - """ - Given a vector e_xy containing the vectorized E_x and E_y fields, - returns normalized, vectorized E and H fields for the system. - - :param e_xy: Vector containing E_x and E_y fields - :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` - :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param epsilon: Vectorized dielectric constant grid - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :param dxes_prop: Grid cell width in the propagation direction. Default 0 (continuous). - :return: Normalized, vectorized (e, h) containing all vector components. - """ - e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy - h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy - e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, - mu=mu, prop_phase=prop_phase) - return e_norm, h_norm - - -def _normalized_fields(e: numpy.ndarray, - h: numpy.ndarray, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - prop_phase: float = 0, - ) -> Tuple[vfield_t, vfield_t]: - # TODO documentation - shape = [s.size for s in dxes[0]] - dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)] - - E = unvec(e, shape) - H = unvec(h, shape) - - # Find time-averaged Sz and normalize to it - # H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting - phase = numpy.exp(-1j * -prop_phase / 2) - Sz_a = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0] - Sz_b = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1] - Sz_tavg = numpy.real(Sz_a.sum() - Sz_b.sum()) * 0.5 # 0.5 since E, H are assumed to be peak (not RMS) amplitudes - assert Sz_tavg > 0, 'Found a mode propagating in the wrong direction! Sz_tavg={}'.format(Sz_tavg) - - energy = epsilon * e.conj() * e - - norm_amplitude = 1 / numpy.sqrt(Sz_tavg) - norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric - - # Try to break symmetry to assign a consistent sign [experimental TODO] - E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape) - sign = numpy.sign(E_weighted[:, :max(shape[0]//2, 1), :max(shape[1]//2, 1)].real.sum()) - - norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) - - e *= norm_factor - h *= norm_factor - - return e, h - - -def exy2h(wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None - ) -> sparse.spmatrix: - """ - Operator which transforms the vector e_xy containing the vectorized E_x and E_y fields, - into a vectorized H containing all three H components - - :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` - :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param epsilon: Vectorized dielectric constant grid - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Sparse matrix representing the operator - """ - e2hop = e2h(wavenumber=wavenumber, omega=omega, dxes=dxes, mu=mu) - return e2hop @ exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) - - -def hxy2e(wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None - ) -> sparse.spmatrix: - """ - Operator which transforms the vector h_xy containing the vectorized H_x and H_y fields, - into a vectorized E containing all three E components - - :param wavenumber: Wavenumber satisfying `operator_h(...) @ h_xy == wavenumber**2 * h_xy` - :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param epsilon: Vectorized dielectric constant grid - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Sparse matrix representing the operator - """ - h2eop = h2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon) - return h2eop @ hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) - - -def hxy2h(wavenumber: complex, - dxes: dx_lists_t, - mu: vfield_t = None - ) -> sparse.spmatrix: - """ - Operator which transforms the vector h_xy containing the vectorized H_x and H_y fields, - into a vectorized H containing all three H components - - :param wavenumber: Wavenumber satisfying `operator_h(...) @ h_xy == wavenumber**2 * h_xy` - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Sparse matrix representing the operator - """ - Dfx, Dfy = operators.deriv_forward(dxes[0]) - hxy2hz = sparse.hstack((Dfx, Dfy)) / (1j * wavenumber) - - if not numpy.any(numpy.equal(mu, None)): - mu_parts = numpy.split(mu, 3) - mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) - mu_z_inv = sparse.diags(1 / mu_parts[2]) - - hxy2hz = mu_z_inv @ hxy2hz @ mu_xy - - n_pts = dxes[1][0].size * dxes[1][1].size - op = sparse.vstack((sparse.eye(2 * n_pts), - hxy2hz)) - return op - - -def exy2e(wavenumber: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - ) -> sparse.spmatrix: - """ - Operator which transforms the vector e_xy containing the vectorized E_x and E_y fields, - into a vectorized E containing all three E components - - :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param epsilon: Vectorized dielectric constant grid - :return: Sparse matrix representing the operator - """ - Dbx, Dby = operators.deriv_back(dxes[1]) - exy2ez = sparse.hstack((Dbx, Dby)) / (1j * wavenumber) - - if not numpy.any(numpy.equal(epsilon, None)): - epsilon_parts = numpy.split(epsilon, 3) - epsilon_xy = sparse.diags(numpy.hstack((epsilon_parts[0], epsilon_parts[1]))) - epsilon_z_inv = sparse.diags(1 / epsilon_parts[2]) - - exy2ez = epsilon_z_inv @ exy2ez @ epsilon_xy - - n_pts = dxes[0][0].size * dxes[0][1].size - op = sparse.vstack((sparse.eye(2 * n_pts), - exy2ez)) - return op - - -def e2h(wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - mu: vfield_t = None - ) -> sparse.spmatrix: - """ - Returns an operator which, when applied to a vectorized E eigenfield, produces - the vectorized H eigenfield. - - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Sparse matrix representation of the operator - """ - op = curl_e(wavenumber, dxes) / (-1j * omega) - if not numpy.any(numpy.equal(mu, None)): - op = sparse.diags(1 / mu) @ op - return op - - -def h2e(wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t - ) -> sparse.spmatrix: - """ - Returns an operator which, when applied to a vectorized H eigenfield, produces - the vectorized E eigenfield. - - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param epsilon: Vectorized dielectric constant grid - :return: Sparse matrix representation of the operator - """ - op = sparse.diags(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes) - return op - - -def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: - """ - Discretized curl operator for use with the waveguide E field. - - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :return: Sparse matrix representation of the operator - """ - n = 1 - for d in dxes[0]: - n *= len(d) - - Bz = -1j * wavenumber * sparse.eye(n) - Dfx, Dfy = operators.deriv_forward(dxes[0]) - return operators.cross([Dfx, Dfy, Bz]) - - -def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: - """ - Discretized curl operator for use with the waveguide H field. - - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :return: Sparse matrix representation of the operator - """ - n = 1 - for d in dxes[1]: - n *= len(d) - - Bz = -1j * wavenumber * sparse.eye(n) - Dbx, Dby = operators.deriv_back(dxes[1]) - return operators.cross([Dbx, Dby, Bz]) - - -def h_err(h: vfield_t, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None - ) -> float: - """ - Calculates the relative error in the H field - - :param h: Vectorized H field - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param epsilon: Vectorized dielectric constant grid - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Relative error norm(OP @ h) / norm(h) - """ - ce = curl_e(wavenumber, dxes) - ch = curl_h(wavenumber, dxes) - - eps_inv = sparse.diags(1 / epsilon) - - if numpy.any(numpy.equal(mu, None)): - op = ce @ eps_inv @ ch @ h - omega ** 2 * h - else: - op = ce @ eps_inv @ ch @ h - omega ** 2 * (mu * h) - - return norm(op) / norm(h) - - -def e_err(e: vfield_t, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None - ) -> float: - """ - Calculates the relative error in the E field - - :param e: Vectorized E field - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param epsilon: Vectorized dielectric constant grid - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Relative error norm(OP @ e) / norm(e) - """ - ce = curl_e(wavenumber, dxes) - ch = curl_h(wavenumber, dxes) - - if numpy.any(numpy.equal(mu, None)): - op = ch @ ce @ e - omega ** 2 * (epsilon * e) - else: - mu_inv = sparse.diags(1 / mu) - op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e) - - return norm(op) / norm(e) - - -def cylindrical_operator(omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - r0: float, - ) -> sparse.spmatrix: - """ - Cylindrical coordinate waveguide operator of the form - - TODO - - for use with a field vector of the form [E_r, E_y]. - - This operator can be used to form an eigenvalue problem of the form - A @ [E_r, E_y] = wavenumber**2 * [E_r, E_y] - - which can then be solved for the eigenmodes of the system (an exp(-i * wavenumber * theta) - theta-dependence is assumed for the fields). - - :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) - :param epsilon: Vectorized dielectric constant grid - :param r0: Radius of curvature for the simulation. This should be the minimum value of - r within the simulation domain. - :return: Sparse matrix representation of the operator - """ - - Dfx, Dfy = operators.deriv_forward(dxes[0]) - Dbx, Dby = operators.deriv_back(dxes[1]) - - rx = r0 + numpy.cumsum(dxes[0][0]) - ry = r0 + dxes[0][0]/2.0 + numpy.cumsum(dxes[1][0]) - tx = rx/r0 - ty = ry/r0 - - Tx = sparse.diags(vec(tx[:, None].repeat(dxes[0][1].size, axis=1))) - Ty = sparse.diags(vec(ty[:, None].repeat(dxes[1][1].size, axis=1))) - - eps_parts = numpy.split(epsilon, 3) - eps_x = sparse.diags(eps_parts[0]) - eps_y = sparse.diags(eps_parts[1]) - eps_z_inv = sparse.diags(1 / eps_parts[2]) - - pa = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dbx, Dby)) - pb = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dby, Dbx)) - a0 = Ty @ eps_x + omega**-2 * Dby @ Ty @ Dfy - a1 = Tx @ eps_y + omega**-2 * Dbx @ Ty @ Dfx - b0 = Dbx @ Ty @ Dfy - b1 = Dby @ Ty @ Dfx - - diag = sparse.block_diag - op = (omega**2 * diag((Tx, Ty)) + pa) @ diag((a0, a1)) + \ - - (sparse.bmat(((None, Ty), (Tx, None))) + omega**-2 * pb) @ diag((b0, b1)) - - return op - diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py new file mode 100644 index 0000000..6a7f24f --- /dev/null +++ b/meanas/fdfd/waveguide_2d.py @@ -0,0 +1,607 @@ +""" +Operators and helper functions for waveguides with unchanging cross-section. + +The propagation direction is chosen to be along the z axis, and all fields +are given an implicit z-dependence of the form `exp(-1 * wavenumber * z)`. + +As the z-dependence is known, all the functions in this file assume a 2D grid + (i.e. `dxes = [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dx_h_0, ...], [dy_h_0, ...]]]`). +""" +# TODO update module docs + +from typing import List, Tuple +import numpy +from numpy.linalg import norm +import scipy.sparse as sparse + +from .. import vec, unvec, dx_lists_t, field_t, vfield_t +from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration +from . import operators + + +__author__ = 'Jan Petykiewicz' + + +def operator_e(omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + ) -> sparse.spmatrix: + """ + Waveguide operator of the form + + omega**2 * mu * epsilon + + mu * [[-Dy], [Dx]] / mu * [-Dy, Dx] + + [[Dx], [Dy]] / epsilon * [Dx, Dy] * epsilon + + for use with a field vector of the form `cat([E_x, E_y])`. + + More precisely, the operator is + $$ \\omega^2 \\mu_{yx} \\epsilon_{xy} + + \\mu_{yx} \\begin{bmatrix} -D_{by} \\\\ + D_{bx} \\end{bmatrix} \\mu_z^{-1} + \\begin{bmatrix} -D_{fy} & D_{fx} \\end{bmatrix} + + \\begin{bmatrix} D_{fx} \\\\ + D_{fy} \\end{bmatrix} \\epsilon_z^{-1} + \\begin{bmatrix} D_{bx} & D_{by} \\end{bmatrix} \\epsilon_{xy} $$ + + where + \\( \\epsilon_{xy} = \\begin{bmatrix} + \\epsilon_x & 0 \\\\ + 0 & \\epsilon_y + \\end{bmatrix} \\), + \\( \\mu_{yx} = \\begin{bmatrix} + \\mu_y & 0 \\\\ + 0 & \\mu_x + \\end{bmatrix} \\), + \\( D_{fx} \\) and \\( D_{bx} \\) are the forward and backward derivatives along x, + and each \\( \\epsilon_x, \\mu_y, \\) etc. is a diagonal matrix representing + + + This operator can be used to form an eigenvalue problem of the form + `operator_e(...) @ [E_x, E_y] = wavenumber**2 * [E_x, E_y]` + + which can then be solved for the eigenmodes of the system (an `exp(-i * wavenumber * z)` + z-dependence is assumed for the fields). + + Args: + omega: The angular frequency of the system. + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + mu: Vectorized magnetic permeability grid (default 1 everywhere) + + Returns: + Sparse matrix representation of the operator. + """ + if numpy.any(numpy.equal(mu, None)): + mu = numpy.ones_like(epsilon) + + Dfx, Dfy = operators.deriv_forward(dxes[0]) + Dbx, Dby = operators.deriv_back(dxes[1]) + + eps_parts = numpy.split(epsilon, 3) + eps_xy = sparse.diags(numpy.hstack((eps_parts[0], eps_parts[1]))) + eps_z_inv = sparse.diags(1 / eps_parts[2]) + + mu_parts = numpy.split(mu, 3) + mu_yx = sparse.diags(numpy.hstack((mu_parts[1], mu_parts[0]))) + mu_z_inv = sparse.diags(1 / mu_parts[2]) + + op = omega * omega * mu_yx @ eps_xy + \ + mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx)) + \ + sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy + return op + + +def operator_h(omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + ) -> sparse.spmatrix: + """ + Waveguide operator of the form + + omega**2 * epsilon * mu + + epsilon * [[-Dy], [Dx]] / epsilon * [-Dy, Dx] + + [[Dx], [Dy]] / mu * [Dx, Dy] * mu + + for use with a field vector of the form `cat([H_x, H_y])`. + + More precisely, the operator is + $$ \\omega^2 \\epsilon_{yx} \\mu_{xy} + + \\epsilon_{yx} \\begin{bmatrix} -D_{fy} \\\\ + D_{fx} \\end{bmatrix} \\epsilon_z^{-1} + \\begin{bmatrix} -D_{by} & D_{bx} \\end{bmatrix} + + \\begin{bmatrix} D_{bx} \\\\ + D_{by} \\end{bmatrix} \\mu_z^{-1} + \\begin{bmatrix} D_{fx} & D_{fy} \\end{bmatrix} \\mu_{xy} $$ + + where + \\( \\epsilon_{yx} = \\begin{bmatrix} + \\epsilon_y & 0 \\\\ + 0 & \\epsilon_x + \\end{bmatrix} \\), + \\( \\mu_{xy} = \\begin{bmatrix} + \\mu_x & 0 \\\\ + 0 & \\mu_y + \\end{bmatrix} \\), + \\( D_{fx} \\) and \\( D_{bx} \\) are the forward and backward derivatives along x, + and each \\( \\epsilon_x, \\mu_y, \\) etc. is a diagonal matrix. + + + This operator can be used to form an eigenvalue problem of the form + `operator_h(...) @ [H_x, H_y] = wavenumber**2 * [H_x, H_y]` + + which can then be solved for the eigenmodes of the system (an `exp(-i * wavenumber * z)` + z-dependence is assumed for the fields). + + Args: + omega: The angular frequency of the system. + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + mu: Vectorized magnetic permeability grid (default 1 everywhere) + + Returns: + Sparse matrix representation of the operator. + """ + if numpy.any(numpy.equal(mu, None)): + mu = numpy.ones_like(epsilon) + + Dfx, Dfy = operators.deriv_forward(dxes[0]) + Dbx, Dby = operators.deriv_back(dxes[1]) + + eps_parts = numpy.split(epsilon, 3) + eps_yx = sparse.diags(numpy.hstack((eps_parts[1], eps_parts[0]))) + eps_z_inv = sparse.diags(1 / eps_parts[2]) + + mu_parts = numpy.split(mu, 3) + mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) + mu_z_inv = sparse.diags(1 / mu_parts[2]) + + op = omega * omega * eps_yx @ mu_xy + \ + eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx)) + \ + sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy + + return op + + +def normalized_fields_e(e_xy: numpy.ndarray, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + prop_phase: float = 0, + ) -> Tuple[vfield_t, vfield_t]: + """ + Given a vector `e_xy` containing the vectorized E_x and E_y fields, + returns normalized, vectorized E and H fields for the system. + + Args: + e_xy: Vector containing E_x and E_y fields + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` + omega: The angular frequency of the system + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + mu: Vectorized magnetic permeability grid (default 1 everywhere) + prop_phase: Phase shift `(dz * corrected_wavenumber)` over 1 cell in propagation direction. + Default 0 (continuous propagation direction, i.e. dz->0). + + Returns: + `(e, h)`, where each field is vectorized, normalized, + and contains all three vector components. + """ + e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy + h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy + e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, + mu=mu, prop_phase=prop_phase) + return e_norm, h_norm + + +def normalized_fields_h(h_xy: numpy.ndarray, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + prop_phase: float = 0, + ) -> Tuple[vfield_t, vfield_t]: + """ + Given a vector `h_xy` containing the vectorized H_x and H_y fields, + returns normalized, vectorized E and H fields for the system. + + Args: + h_xy: Vector containing H_x and H_y fields + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_h() @ h_xy == wavenumber**2 * h_xy` + omega: The angular frequency of the system + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + mu: Vectorized magnetic permeability grid (default 1 everywhere) + prop_phase: Phase shift `(dz * corrected_wavenumber)` over 1 cell in propagation direction. + Default 0 (continuous propagation direction, i.e. dz->0). + + Returns: + `(e, h)`, where each field is vectorized, normalized, + and contains all three vector components. + """ + e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy + h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy + e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, + mu=mu, prop_phase=prop_phase) + return e_norm, h_norm + + +def _normalized_fields(e: numpy.ndarray, + h: numpy.ndarray, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + prop_phase: float = 0, + ) -> Tuple[vfield_t, vfield_t]: + # TODO documentation + shape = [s.size for s in dxes[0]] + dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)] + + E = unvec(e, shape) + H = unvec(h, shape) + + # Find time-averaged Sz and normalize to it + # H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting + phase = numpy.exp(-1j * -prop_phase / 2) + Sz_a = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0] + Sz_b = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1] + Sz_tavg = numpy.real(Sz_a.sum() - Sz_b.sum()) * 0.5 # 0.5 since E, H are assumed to be peak (not RMS) amplitudes + assert Sz_tavg > 0, 'Found a mode propagating in the wrong direction! Sz_tavg={}'.format(Sz_tavg) + + energy = epsilon * e.conj() * e + + norm_amplitude = 1 / numpy.sqrt(Sz_tavg) + norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric + + # Try to break symmetry to assign a consistent sign [experimental TODO] + E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape) + sign = numpy.sign(E_weighted[:, :max(shape[0]//2, 1), :max(shape[1]//2, 1)].real.sum()) + + norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) + + e *= norm_factor + h *= norm_factor + + return e, h + + +def exy2h(wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, + into a vectorized H containing all three H components + + Args: + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` + omega: The angular frequency of the system + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + mu: Vectorized magnetic permeability grid (default 1 everywhere) + + Returns: + Sparse matrix representing the operator. + """ + e2hop = e2h(wavenumber=wavenumber, omega=omega, dxes=dxes, mu=mu) + return e2hop @ exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) + + +def hxy2e(wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields, + into a vectorized E containing all three E components + + Args: + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_h() @ h_xy == wavenumber**2 * h_xy` + omega: The angular frequency of the system + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + mu: Vectorized magnetic permeability grid (default 1 everywhere) + + Returns: + Sparse matrix representing the operator. + """ + h2eop = h2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon) + return h2eop @ hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) + + +def hxy2h(wavenumber: complex, + dxes: dx_lists_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields, + into a vectorized H containing all three H components + + Args: + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. + It should satisfy `operator_h() @ h_xy == wavenumber**2 * h_xy` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + mu: Vectorized magnetic permeability grid (default 1 everywhere) + + Returns: + Sparse matrix representing the operator. + """ + Dfx, Dfy = operators.deriv_forward(dxes[0]) + hxy2hz = sparse.hstack((Dfx, Dfy)) / (1j * wavenumber) + + if not numpy.any(numpy.equal(mu, None)): + mu_parts = numpy.split(mu, 3) + mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) + mu_z_inv = sparse.diags(1 / mu_parts[2]) + + hxy2hz = mu_z_inv @ hxy2hz @ mu_xy + + n_pts = dxes[1][0].size * dxes[1][1].size + op = sparse.vstack((sparse.eye(2 * n_pts), + hxy2hz)) + return op + + +def exy2e(wavenumber: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + ) -> sparse.spmatrix: + """ + Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, + into a vectorized E containing all three E components + + Args: + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + + Returns: + Sparse matrix representing the operator. + """ + Dbx, Dby = operators.deriv_back(dxes[1]) + exy2ez = sparse.hstack((Dbx, Dby)) / (1j * wavenumber) + + if not numpy.any(numpy.equal(epsilon, None)): + epsilon_parts = numpy.split(epsilon, 3) + epsilon_xy = sparse.diags(numpy.hstack((epsilon_parts[0], epsilon_parts[1]))) + epsilon_z_inv = sparse.diags(1 / epsilon_parts[2]) + + exy2ez = epsilon_z_inv @ exy2ez @ epsilon_xy + + n_pts = dxes[0][0].size * dxes[0][1].size + op = sparse.vstack((sparse.eye(2 * n_pts), + exy2ez)) + return op + + +def e2h(wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Returns an operator which, when applied to a vectorized E eigenfield, produces + the vectorized H eigenfield. + + Args: + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + omega: The angular frequency of the system + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + mu: Vectorized magnetic permeability grid (default 1 everywhere) + + Returns: + Sparse matrix representation of the operator. + """ + op = curl_e(wavenumber, dxes) / (-1j * omega) + if not numpy.any(numpy.equal(mu, None)): + op = sparse.diags(1 / mu) @ op + return op + + +def h2e(wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t + ) -> sparse.spmatrix: + """ + Returns an operator which, when applied to a vectorized H eigenfield, produces + the vectorized E eigenfield. + + Args: + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + omega: The angular frequency of the system + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + + Returns: + Sparse matrix representation of the operator. + """ + op = sparse.diags(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes) + return op + + +def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: + """ + Discretized curl operator for use with the waveguide E field. + + Args: + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + + Return: + Sparse matrix representation of the operator. + """ + n = 1 + for d in dxes[0]: + n *= len(d) + + Bz = -1j * wavenumber * sparse.eye(n) + Dfx, Dfy = operators.deriv_forward(dxes[0]) + return operators.cross([Dfx, Dfy, Bz]) + + +def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: + """ + Discretized curl operator for use with the waveguide H field. + + Args: + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + + Return: + Sparse matrix representation of the operator. + """ + n = 1 + for d in dxes[1]: + n *= len(d) + + Bz = -1j * wavenumber * sparse.eye(n) + Dbx, Dby = operators.deriv_back(dxes[1]) + return operators.cross([Dbx, Dby, Bz]) + + +def h_err(h: vfield_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> float: + """ + Calculates the relative error in the H field + + Args: + h: Vectorized H field + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + omega: The angular frequency of the system + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + mu: Vectorized magnetic permeability grid (default 1 everywhere) + + Returns: + Relative error `norm(A_h @ h) / norm(h)`. + """ + ce = curl_e(wavenumber, dxes) + ch = curl_h(wavenumber, dxes) + + eps_inv = sparse.diags(1 / epsilon) + + if numpy.any(numpy.equal(mu, None)): + op = ce @ eps_inv @ ch @ h - omega ** 2 * h + else: + op = ce @ eps_inv @ ch @ h - omega ** 2 * (mu * h) + + return norm(op) / norm(h) + + +def e_err(e: vfield_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> float: + """ + Calculates the relative error in the E field + + Args: + e: Vectorized E field + wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` + omega: The angular frequency of the system + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + mu: Vectorized magnetic permeability grid (default 1 everywhere) + + Returns: + Relative error `norm(A_e @ e) / norm(e)`. + """ + ce = curl_e(wavenumber, dxes) + ch = curl_h(wavenumber, dxes) + + if numpy.any(numpy.equal(mu, None)): + op = ch @ ce @ e - omega ** 2 * (epsilon * e) + else: + mu_inv = sparse.diags(1 / mu) + op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e) + + return norm(op) / norm(e) + + +def solve_modes(mode_numbers: List[int], + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + mode_margin: int = 2, + ) -> Tuple[List[vfield_t], List[complex]]: + """ + Given a 2D region, attempts to solve for the eigenmode with the specified mode numbers. + + Args: + mode_numbers: List of 0-indexed mode numbers to solve for + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + epsilon: Dielectric constant + mu: Magnetic permeability (default 1 everywhere) + mode_margin: The eigensolver will actually solve for `(max(mode_number) + mode_margin)` + modes, but only return the target mode. Increasing this value can improve the solver's + ability to find the correct mode. Default 2. + + Returns: + (e_xys, wavenumbers) + """ + + ''' + Solve for the largest-magnitude eigenvalue of the real operator + ''' + dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] + A_r = waveguide.operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) + + eigvals, eigvecs = signed_eigensolve(A_r, max(mode_number) + mode_margin) + e_xys = eigvecs[:, -(numpy.array(mode_number) + 1)] + + ''' + Now solve for the eigenvector of the full operator, using the real operator's + eigenvector as an initial guess for Rayleigh quotient iteration. + ''' + A = waveguide.operator_e(omega, dxes, epsilon, mu) + eigvals, e_xys = rayleigh_quotient_iteration(A, e_xys) + + # Calculate the wave-vector (force the real part to be positive) + wavenumbers = numpy.sqrt(eigvals) + wavenumbers *= numpy.sign(numpy.real(wavenumbers)) + + return e_xys, wavenumbers + + +def solve_mode(mode_number: int, + *args, + **kwargs + ) -> Tuple[vfield_t, complex]: + """ + Wrapper around `solve_modes()` that solves for a single mode. + + Args: + mode_number: 0-indexed mode number to solve for + *args: passed to `solve_modes()` + **kwargs: passed to `solve_modes()` + + Returns: + (e_xy, wavenumber) + """ + return solve_modes(mode_numbers=[mode_number], *args, **kwargs) diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py new file mode 100644 index 0000000..18727ce --- /dev/null +++ b/meanas/fdfd/waveguide_3d.py @@ -0,0 +1,236 @@ +""" +Tools for working with waveguide modes in 3D domains. + +This module relies heavily on `waveguide_2d` and mostly just transforms +its parameters into 2D equivalents and expands the results back into 3D. +""" +from typing import Dict, List, Tuple +import numpy +import scipy.sparse as sparse + +from .. import vec, unvec, dx_lists_t, vfield_t, field_t +from . import operators, waveguide_2d, functional + + +def solve_mode(mode_number: int, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + epsilon: field_t, + mu: field_t = None, + ) -> Dict[str, complex or numpy.ndarray]: + """ + Given a 3D grid, selects a slice from the grid and attempts to + solve for an eigenmode propagating through that slice. + + Args: + mode_number: Number of the mode, 0-indexed + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + axis: Propagation axis (0=x, 1=y, 2=z) + polarity: Propagation direction (+1 for +ve, -1 for -ve) + slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use + as the waveguide cross-section. `slices[axis]` should select only one item. + epsilon: Dielectric constant + mu: Magnetic permeability (default 1 everywhere) + + Returns: + `{'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex}` + """ + if mu is None: + mu = numpy.ones_like(epsilon) + + slices = tuple(slices) + + ''' + Solve the 2D problem in the specified plane + ''' + # Define rotation to set z as propagation direction + order = numpy.roll(range(3), 2 - axis) + reverse_order = numpy.roll(range(3), axis - 2) + + # Find dx in propagation direction + dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes]) + dx_prop = 0.5 * sum(dxab_forward)[0] + + # Reduce to 2D and solve the 2D problem + args_2d = { + 'omega': omega, + 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], + 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), + 'mu': vec([mu[i][slices].transpose(order) for i in order]), + } + e_xy, wavenumber_2d = waveguide_2d.solve_mode(mode_number, **args_2d) + + ''' + Apply corrections and expand to 3D + ''' + # Correct wavenumber to account for numerical dispersion. + wavenumber = 2/dx_prop * numpy.arcsin(wavenumber_2d * dx_prop/2) + + shape = [d.size for d in args_2d['dxes'][0]] + ve, vh = waveguide.normalized_fields_e(e_xy, wavenumber=wavenumber_2d, **args_2d, prop_phase=dx_prop * wavenumber) + e = unvec(ve, shape) + h = unvec(vh, shape) + + # Adjust for propagation direction + h *= polarity + + # Apply phase shift to H-field + h[:2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop) + e[2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop) + + # Expand E, H to full epsilon space we were given + E = numpy.zeros_like(epsilon, dtype=complex) + H = numpy.zeros_like(epsilon, dtype=complex) + for a, o in enumerate(reverse_order): + E[(a, *slices)] = e[o][:, :, None].transpose(reverse_order) + H[(a, *slices)] = h[o][:, :, None].transpose(reverse_order) + + results = { + 'wavenumber': wavenumber, + 'wavenumber_2d': wavenumber_2d, + 'H': H, + 'E': E, + } + return results + + +def compute_source(E: field_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + epsilon: field_t, + mu: field_t = None, + ) -> field_t: + """ + Given an eigenmode obtained by `solve_mode`, returns the current source distribution + necessary to position a unidirectional source at the slice location. + + Args: + E: E-field of the mode + wavenumber: Wavenumber of the mode + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + axis: Propagation axis (0=x, 1=y, 2=z) + polarity: Propagation direction (+1 for +ve, -1 for -ve) + slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use + as the waveguide cross-section. `slices[axis]` should select only one item. + mu: Magnetic permeability (default 1 everywhere) + + Returns: + J distribution for the unidirectional source + """ + E_expanded = expand_e(E=E, dxes=dxes, wavenumber=wavenumber, axis=axis, + polarity=polarity, slices=slices) + + smask = [slice(None)] * 4 + if polarity > 0: + smask[axis + 1] = slice(slices[axis].start, None) + else: + smask[axis + 1] = slice(None, slices[axis].stop) + + mask = numpy.zeros_like(E_expanded, dtype=int) + mask[tuple(smask)] = 1 + + masked_e2j = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon), mu=vec(mu)) + J = unvec(masked_e2j @ vec(E_expanded), E.shape[1:]) + return J + + +def compute_overlap_e(E: field_t, + wavenumber: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + ) -> field_t: # TODO DOCS + """ + Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the + mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) + [assumes reflection symmetry]. + + Args: + E: E-field of the mode + H: H-field of the mode (advanced by half of a Yee cell from E) + wavenumber: Wavenumber of the mode + omega: Angular frequency of the simulation + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + axis: Propagation axis (0=x, 1=y, 2=z) + polarity: Propagation direction (+1 for +ve, -1 for -ve) + slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] should select only one item. + mu: Magnetic permeability (default 1 everywhere) + + Returns: + overlap_e such that `numpy.sum(overlap_e * other_e)` computes the overlap integral + """ + slices = tuple(slices) + + Ee = expand_e(E=E, wavenumber=wavenumber, dxes=dxes, + axis=axis, polarity=polarity, slices=slices) + + start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity)) + + slices2 = list(slices) + slices2[axis] = slice(start, stop) + slices2 = (slice(None), *slices2) + + Etgt = numpy.zeros_like(Ee) + Etgt[slices2] = Ee[slices2] + + Etgt /= (Etgt.conj() * Etgt).sum() + return Etgt + + +def expand_e(E: field_t, + wavenumber: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + ) -> field_t: + """ + Given an eigenmode obtained by `solve_mode`, expands the E-field from the 2D + slice where the mode was calculated to the entire domain (along the propagation + axis). This assumes the epsilon cross-section remains constant throughout the + entire domain; it is up to the caller to truncate the expansion to any regions + where it is valid. + + Args: + E: E-field of the mode + wavenumber: Wavenumber of the mode + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + axis: Propagation axis (0=x, 1=y, 2=z) + polarity: Propagation direction (+1 for +ve, -1 for -ve) + slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] should select only one item. + + Returns: + `E`, with the original field expanded along the specified `axis`. + """ + slices = tuple(slices) + + # Determine phase factors for parallel slices + a_shape = numpy.roll([1, -1, 1, 1], axis) + a_E = numpy.real(dxes[0][axis]).cumsum() + r_E = a_E - a_E[slices[axis]] + iphi = polarity * -1j * wavenumber + phase_E = numpy.exp(iphi * r_E).reshape(a_shape) + + # Expand our slice to the entire grid using the phase factors + E_expanded = numpy.zeros_like(E) + + slices_exp = list(slices) + slices_exp[axis] = slice(E.shape[axis + 1]) + slices_exp = (slice(None), *slices_exp) + + slices_in = (slice(None), *slices) + + E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in] + return E_expanded diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py new file mode 100644 index 0000000..ebfb41d --- /dev/null +++ b/meanas/fdfd/waveguide_cyl.py @@ -0,0 +1,138 @@ +""" +Operators and helper functions for cylindrical waveguides with unchanging cross-section. + +WORK IN PROGRESS, CURRENTLY BROKEN + +As the z-dependence is known, all the functions in this file assume a 2D grid + (i.e. `dxes = [[[dr_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`). +""" +# TODO update module docs + +from typing import List, Tuple, Dict +import numpy +from numpy.linalg import norm +import scipy.sparse as sparse + +from .. import vec, unvec, dx_lists_t, field_t, vfield_t +from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration +from . import operators + + +__author__ = 'Jan Petykiewicz' + + +def cylindrical_operator(omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + r0: float, + ) -> sparse.spmatrix: + """ + Cylindrical coordinate waveguide operator of the form + + TODO + + for use with a field vector of the form `[E_r, E_y]`. + + This operator can be used to form an eigenvalue problem of the form + A @ [E_r, E_y] = wavenumber**2 * [E_r, E_y] + + which can then be solved for the eigenmodes of the system + (an `exp(-i * wavenumber * theta)` theta-dependence is assumed for the fields). + + Args: + omega: The angular frequency of the system + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + epsilon: Vectorized dielectric constant grid + r0: Radius of curvature for the simulation. This should be the minimum value of + r within the simulation domain. + + Returns: + Sparse matrix representation of the operator + """ + + Dfx, Dfy = operators.deriv_forward(dxes[0]) + Dbx, Dby = operators.deriv_back(dxes[1]) + + rx = r0 + numpy.cumsum(dxes[0][0]) + ry = r0 + dxes[0][0]/2.0 + numpy.cumsum(dxes[1][0]) + tx = rx/r0 + ty = ry/r0 + + Tx = sparse.diags(vec(tx[:, None].repeat(dxes[0][1].size, axis=1))) + Ty = sparse.diags(vec(ty[:, None].repeat(dxes[1][1].size, axis=1))) + + eps_parts = numpy.split(epsilon, 3) + eps_x = sparse.diags(eps_parts[0]) + eps_y = sparse.diags(eps_parts[1]) + eps_z_inv = sparse.diags(1 / eps_parts[2]) + + pa = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dbx, Dby)) + pb = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dby, Dbx)) + a0 = Ty @ eps_x + omega**-2 * Dby @ Ty @ Dfy + a1 = Tx @ eps_y + omega**-2 * Dbx @ Ty @ Dfx + b0 = Dbx @ Ty @ Dfy + b1 = Dby @ Ty @ Dfx + + diag = sparse.block_diag + op = (omega**2 * diag((Tx, Ty)) + pa) @ diag((a0, a1)) + \ + - (sparse.bmat(((None, Ty), (Tx, None))) + omega**-2 * pb) @ diag((b0, b1)) + + return op + + +def solve_mode(mode_number: int, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + r0: float, + ) -> Dict[str, complex or field_t]: + """ + TODO: fixup + Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode + of the bent waveguide with the specified mode number. + + Args: + mode_number: Number of the mode, 0-indexed + omega: Angular frequency of the simulation + dxes: Grid parameters [dx_e, dx_h] as described in meanas.types. + The first coordinate is assumed to be r, the second is y. + epsilon: Dielectric constant + r0: Radius of curvature for the simulation. This should be the minimum value of + r within the simulation domain. + + Returns: + `{'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex}` + """ + + ''' + Solve for the largest-magnitude eigenvalue of the real operator + ''' + dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] + + A_r = waveguide.cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0) + eigvals, eigvecs = signed_eigensolve(A_r, mode_number + 3) + e_xy = eigvecs[:, -(mode_number+1)] + + ''' + Now solve for the eigenvector of the full operator, using the real operator's + eigenvector as an initial guess for Rayleigh quotient iteration. + ''' + A = waveguide.cylindrical_operator(omega, dxes, epsilon, r0) + eigval, e_xy = rayleigh_quotient_iteration(A, e_xy) + + # Calculate the wave-vector (force the real part to be positive) + wavenumber = numpy.sqrt(eigval) + wavenumber *= numpy.sign(numpy.real(wavenumber)) + + # TODO: Perform correction on wavenumber to account for numerical dispersion. + + shape = [d.size for d in dxes[0]] + e_xy = numpy.hstack((e_xy, numpy.zeros(shape[0] * shape[1]))) + fields = { + 'wavenumber': wavenumber, + 'E': unvec(e_xy, shape), +# 'E': unvec(e, shape), +# 'H': unvec(h, shape), + } + + return fields diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py deleted file mode 100644 index 4318cc0..0000000 --- a/meanas/fdfd/waveguide_mode.py +++ /dev/null @@ -1,307 +0,0 @@ -from typing import Dict, List, Tuple -import numpy -import scipy.sparse as sparse - -from .. import vec, unvec, dx_lists_t, vfield_t, field_t -from . import operators, waveguide, functional -from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration - - -def vsolve_waveguide_mode_2d(mode_number: int, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - mode_margin: int = 2, - ) -> Tuple[vfield_t, complex]: - """ - Given a 2d region, attempts to solve for the eigenmode with the specified mode number. - - :param mode_number: Number of the mode, 0-indexed. - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param epsilon: Dielectric constant - :param mu: Magnetic permeability (default 1 everywhere) - :param mode_margin: The eigensolver will actually solve for (mode_number + mode_margin) - modes, but only return the target mode. Increasing this value can improve the solver's - ability to find the correct mode. Default 2. - :return: (e_xy, wavenumber) - """ - - ''' - Solve for the largest-magnitude eigenvalue of the real operator - ''' - dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] - A_r = waveguide.operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) - - eigvals, eigvecs = signed_eigensolve(A_r, mode_number + mode_margin) - e_xy = eigvecs[:, -(mode_number + 1)] - - ''' - Now solve for the eigenvector of the full operator, using the real operator's - eigenvector as an initial guess for Rayleigh quotient iteration. - ''' - A = waveguide.operator_e(omega, dxes, epsilon, mu) - eigval, e_xy = rayleigh_quotient_iteration(A, e_xy) - - # Calculate the wave-vector (force the real part to be positive) - wavenumber = numpy.sqrt(eigval) - wavenumber *= numpy.sign(numpy.real(wavenumber)) - - return e_xy, wavenumber - - - -def solve_waveguide_mode(mode_number: int, - omega: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - epsilon: field_t, - mu: field_t = None, - ) -> Dict[str, complex or numpy.ndarray]: - """ - Given a 3D grid, selects a slice from the grid and attempts to - solve for an eigenmode propagating through that slice. - - :param mode_number: Number of the mode, 0-indexed - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param axis: Propagation axis (0=x, 1=y, 2=z) - :param polarity: Propagation direction (+1 for +ve, -1 for -ve) - :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use - as the waveguide cross-section. slices[axis] should select only one - :param epsilon: Dielectric constant - :param mu: Magnetic permeability (default 1 everywhere) - :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} - """ - if mu is None: - mu = numpy.ones_like(epsilon) - - slices = tuple(slices) - - ''' - Solve the 2D problem in the specified plane - ''' - # Define rotation to set z as propagation direction - order = numpy.roll(range(3), 2 - axis) - reverse_order = numpy.roll(range(3), axis - 2) - - # Find dx in propagation direction - dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes]) - dx_prop = 0.5 * sum(dxab_forward)[0] - - # Reduce to 2D and solve the 2D problem - args_2d = { - 'omega': omega, - 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], - 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), - 'mu': vec([mu[i][slices].transpose(order) for i in order]), - } - e_xy, wavenumber_2d = vsolve_waveguide_mode_2d(mode_number, **args_2d) - - ''' - Apply corrections and expand to 3D - ''' - # Correct wavenumber to account for numerical dispersion. - wavenumber = 2/dx_prop * numpy.arcsin(wavenumber_2d * dx_prop/2) - print(wavenumber_2d / wavenumber) - - shape = [d.size for d in args_2d['dxes'][0]] - ve, vh = waveguide.normalized_fields_e(e_xy, wavenumber=wavenumber_2d, **args_2d, prop_phase=dx_prop * wavenumber) - e = unvec(ve, shape) - h = unvec(vh, shape) - - # Adjust for propagation direction - h *= polarity - - # Apply phase shift to H-field - h[:2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop) - e[2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop) - - # Expand E, H to full epsilon space we were given - E = numpy.zeros_like(epsilon, dtype=complex) - H = numpy.zeros_like(epsilon, dtype=complex) - for a, o in enumerate(reverse_order): - E[(a, *slices)] = e[o][:, :, None].transpose(reverse_order) - H[(a, *slices)] = h[o][:, :, None].transpose(reverse_order) - - results = { - 'wavenumber': wavenumber, - 'wavenumber_2d': wavenumber_2d, - 'H': H, - 'E': E, - } - return results - - -def compute_source(E: field_t, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - epsilon: field_t, - mu: field_t = None, - ) -> field_t: - """ - Given an eigenmode obtained by solve_waveguide_mode, returns the current source distribution - necessary to position a unidirectional source at the slice location. - - :param E: E-field of the mode - :param wavenumber: Wavenumber of the mode - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param axis: Propagation axis (0=x, 1=y, 2=z) - :param polarity: Propagation direction (+1 for +ve, -1 for -ve) - :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use - as the waveguide cross-section. slices[axis] should select only one - :param mu: Magnetic permeability (default 1 everywhere) - :return: J distribution for the unidirectional source - """ - E_expanded = expand_wgmode_e(E=E, dxes=dxes, wavenumber=wavenumber, axis=axis, - polarity=polarity, slices=slices) - - smask = [slice(None)] * 4 - if polarity > 0: - smask[axis + 1] = slice(slices[axis].start, None) - else: - smask[axis + 1] = slice(None, slices[axis].stop) - - mask = numpy.zeros_like(E_expanded, dtype=int) - mask[tuple(smask)] = 1 - - masked_e2j = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon), mu=vec(mu)) - J = unvec(masked_e2j @ vec(E_expanded), E.shape[1:]) - return J - - -def compute_overlap_e(E: field_t, - wavenumber: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - ) -> field_t: # TODO DOCS - """ - Given an eigenmode obtained by solve_waveguide_mode, calculates overlap_e for the - mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) - [assumes reflection symmetry].i - - overlap_e makes use of the e2h operator to collapse the above expression into - (vec(E) @ vec(overlap_e)), allowing for simple calculation of the mode overlap. - - :param E: E-field of the mode - :param H: H-field of the mode (advanced by half of a Yee cell from E) - :param wavenumber: Wavenumber of the mode - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :param axis: Propagation axis (0=x, 1=y, 2=z) - :param polarity: Propagation direction (+1 for +ve, -1 for -ve) - :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use - as the waveguide cross-section. slices[axis] should select only one - :param mu: Magnetic permeability (default 1 everywhere) - :return: overlap_e for calculating the mode overlap - """ - slices = tuple(slices) - - Ee = expand_wgmode_e(E=E, wavenumber=wavenumber, dxes=dxes, - axis=axis, polarity=polarity, slices=slices) - - start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity)) - - slices2 = list(slices) - slices2[axis] = slice(start, stop) - slices2 = (slice(None), *slices2) - - Etgt = numpy.zeros_like(Ee) - Etgt[slices2] = Ee[slices2] - - Etgt /= (Etgt.conj() * Etgt).sum() - return Etgt - - -def solve_waveguide_mode_cylindrical(mode_number: int, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - r0: float, - ) -> Dict[str, complex or field_t]: - """ - TODO: fixup - Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode - of the bent waveguide with the specified mode number. - - :param mode_number: Number of the mode, 0-indexed - :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types. - The first coordinate is assumed to be r, the second is y. - :param epsilon: Dielectric constant - :param r0: Radius of curvature for the simulation. This should be the minimum value of - r within the simulation domain. - :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} - """ - - ''' - Solve for the largest-magnitude eigenvalue of the real operator - ''' - dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] - - A_r = waveguide.cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0) - eigvals, eigvecs = signed_eigensolve(A_r, mode_number + 3) - e_xy = eigvecs[:, -(mode_number+1)] - - ''' - Now solve for the eigenvector of the full operator, using the real operator's - eigenvector as an initial guess for Rayleigh quotient iteration. - ''' - A = waveguide.cylindrical_operator(omega, dxes, epsilon, r0) - eigval, e_xy = rayleigh_quotient_iteration(A, e_xy) - - # Calculate the wave-vector (force the real part to be positive) - wavenumber = numpy.sqrt(eigval) - wavenumber *= numpy.sign(numpy.real(wavenumber)) - - # TODO: Perform correction on wavenumber to account for numerical dispersion. - - shape = [d.size for d in dxes[0]] - e_xy = numpy.hstack((e_xy, numpy.zeros(shape[0] * shape[1]))) - fields = { - 'wavenumber': wavenumber, - 'E': unvec(e_xy, shape), -# 'E': unvec(e, shape), -# 'H': unvec(h, shape), - } - - return fields - - -def expand_wgmode_e(E: field_t, - wavenumber: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - ) -> field_t: - slices = tuple(slices) - - # Determine phase factors for parallel slices - a_shape = numpy.roll([1, -1, 1, 1], axis) - a_E = numpy.real(dxes[0][axis]).cumsum() - r_E = a_E - a_E[slices[axis]] - iphi = polarity * -1j * wavenumber - phase_E = numpy.exp(iphi * r_E).reshape(a_shape) - - # Expand our slice to the entire grid using the phase factors - E_expanded = numpy.zeros_like(E) - - slices_exp = list(slices) - slices_exp[axis] = slice(E.shape[axis + 1]) - slices_exp = (slice(None), *slices_exp) - - slices_in = (slice(None), *slices) - - E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in] - return E_expanded diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py new file mode 100644 index 0000000..8593d4a --- /dev/null +++ b/meanas/fdmath/functional.py @@ -0,0 +1,109 @@ +""" +Math functions for finite difference simulations + +Basic discrete calculus etc. +""" +from typing import List, Callable, Tuple, Dict +import numpy + +from .. import field_t, field_updater + + +def deriv_forward(dx_e: List[numpy.ndarray] = None) -> field_updater: + """ + Utility operators for taking discretized derivatives (backward variant). + + Args: + dx_e: Lists of cell sizes for all axes + `[[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...]`. + + Returns: + List of functions for taking forward derivatives along each axis. + """ + if dx_e: + derivs = [lambda f: (numpy.roll(f, -1, axis=0) - f) / dx_e[0][:, None, None], + lambda f: (numpy.roll(f, -1, axis=1) - f) / dx_e[1][None, :, None], + lambda f: (numpy.roll(f, -1, axis=2) - f) / dx_e[2][None, None, :]] + else: + derivs = [lambda f: numpy.roll(f, -1, axis=0) - f, + lambda f: numpy.roll(f, -1, axis=1) - f, + lambda f: numpy.roll(f, -1, axis=2) - f] + return derivs + + +def deriv_back(dx_h: List[numpy.ndarray] = None) -> field_updater: + """ + Utility operators for taking discretized derivatives (forward variant). + + Args: + dx_h: Lists of cell sizes for all axes + `[[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...]`. + + Returns: + List of functions for taking forward derivatives along each axis. + """ + if dx_h: + derivs = [lambda f: (f - numpy.roll(f, 1, axis=0)) / dx_h[0][:, None, None], + lambda f: (f - numpy.roll(f, 1, axis=1)) / dx_h[1][None, :, None], + lambda f: (f - numpy.roll(f, 1, axis=2)) / dx_h[2][None, None, :]] + else: + derivs = [lambda f: f - numpy.roll(f, 1, axis=0), + lambda f: f - numpy.roll(f, 1, axis=1), + lambda f: f - numpy.roll(f, 1, axis=2)] + return derivs + + +def curl_forward(dx_e: List[numpy.ndarray] = None) -> field_updater: + """ + Curl operator for use with the E field. + + Args: + dx_e: Lists of cell sizes for all axes + `[[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...]`. + + Returns: + Function `f` for taking the discrete forward curl of a field, + `f(E)` -> curlE \\( = \\nabla_f \\times E \\) + """ + Dx, Dy, Dz = deriv_forward(dx_e) + + def ce_fun(e: field_t) -> field_t: + output = numpy.empty_like(e) + output[0] = Dy(e[2]) + output[1] = Dz(e[0]) + output[2] = Dx(e[1]) + output[0] -= Dz(e[1]) + output[1] -= Dx(e[2]) + output[2] -= Dy(e[0]) + return output + + return ce_fun + + +def curl_back(dx_h: List[numpy.ndarray] = None) -> field_updater: + """ + Create a function which takes the backward curl of a field. + + Args: + dx_h: Lists of cell sizes for all axes + `[[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...]`. + + Returns: + Function `f` for taking the discrete backward curl of a field, + `f(H)` -> curlH \\( = \\nabla_b \\times H \\) + """ + Dx, Dy, Dz = deriv_back(dx_h) + + def ch_fun(h: field_t) -> field_t: + output = numpy.empty_like(h) + output[0] = Dy(h[2]) + output[1] = Dz(h[0]) + output[2] = Dx(h[1]) + output[0] -= Dz(h[1]) + output[1] -= Dx(h[2]) + output[2] -= Dy(h[0]) + return output + + return ch_fun + + diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py new file mode 100644 index 0000000..64f04aa --- /dev/null +++ b/meanas/fdmath/operators.py @@ -0,0 +1,231 @@ +""" +Matrix operators for finite difference simulations + +Basic discrete calculus etc. +""" +from typing import List, Callable, Tuple, Dict +import numpy +import scipy.sparse as sparse + +from .. import field_t, vfield_t + + +def rotation(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmatrix: + """ + Utility operator for performing a circular shift along a specified axis by a + specified number of elements. + + Args: + axis: Axis to shift along. x=0, y=1, z=2 + shape: Shape of the grid being shifted + shift_distance: Number of cells to shift by. May be negative. Default 1. + + Returns: + Sparse matrix for performing the circular shift. + """ + if len(shape) not in (2, 3): + raise Exception('Invalid shape: {}'.format(shape)) + if axis not in range(len(shape)): + raise Exception('Invalid direction: {}, shape is {}'.format(axis, shape)) + + shifts = [abs(shift_distance) if a == axis else 0 for a in range(3)] + shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts)] + ijk = numpy.meshgrid(*shifted_diags, indexing='ij') + + n = numpy.prod(shape) + i_ind = numpy.arange(n) + j_ind = numpy.ravel_multi_index(ijk, shape, order='C') + + vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) + + d = sparse.csr_matrix(vij, shape=(n, n)) + + if shift_distance < 0: + d = d.T + + return d + + +def shift_with_mirror(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmatrix: + """ + Utility operator for performing an n-element shift along a specified axis, with mirror + boundary conditions applied to the cells beyond the receding edge. + + Args: + axis: Axis to shift along. x=0, y=1, z=2 + shape: Shape of the grid being shifted + shift_distance: Number of cells to shift by. May be negative. Default 1. + + Returns: + Sparse matrix for performing the shift-with-mirror. + """ + if len(shape) not in (2, 3): + raise Exception('Invalid shape: {}'.format(shape)) + if axis not in range(len(shape)): + raise Exception('Invalid direction: {}, shape is {}'.format(axis, shape)) + if shift_distance >= shape[axis]: + raise Exception('Shift ({}) is too large for axis {} of size {}'.format( + shift_distance, axis, shape[axis])) + + def mirrored_range(n, s): + v = numpy.arange(n) + s + v = numpy.where(v >= n, 2 * n - v - 1, v) + v = numpy.where(v < 0, - 1 - v, v) + return v + + shifts = [shift_distance if a == axis else 0 for a in range(3)] + shifted_diags = [mirrored_range(n, s) for n, s in zip(shape, shifts)] + ijk = numpy.meshgrid(*shifted_diags, indexing='ij') + + n = numpy.prod(shape) + i_ind = numpy.arange(n) + j_ind = numpy.ravel_multi_index(ijk, shape, order='C') + + vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) + + d = sparse.csr_matrix(vij, shape=(n, n)) + return d + + +def deriv_forward(dx_e: List[numpy.ndarray]) -> List[sparse.spmatrix]: + """ + Utility operators for taking discretized derivatives (forward variant). + + Args: + dx_e: Lists of cell sizes for all axes + `[[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...]`. + + Returns: + List of operators for taking forward derivatives along each axis. + """ + shape = [s.size for s in dx_e] + n = numpy.prod(shape) + + dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij') + + def deriv(axis): + return rotation(axis, shape, 1) - sparse.eye(n) + + Ds = [sparse.diags(+1 / dx.ravel(order='C')) @ deriv(a) + for a, dx in enumerate(dx_e_expanded)] + + return Ds + + +def deriv_back(dx_h: List[numpy.ndarray]) -> List[sparse.spmatrix]: + """ + Utility operators for taking discretized derivatives (backward variant). + + Args: + dx_h: Lists of cell sizes for all axes + `[[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...]`. + + Returns: + List of operators for taking forward derivatives along each axis. + """ + shape = [s.size for s in dx_h] + n = numpy.prod(shape) + + dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij') + + def deriv(axis): + return rotation(axis, shape, -1) - sparse.eye(n) + + Ds = [sparse.diags(-1 / dx.ravel(order='C')) @ deriv(a) + for a, dx in enumerate(dx_h_expanded)] + + return Ds + + +def cross(B: List[sparse.spmatrix]) -> sparse.spmatrix: + """ + Cross product operator + + Args: + B: List `[Bx, By, Bz]` of sparse matrices corresponding to the x, y, z + portions of the operator on the left side of the cross product. + + Returns: + Sparse matrix corresponding to (B x), where x is the cross product. + """ + n = B[0].shape[0] + zero = sparse.csr_matrix((n, n)) + return sparse.bmat([[zero, -B[2], B[1]], + [B[2], zero, -B[0]], + [-B[1], B[0], zero]]) + + +def vec_cross(b: vfield_t) -> sparse.spmatrix: + """ + Vector cross product operator + + Args: + b: Vector on the left side of the cross product. + + Returns: + + Sparse matrix corresponding to (b x), where x is the cross product. + + """ + B = [sparse.diags(c) for c in numpy.split(b, 3)] + return cross(B) + + +def avg_forward(axis: int, shape: List[int]) -> sparse.spmatrix: + """ + Forward average operator `(x4 = (x4 + x5) / 2)` + + Args: + axis: Axis to average along (x=0, y=1, z=2) + shape: Shape of the grid to average + + Returns: + Sparse matrix for forward average operation. + """ + if len(shape) not in (2, 3): + raise Exception('Invalid shape: {}'.format(shape)) + + n = numpy.prod(shape) + return 0.5 * (sparse.eye(n) + rotation(axis, shape)) + + +def avg_back(axis: int, shape: List[int]) -> sparse.spmatrix: + """ + Backward average operator `(x4 = (x4 + x3) / 2)` + + Args: + axis: Axis to average along (x=0, y=1, z=2) + shape: Shape of the grid to average + + Returns: + Sparse matrix for backward average operation. + """ + return avg_forward(axis, shape).T + + +def curl_forward(dx_e: List[numpy.ndarray]) -> sparse.spmatrix: + """ + Curl operator for use with the E field. + + Args: + dx_e: Lists of cell sizes for all axes + `[[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...]`. + + Returns: + Sparse matrix for taking the discretized curl of the E-field + """ + return cross(deriv_forward(dx_e)) + + +def curl_back(dx_h: List[numpy.ndarray]) -> sparse.spmatrix: + """ + Curl operator for use with the H field. + + Args: + dx_h: Lists of cell sizes for all axes + `[[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...]`. + + Returns: + Sparse matrix for taking the discretized curl of the H-field + """ + return cross(deriv_back(dx_h)) diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py index a1d278a..02cfc05 100644 --- a/meanas/fdtd/__init__.py +++ b/meanas/fdtd/__init__.py @@ -1,5 +1,5 @@ """ -Basic FDTD functionality +Utilities for running finite-difference time-domain (FDTD) simulations """ from .base import maxwell_e, maxwell_h diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py index 389a7b5..87a234b 100644 --- a/meanas/fdtd/base.py +++ b/meanas/fdtd/base.py @@ -5,70 +5,14 @@ from typing import List, Callable, Tuple, Dict import numpy from .. import dx_lists_t, field_t, field_updater +from ..fdmath.functional import curl_forward, curl_back + __author__ = 'Jan Petykiewicz' -def curl_h(dxes: dx_lists_t = None) -> field_updater: - """ - Curl operator for use with the H field. - - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :return: Function for taking the discretized curl of the H-field, F(H) -> curlH - """ - if dxes: - dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') - - def dh(f, ax): - return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] - else: - def dh(f, ax): - return f - numpy.roll(f, 1, axis=ax) - - def ch_fun(h: field_t) -> field_t: - output = numpy.empty_like(h) - output[0] = dh(h[2], 1) - output[1] = dh(h[0], 2) - output[2] = dh(h[1], 0) - output[0] -= dh(h[1], 2) - output[1] -= dh(h[2], 0) - output[2] -= dh(h[0], 1) - return output - - return ch_fun - - -def curl_e(dxes: dx_lists_t = None) -> field_updater: - """ - Curl operator for use with the E field. - - :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types - :return: Function for taking the discretized curl of the E-field, F(E) -> curlE - """ - if dxes is not None: - dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') - - def de(f, ax): - return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] - else: - def de(f, ax): - return numpy.roll(f, -1, axis=ax) - f - - def ce_fun(e: field_t) -> field_t: - output = numpy.empty_like(e) - output[0] = de(e[2], 1) - output[1] = de(e[0], 2) - output[2] = de(e[1], 0) - output[0] -= de(e[1], 2) - output[1] -= de(e[2], 0) - output[2] -= de(e[0], 1) - return output - - return ce_fun - - def maxwell_e(dt: float, dxes: dx_lists_t = None) -> field_updater: - curl_h_fun = curl_h(dxes) + curl_h_fun = curl_back(dxes[1]) def me_fun(e: field_t, h: field_t, epsilon: field_t): e += dt * curl_h_fun(h) / epsilon @@ -78,7 +22,7 @@ def maxwell_e(dt: float, dxes: dx_lists_t = None) -> field_updater: def maxwell_h(dt: float, dxes: dx_lists_t = None) -> field_updater: - curl_e_fun = curl_e(dxes) + curl_e_fun = curl_forward(dxes[0]) def mh_fun(e: field_t, h: field_t): h -= dt * curl_e_fun(e) diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index 8644646..d5ea1dc 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -35,6 +35,7 @@ def poynting_divergence(s: field_t = None, if s is None: s = poynting(e, h, dxes=dxes) + #TODO use deriv operators ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) + (s[1] - numpy.roll(s[1], 1, axis=1)) + (s[2] - numpy.roll(s[2], 1, axis=2))) diff --git a/meanas/test/__init__.py b/meanas/test/__init__.py index e69de29..e02b636 100644 --- a/meanas/test/__init__.py +++ b/meanas/test/__init__.py @@ -0,0 +1,3 @@ +""" +Tests (run with `python3 -m pytest -rxPXs | tee results.txt`) +""" diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py index 112193d..eafa14f 100644 --- a/meanas/test/test_fdfd_pml.py +++ b/meanas/test/test_fdfd_pml.py @@ -101,8 +101,8 @@ def j_distribution(request, shape, epsilon, dxes, omega, src_polarity): slices[dim] = slice(shape[dim + 1] // 2, shape[dim + 1] // 2 + 1) - j = fdfd.waveguide_mode.compute_source(E=e, wavenumber=wavenumber_corrected, omega=omega, dxes=dxes, - axis=dim, polarity=src_polarity, slices=slices, epsilon=epsilon) + j = fdfd.waveguide_3d.compute_source(E=e, wavenumber=wavenumber_corrected, omega=omega, dxes=dxes, + axis=dim, polarity=src_polarity, slices=slices, epsilon=epsilon) yield j @@ -145,4 +145,3 @@ def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): ) return sim - diff --git a/meanas/types.py b/meanas/types.py index 7dc5c1c..667a286 100644 --- a/meanas/types.py +++ b/meanas/types.py @@ -6,17 +6,20 @@ from typing import List, Callable # Field types -field_t = numpy.ndarray # vector field with shape (3, X, Y, Z) (e.g. [E_x, E_y, E_z]) -vfield_t = numpy.ndarray # linearized vector field (vector of length 3*X*Y*Z) +field_t = numpy.ndarray +"""vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)""" +vfield_t = numpy.ndarray +"""Linearized vector field (vector of length 3*X*Y*Z)""" + +dx_lists_t = List[List[numpy.ndarray]] ''' 'dxes' datastructure which contains grid cell width information in the following format: - [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]], - [[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]] - where dx_e_0 is the x-width of the x=0 cells, as used when calculating dE/dx, - and dy_h_0 is the y-width of the y=0 cells, as used when calculating dH/dy, etc. + `[[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]], + [[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]]` + where `dx_e_0` is the x-width of the `x=0` cells, as used when calculating dE/dx, + and `dy_h_0` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc. ''' -dx_lists_t = List[List[numpy.ndarray]] field_updater = Callable[[field_t], field_t] diff --git a/pdoc_templates/config.mako b/pdoc_templates/config.mako new file mode 100644 index 0000000..0642566 --- /dev/null +++ b/pdoc_templates/config.mako @@ -0,0 +1,46 @@ +<%! + # Template configuration. Copy over in your template directory + # (used with --template-dir) and adapt as required. + html_lang = 'en' + show_inherited_members = False + extract_module_toc_into_sidebar = True + list_class_variables_in_index = True + sort_identifiers = True + show_type_annotations = True + + # Show collapsed source code block next to each item. + # Disabling this can improve rendering speed of large modules. + show_source_code = True + + # If set, format links to objects in online source code repository + # according to this template. Supported keywords for interpolation + # are: commit, path, start_line, end_line. + #git_link_template = 'https://github.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' + #git_link_template = 'https://gitlab.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' + #git_link_template = 'https://bitbucket.org/USER/PROJECT/src/{commit}/{path}#lines-{start_line}:{end_line}' + #git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start-line}' + git_link_template = None + + # A prefix to use for every HTML hyperlink in the generated documentation. + # No prefix results in all links being relative. + link_prefix = '' + + # Enable syntax highlighting for code/source blocks by including Highlight.js + syntax_highlighting = True + + # Set the style keyword such as 'atom-one-light' or 'github-gist' + # Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles + # Demo: https://highlightjs.org/static/demo/ + hljs_style = 'github' + + # If set, insert Google Analytics tracking code. Value is GA + # tracking id (UA-XXXXXX-Y). + google_analytics = '' + + # If set, render LaTeX math syntax within \(...\) (inline equations), + # or within \[...\] or $$...$$ or `.. math::` (block equations) + # as nicely-formatted math formulas using MathJax. + # Note: in Python docstrings, either all backslashes need to be escaped (\\) + # or you need to use raw r-strings. + latex_math = True +%> diff --git a/pdoc_templates/css.mako b/pdoc_templates/css.mako new file mode 100644 index 0000000..39a77ed --- /dev/null +++ b/pdoc_templates/css.mako @@ -0,0 +1,389 @@ +<%! + from pdoc.html_helpers import minify_css +%> + +<%def name="mobile()" filter="minify_css"> + .flex { + display: flex !important; + } + + body { + line-height: 1.5em; + background: black; + color: #DDD; + } + + #content { + padding: 20px; + } + + #sidebar { + padding: 30px; + overflow: hidden; + } + + .http-server-breadcrumbs { + font-size: 130%; + margin: 0 0 15px 0; + } + + #footer { + font-size: .75em; + padding: 5px 30px; + border-top: 1px solid #ddd; + text-align: right; + } + #footer p { + margin: 0 0 0 1em; + display: inline-block; + } + #footer p:last-child { + margin-right: 30px; + } + + h1, h2, h3, h4, h5 { + font-weight: 300; + } + h1 { + font-size: 2.5em; + line-height: 1.1em; + } + h2 { + font-size: 1.75em; + margin: 1em 0 .50em 0; + } + h3 { + font-size: 1.4em; + margin: 25px 0 10px 0; + } + h4 { + margin: 0; + font-size: 105%; + } + + a { + color: #999; + text-decoration: none; + transition: color .3s ease-in-out; + } + a:hover { + color: #18d; + } + + .title code { + font-weight: bold; + } + h2[id^="header-"] { + margin-top: 2em; + } + .ident { + color: #7ff; + } + + pre code { + background: transparent; + font-size: .8em; + line-height: 1.4em; + } + code { + background: #0d0d0e; + padding: 1px 4px; + overflow-wrap: break-word; + } + h1 code { background: transparent } + + pre { + background: #111; + border: 0; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + margin: 1em 0; + padding: 1ex; + } + + #http-server-module-list { + display: flex; + flex-flow: column; + } + #http-server-module-list div { + display: flex; + } + #http-server-module-list dt { + min-width: 10%; + } + #http-server-module-list p { + margin-top: 0; + } + + .toc ul, + #index { + list-style-type: none; + margin: 0; + padding: 0; + } + #index code { + background: transparent; + } + #index h3 { + border-bottom: 1px solid #ddd; + } + #index ul { + padding: 0; + } + #index h4 { + font-weight: bold; + } + #index h4 + ul { + margin-bottom:.6em; + } + /* Make TOC lists have 2+ columns when viewport is wide enough. + Assuming ~20-character identifiers and ~30% wide sidebar. */ + @media (min-width: 200ex) { #index .two-column { column-count: 2 } } + @media (min-width: 300ex) { #index .two-column { column-count: 3 } } + + dl { + margin-bottom: 2em; + } + dl dl:last-child { + margin-bottom: 4em; + } + dd { + margin: 0 0 1em 3em; + } + #header-classes + dl > dd { + margin-bottom: 3em; + } + dd dd { + margin-left: 2em; + } + dd p { + margin: 10px 0; + } + .name { + background: #111; + font-weight: bold; + font-size: .85em; + padding: 5px 10px; + display: inline-block; + min-width: 40%; + } + .name:hover { + background: #101010; + } + .name > span:first-child { + white-space: nowrap; + } + .name.class > span:nth-child(2) { + margin-left: .4em; + } + .inherited { + color: #777; + border-left: 5px solid #eee; + padding-left: 1em; + } + .inheritance em { + font-style: normal; + font-weight: bold; + } + + /* Docstrings titles, e.g. in numpydoc format */ + .desc h2 { + font-weight: 400; + font-size: 1.25em; + } + .desc h3 { + font-size: 1em; + } + .desc dt code { + background: inherit; /* Don't grey-back parameters */ + } + + .source summary, + .git-link-div { + color: #aaa; + text-align: right; + font-weight: 400; + font-size: .8em; + text-transform: uppercase; + } + .source summary > * { + white-space: nowrap; + cursor: pointer; + } + .git-link { + color: inherit; + margin-left: 1em; + } + .source pre { + max-height: 500px; + overflow: auto; + margin: 0; + } + .source pre code { + font-size: 12px; + overflow: visible; + } + .hlist { + list-style: none; + } + .hlist li { + display: inline; + } + .hlist li:after { + content: ',\2002'; + } + .hlist li:last-child:after { + content: none; + } + .hlist .hlist { + display: inline; + padding-left: 1em; + } + + img { + max-width: 100%; + } + + .admonition { + padding: .1em .5em; + margin-bottom: 1em; + } + .admonition-title { + font-weight: bold; + } + .admonition.note, + .admonition.info, + .admonition.important { + background: #610; + } + .admonition.todo, + .admonition.versionadded, + .admonition.tip, + .admonition.hint { + background: #202; + } + .admonition.warning, + .admonition.versionchanged, + .admonition.deprecated { + background: #02b; + } + .admonition.error, + .admonition.danger, + .admonition.caution { + background: darkpink; + } + + +<%def name="desktop()" filter="minify_css"> + @media screen and (min-width: 700px) { + #sidebar { + width: 30%; + } + #content { + width: 70%; + max-width: 100ch; + padding: 3em 4em; + border-left: 1px solid #ddd; + } + pre code { + font-size: 1em; + } + .item .name { + font-size: 1em; + } + main { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + } + .toc ul ul, + #index ul { + padding-left: 1.5em; + } + .toc > ul > li { + margin-top: .5em; + } + } + + +<%def name="print()" filter="minify_css"> +@media print { + #sidebar h1 { + page-break-before: always; + } + .source { + display: none; + } +} +@media print { + * { + background: transparent !important; + color: #000 !important; /* Black prints faster: h5bp.com/s */ + box-shadow: none !important; + text-shadow: none !important; + } + + a[href]:after { + content: " (" attr(href) ")"; + font-size: 90%; + } + /* Internal, documentation links, recognized by having a title, + don't need the URL explicity stated. */ + a[href][title]:after { + content: none; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + /* + * Don't show links for images, or javascript/internal links + */ + + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; /* h5bp.com/t */ + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + @page { + margin: 0.5cm; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + page-break-after: avoid; + } +} + diff --git a/pdoc_templates/html.mako b/pdoc_templates/html.mako new file mode 100644 index 0000000..9cf1137 --- /dev/null +++ b/pdoc_templates/html.mako @@ -0,0 +1,421 @@ +<% + import os + + import pdoc + from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link + + + def link(d, name=None, fmt='{}'): + name = fmt.format(name or d.qualname + ('()' if isinstance(d, pdoc.Function) else '')) + if not isinstance(d, pdoc.Doc) or isinstance(d, pdoc.External) and not external_links: + return name + url = d.url(relative_to=module, link_prefix=link_prefix, + top_ancestor=not show_inherited_members) + return '{}'.format(d.refname, url, name) + + + def to_html(text): + return _to_html(text, module=module, link=link, latex_math=latex_math) +%> + +<%def name="ident(name)">${name} + +<%def name="show_source(d)"> + % if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None): + <% git_link = format_git_link(git_link_template, d) %> + % if show_source_code: +
+ + Expand source code + % if git_link: + Browse git + %endif + +
${d.source | h}
+
+ % elif git_link: + + %endif + %endif + + +<%def name="show_desc(d, short=False)"> + <% + inherits = ' inherited' if d.inherits else '' + docstring = glimpse(d.docstring) if short or inherits else d.docstring + %> + % if d.inherits: +

+ Inherited from: + % if hasattr(d.inherits, 'cls'): + ${link(d.inherits.cls)}.${link(d.inherits, d.name)} + % else: + ${link(d.inherits)} + % endif +

+ % endif +
${docstring | to_html}
+ % if not isinstance(d, pdoc.Module): + ${show_source(d)} + % endif + + +<%def name="show_module_list(modules)"> +

Python module list

+ +% if not modules: +

No modules found.

+% else: +
+ % for name, desc in modules: +
+
${name}
+
${desc | glimpse, to_html}
+
+ % endfor +
+% endif + + +<%def name="show_column_list(items)"> + <% + two_column = len(items) >= 6 and all(len(i.name) < 20 for i in items) + %> +
    + % for item in items: +
  • ${link(item, item.name)}
  • + % endfor +
+ + +<%def name="show_module(module)"> + <% + variables = module.variables(sort=sort_identifiers) + classes = module.classes(sort=sort_identifiers) + functions = module.functions(sort=sort_identifiers) + submodules = module.submodules() + %> + + <%def name="show_func(f)"> +
+ <% + params = ', '.join(f.params(annotate=show_type_annotations, link=link)) + returns = show_type_annotations and f.return_annotation(link=link) or '' + if returns: + returns = ' ->\N{NBSP}' + returns + %> + ${f.funcdef()} ${ident(f.name)}(${params})${returns} +
+
${show_desc(f)}
+ + +
+ % if http_server: + + % endif +

${'Namespace' if module.is_namespace else 'Module'} ${module.name}

+
+ +
+ ${module.docstring | to_html} + ${show_source(module)} +
+ +
+ % if submodules: +

Sub-modules

+
+ % for m in submodules: +
${link(m)}
+
${show_desc(m, short=True)}
+ % endfor +
+ % endif +
+ +
+ % if variables: +

Global variables

+
+ % for v in variables: +
var ${ident(v.name)}
+
${show_desc(v)}
+ % endfor +
+ % endif +
+ +
+ % if functions: +

Functions

+
+ % for f in functions: + ${show_func(f)} + % endfor +
+ % endif +
+ +
+ % if classes: +

Classes

+
+ % for c in classes: + <% + class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers) + smethods = c.functions(show_inherited_members, sort=sort_identifiers) + inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers) + methods = c.methods(show_inherited_members, sort=sort_identifiers) + mro = c.mro() + subclasses = c.subclasses() + params = ', '.join(c.params(annotate=show_type_annotations, link=link)) + %> +
+ class ${ident(c.name)} + % if params: + (${params}) + % endif +
+ +
${show_desc(c)} + + % if mro: +

Ancestors

+
    + % for cls in mro: +
  • ${link(cls)}
  • + % endfor +
+ %endif + + % if subclasses: +

Subclasses

+
    + % for sub in subclasses: +
  • ${link(sub)}
  • + % endfor +
+ % endif + % if class_vars: +

Class variables

+
+ % for v in class_vars: +
var ${ident(v.name)}
+
${show_desc(v)}
+ % endfor +
+ % endif + % if smethods: +

Static methods

+
+ % for f in smethods: + ${show_func(f)} + % endfor +
+ % endif + % if inst_vars: +

Instance variables

+
+ % for v in inst_vars: +
var ${ident(v.name)}
+
${show_desc(v)}
+ % endfor +
+ % endif + % if methods: +

Methods

+
+ % for f in methods: + ${show_func(f)} + % endfor +
+ % endif + + % if not show_inherited_members: + <% + members = c.inherited_members() + %> + % if members: +

Inherited members

+
    + % for cls, mems in members: +
  • ${link(cls)}: +
      + % for m in mems: +
    • ${link(m, name=m.name)}
    • + % endfor +
    + +
  • + % endfor +
+ % endif + % endif + +
+ % endfor +
+ % endif +
+ + +<%def name="module_index(module)"> + <% + variables = module.variables(sort=sort_identifiers) + classes = module.classes(sort=sort_identifiers) + functions = module.functions(sort=sort_identifiers) + submodules = module.submodules() + supermodule = module.supermodule + %> + + + + + + + + + + +<% + module_list = 'modules' in context.keys() # Whether we're showing module list in server mode +%> + + % if module_list: + Python module list + + % else: + ${module.name} API documentation + + % endif + + + + % if syntax_highlighting: + + %endif + + <%namespace name="css" file="css.mako" /> + + + + + % if google_analytics: + + % endif + + % if latex_math: + + % endif + + <%include file="head.mako"/> + + +
+ % if module_list: +
+ ${show_module_list(modules)} +
+ % else: +
+ ${show_module(module)} +
+ ${module_index(module)} + % endif +
+ + + +% if syntax_highlighting: + + +% endif + +% if http_server and module: ## Auto-reload on file change in dev mode + +% endif + + From 004295fdf7681fd424f14e464919cbcfe7e65a0c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 24 Nov 2019 23:48:59 -0800 Subject: [PATCH 196/437] ignore docs directory --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 825f5ec..bf30555 100644 --- a/.gitignore +++ b/.gitignore @@ -52,8 +52,8 @@ coverage.xml # Django stuff: *.log -# Sphinx documentation -docs/_build/ +# documentation +doc/ # PyBuilder target/ From e69b0f61ecc4d15e64a2d84e5eafdd046afb6590 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 24 Nov 2019 23:54:24 -0800 Subject: [PATCH 197/437] ignore vim swap files --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index bf30555..5c5a8db 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,7 @@ target/ .idea/ + + +.swp +.swo From e5db39fd49f0d064cd3ef1035cc767cb7ed5a6e9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 25 Nov 2019 00:04:53 -0800 Subject: [PATCH 198/437] use fdmath derivatives where possible --- meanas/fdtd/energy.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index d5ea1dc..608137f 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -2,8 +2,7 @@ from typing import List, Callable, Tuple, Dict import numpy -from .. import dx_lists_t, field_t, field_updater - +from .. import dx_lists_t, field_t, field_updater, fdmath def poynting(e: field_t, h: field_t, @@ -35,10 +34,8 @@ def poynting_divergence(s: field_t = None, if s is None: s = poynting(e, h, dxes=dxes) - #TODO use deriv operators - ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) + - (s[1] - numpy.roll(s[1], 1, axis=1)) + - (s[2] - numpy.roll(s[2], 1, axis=2))) + Dx, Dy, Dz = fdmath.functional.deriv_back() + ds = Dx(s[0]) + Dy(s[1]) + Dz(s[2]) return ds From 93f1ab7c92d3622758a93a70105b1a9a35890424 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 25 Nov 2019 00:05:54 -0800 Subject: [PATCH 199/437] gitignore update --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5c5a8db..1d95edb 100644 --- a/.gitignore +++ b/.gitignore @@ -62,5 +62,4 @@ target/ .idea/ -.swp -.swo +.*.sw[op] From 1242e8794bd10cde1662957db1c7b715deeab212 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 26 Nov 2019 01:47:52 -0800 Subject: [PATCH 200/437] documentation updates --- meanas/fdmath/__init__.py | 94 +++++++++++++++++++++++++++++++++++++++ meanas/types.py | 31 +++++++++---- 2 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 meanas/fdmath/__init__.py diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py new file mode 100644 index 0000000..74deff1 --- /dev/null +++ b/meanas/fdmath/__init__.py @@ -0,0 +1,94 @@ +""" +Basic discrete calculus for finite difference (fd) simulations. + +This documentation and approach is roughly based on W.C. Chew's excellent +"Electromagnetic Theory on a Lattice" (doi:10.1063/1.355770), +which covers a superset of this material with similar notation and more detail. + + +Define the discrete forward derivative as + + Dx_forward(f)[i] = (f[i + 1] - f[i]) / dx[i] + +or + $$ [\\tilde{\\partial}_x f ]_{m + \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m + 1} - f_m) $$ + +Likewise, discrete reverse derivative is + + Dx_back(f)[i] = (f[i] - f[i - 1]) / dx[i] + +or + $$ [\\hat{\\partial}_x f ]_{m - \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m} - f_{m - 1}) $$ + + +The derivatives are shifted by a half-cell relative to the original function: + + _________________________ + | | | | | + | f0 | f1 | f2 | f3 | + |_____|_____|_____|_____| + | | | | + | Df0 | Df1 | Df2 | Df3 + ___|_____|_____|_____|____ + +Periodic boundaries are used unless otherwise noted. + + +Expanding to three dimensions, we can define two gradients + $$ [\\tilde{\\nabla} f]_{n,m,p} = \\vec{x} [\\tilde{\\partial}_x f]_{m + \\frac{1}{2},n,p} + + \\vec{y} [\\tilde{\\partial}_y f]_{m,n + \\frac{1}{2},p} + + \\vec{z} [\\tilde{\\partial}_z f]_{m,n,p + \\frac{1}{2}} $$ + $$ [\\hat{\\nabla} f]_{m,n,p} = \\vec{x} [\\hat{\\partial}_x f]_{m + \\frac{1}{2},n,p} + + \\vec{y} [\\hat{\\partial}_y f]_{m,n + \\frac{1}{2},p} + + \\vec{z} [\\hat{\\partial}_z f]_{m,n,p + \\frac{1}{2}} $$ + +The three derivatives in the gradient cause shifts in different +directions, so the x/y/z components of the resulting "vector" are defined +at different points: the x-component is shifted in the x-direction, +y in y, and z in z. + +We call the resulting object a "fore-vector" or "back-vector", depending +on the direction of the shift. We write it as + $$ \\tilde{g}_{m,n,p} = \\vec{x} g^x_{m + \\frac{1}{2},n,p} + + \\vec{y} g^y_{m,n + \\frac{1}{2},p} + + \\vec{z} g^z_{m,n,p + \\frac{1}{2}} $$ + $$ \\hat{g}_{m,n,p} = \\vec{x} g^x_{m - \\frac{1}{2},n,p} + + \\vec{y} g^y_{m,n - \\frac{1}{2},p} + + \\vec{z} g^z_{m,n,p - \\frac{1}{2}} $$ + + +There are also two divergences, + + $$ d_{n,m,p} = [\\tilde{\\nabla} \\cdot \\hat{g}]_{n,m,p} + = [\\tilde{\\partial}_x g^x]_{m,n,p} + + [\\tilde{\\partial}_y g^y]_{m,n,p} + + [\\tilde{\\partial}_z g^z]_{m,n,p} $$ + + $$ d_{n,m,p} = [\\hat{\\nabla} \\cdot \\tilde{g}]_{n,m,p} + = [\\hat{\\partial}_x g^x]_{m,n,p} + + [\\hat{\\partial}_y g^y]_{m,n,p} + + [\\hat{\\partial}_z g^z]_{m,n,p} $$ + +Since we applied the forward divergence to the back-vector (and vice-versa), the resulting scalar value +is defined at the back-vector's (fore-vectors) location \\( (m,n,p) \\) and not at the locations of its components +\\( (m \\pm \\frac{1}{2},n,p) \\) etc. + + +The two curls are then + $$ \\begin{align} + \\hat{h}_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &= \\\\ + [\\tilde{\\nabla} \\times \\tilde{g}]_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &= + \\vec{x} (\\tilde{\\partial}_y g^z_{m,n,p + \\frac{1}{2}} - \\tilde{\\partial}_z g^y_{m,n + \\frac{1}{2},p}) \\\\ + &+ \\vec{y} (\\tilde{\\partial}_z g^x_{m + \\frac{1}{2},n,p} - \\tilde{\\partial}_x g^z_{m,n,p + \\frac{1}{2}}) \\\\ + &+ \\vec{z} (\\tilde{\\partial}_x g^y_{m,n + \\frac{1}{2},p} - \\tilde{\\partial}_x g^z_{m + \\frac{1}{2},n,p}) + \\end{align}$$ +and + $$ \\tilde{h}_{m - \\frac{1}{2}, n - \\frac{1}{2}, p - \\frac{1}{2}} = + [\\hat{\\nabla} \\times \\hat{g}]_{m - \\frac{1}{2}, n - \\frac{1}{2}, p - \\frac{1}{2}} $$ + + where \\( \\hat{g} \\) and \\( \\tilde{g} \\) are located at \\((m,n,p)\\) + with components at \\( (m \\pm \\frac{1}{2},n,p) \\) etc., + while \\( \\hat{h} \\) and \\( \\tilde{h} \\) are located at \\((m \pm \\frac{1}{2}, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) + with components at \\((m, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) etc. + +""" diff --git a/meanas/types.py b/meanas/types.py index 667a286..2a1351f 100644 --- a/meanas/types.py +++ b/meanas/types.py @@ -6,19 +6,34 @@ from typing import List, Callable # Field types -field_t = numpy.ndarray -"""vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)""" +# TODO: figure out a better way to set the docstrings without creating actual subclasses? +# Probably not a big issue since they're only used for type hinting +class field_t(numpy.ndarray): + """ + Vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`) + + This is actually is just an unaltered `numpy.ndarray` + """ + pass + +class vfield_t(numpy.ndarray): + """ + Linearized vector field (single vector of length 3*X*Y*Z) + + This is actually just an unaltered `numpy.ndarray` + """ + pass -vfield_t = numpy.ndarray -"""Linearized vector field (vector of length 3*X*Y*Z)""" dx_lists_t = List[List[numpy.ndarray]] ''' 'dxes' datastructure which contains grid cell width information in the following format: - `[[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]], - [[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]]` - where `dx_e_0` is the x-width of the `x=0` cells, as used when calculating dE/dx, - and `dy_h_0` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc. + + [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]], + [[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]] + + where `dx_e_0` is the x-width of the `x=0` cells, as used when calculating dE/dx, + and `dy_h_0` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc. ''' From a956323b94f1cff5d896dd9156af679c6626dd90 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 27 Nov 2019 22:59:52 -0800 Subject: [PATCH 201/437] move stuff under fdmath --- examples/fdfd.py | 16 ++-- meanas/__init__.py | 3 - meanas/eigensolvers.py | 15 ++-- meanas/fdfd/bloch.py | 28 +++--- meanas/fdfd/farfield.py | 10 +-- meanas/fdfd/functional.py | 52 ++++++----- meanas/fdfd/operators.py | 70 +++++++-------- meanas/fdfd/scpml.py | 10 +-- meanas/fdfd/solvers.py | 17 ++-- meanas/fdfd/waveguide_2d.py | 126 ++++++++++++++------------- meanas/fdfd/waveguide_3d.py | 32 +++---- meanas/fdfd/waveguide_cyl.py | 12 +-- meanas/fdmath/__init__.py | 8 ++ meanas/fdmath/functional.py | 14 +-- meanas/fdmath/operators.py | 4 +- meanas/{ => fdmath}/types.py | 6 +- meanas/{ => fdmath}/vectorization.py | 6 +- meanas/fdtd/base.py | 20 +++-- meanas/fdtd/boundaries.py | 12 +-- meanas/fdtd/energy.py | 83 +++++++++--------- meanas/fdtd/pml.py | 12 +-- meanas/test/test_fdfd.py | 17 +++- meanas/test/test_fdfd_pml.py | 3 +- 23 files changed, 304 insertions(+), 272 deletions(-) rename meanas/{ => fdmath}/types.py (89%) rename meanas/{ => fdmath}/vectorization.py (90%) diff --git a/examples/fdfd.py b/examples/fdfd.py index aa684e8..3fe895f 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -3,14 +3,18 @@ import numpy from numpy.linalg import norm import meanas -from meanas import vec, unvec, fdtd -from meanas.fdfd import waveguide_mode, functional, scpml, operators +from meanas import fdtd +from meanas.fdmath import vec, unvec +from meanas.fdfd import waveguide_3d, functional, scpml, operators from meanas.fdfd.solvers import generic as generic_solver import gridlock from matplotlib import pyplot +import logging + +logging.basicConfig(level=logging.DEBUG) __author__ = 'Jan Petykiewicz' @@ -134,10 +138,10 @@ def test1(solver=generic_solver): 'polarity': +1, } - wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, omega=omega, epsilon=grid.grids, **wg_args) - J = waveguide_mode.compute_source(E=wg_results['E'], wavenumber=wg_results['wavenumber'], - omega=omega, epsilon=grid.grids, **wg_args) - e_overlap = waveguide_mode.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], **wg_args) + wg_results = waveguide_3d.solve_mode(mode_number=0, omega=omega, epsilon=grid.grids, **wg_args) + J = waveguide_3d.compute_source(E=wg_results['E'], wavenumber=wg_results['wavenumber'], + omega=omega, epsilon=grid.grids, **wg_args) + e_overlap = waveguide_3d.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], **wg_args) pecg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) # pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) diff --git a/meanas/__init__.py b/meanas/__init__.py index 8d30d47..770bdf4 100644 --- a/meanas/__init__.py +++ b/meanas/__init__.py @@ -6,9 +6,6 @@ See the readme or `import meanas; help(meanas)` for more info. import pathlib -from .types import dx_lists_t, field_t, vfield_t, field_updater -from .vectorization import vec, unvec - __author__ = 'Jan Petykiewicz' with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f: diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py index 960df60..51aeb6e 100644 --- a/meanas/eigensolvers.py +++ b/meanas/eigensolvers.py @@ -37,20 +37,17 @@ def power_iteration(operator: sparse.spmatrix, def rayleigh_quotient_iteration(operator: sparse.spmatrix or spalg.LinearOperator, - guess_vectors: numpy.ndarray, + guess_vector: numpy.ndarray, iterations: int = 40, tolerance: float = 1e-13, - solver=None, + solver = None, ) -> Tuple[complex, numpy.ndarray]: """ Use Rayleigh quotient iteration to refine an eigenvector guess. - TODO: - Need to test this for more than one guess_vectors. - Args: operator: Matrix to analyze. - guess_vectors: Eigenvectors to refine. + guess_vector: Eigenvector to refine. iterations: Maximum number of iterations to perform. Default 40. tolerance: Stop iteration if `(A - I*eigenvalue) @ v < num_vectors * tolerance`, Default 1e-13. @@ -73,16 +70,16 @@ def rayleigh_quotient_iteration(operator: sparse.spmatrix or spalg.LinearOperato if solver is None: solver = lambda A, b: spalg.bicgstab(A, b)[0] - v = numpy.atleast_2d(guess_vectors) + v = numpy.squeeze(guess_vector) v /= norm(v) for _ in range(iterations): eigval = v.conj() @ (operator @ v) - if norm(operator @ v - eigval * v) < v.shape[1] * tolerance: + if norm(operator @ v - eigval * v) < tolerance: break shifted_operator = operator - shift(eigval) v = solver(shifted_operator, v) - v /= norm(v, axis=0) + v /= norm(v) return eigval, v diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index 0ca64a7..28b82df 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -83,7 +83,7 @@ import scipy.optimize from scipy.linalg import norm import scipy.sparse.linalg as spalg -from .. import field_t +from ..fdmath import fdfield_t logger = logging.getLogger(__name__) @@ -154,8 +154,8 @@ def generate_kmn(k0: numpy.ndarray, def maxwell_operator(k0: numpy.ndarray, G_matrix: numpy.ndarray, - epsilon: field_t, - mu: field_t = None + epsilon: fdfield_t, + mu: fdfield_t = None ) -> Callable[[numpy.ndarray], numpy.ndarray]: """ Generate the Maxwell operator @@ -227,8 +227,8 @@ def maxwell_operator(k0: numpy.ndarray, def hmn_2_exyz(k0: numpy.ndarray, G_matrix: numpy.ndarray, - epsilon: field_t, - ) -> Callable[[numpy.ndarray], field_t]: + epsilon: fdfield_t, + ) -> Callable[[numpy.ndarray], fdfield_t]: """ Generate an operator which converts a vectorized spatial-frequency-space h_mn into an E-field distribution, i.e. @@ -249,7 +249,7 @@ def hmn_2_exyz(k0: numpy.ndarray, k_mag, m, n = generate_kmn(k0, G_matrix, shape) - def operator(h: numpy.ndarray) -> field_t: + def operator(h: numpy.ndarray) -> fdfield_t: hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] d_xyz = (n * hin_m - m * hin_n) * k_mag @@ -262,8 +262,8 @@ def hmn_2_exyz(k0: numpy.ndarray, def hmn_2_hxyz(k0: numpy.ndarray, G_matrix: numpy.ndarray, - epsilon: field_t - ) -> Callable[[numpy.ndarray], field_t]: + epsilon: fdfield_t + ) -> Callable[[numpy.ndarray], fdfield_t]: """ Generate an operator which converts a vectorized spatial-frequency-space h_mn into an H-field distribution, i.e. @@ -293,8 +293,8 @@ def hmn_2_hxyz(k0: numpy.ndarray, def inverse_maxwell_operator_approx(k0: numpy.ndarray, G_matrix: numpy.ndarray, - epsilon: field_t, - mu: field_t = None + epsilon: fdfield_t, + mu: fdfield_t = None ) -> Callable[[numpy.ndarray], numpy.ndarray]: """ Generate an approximate inverse of the Maxwell operator, @@ -366,8 +366,8 @@ def find_k(frequency: float, tolerance: float, direction: numpy.ndarray, G_matrix: numpy.ndarray, - epsilon: field_t, - mu: field_t = None, + epsilon: fdfield_t, + mu: fdfield_t = None, band: int = 0, k_min: float = 0, k_max: float = 0.5, @@ -409,8 +409,8 @@ def find_k(frequency: float, def eigsolve(num_modes: int, k0: numpy.ndarray, G_matrix: numpy.ndarray, - epsilon: field_t, - mu: field_t = None, + epsilon: fdfield_t, + mu: fdfield_t = None, tolerance: float = 1e-20, max_iters: int = 10000, reset_iters: int = 100, diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index 665e70f..b4b9dbe 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -6,11 +6,11 @@ import numpy from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift from numpy import pi -from .. import field_t +from .. import fdfield_t -def near_to_farfield(E_near: field_t, - H_near: field_t, +def near_to_farfield(E_near: fdfield_t, + H_near: fdfield_t, dx: float, dy: float, padded_size: List[int] = None @@ -117,8 +117,8 @@ def near_to_farfield(E_near: field_t, -def far_to_nearfield(E_far: field_t, - H_far: field_t, +def far_to_nearfield(E_far: fdfield_t, + H_far: fdfield_t, dkx: float, dky: float, padded_size: List[int] = None diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index 74b66da..d995e7b 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -2,32 +2,30 @@ Functional versions of many FDFD operators. These can be useful for performing FDFD calculations without needing to construct large matrices in memory. -The functions generated here expect `field_t` inputs with shape (3, X, Y, Z), +The functions generated here expect `fdfield_t` inputs with shape (3, X, Y, Z), e.g. E = [E_x, E_y, E_z] where each component has shape (X, Y, Z) """ from typing import List, Callable, Tuple import numpy -from .. import dx_lists_t, field_t +from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t from ..fdmath.functional import curl_forward, curl_back + __author__ = 'Jan Petykiewicz' -field_transform_t = Callable[[field_t], field_t] - - def e_full(omega: complex, dxes: dx_lists_t, - epsilon: field_t, - mu: field_t = None - ) -> field_transform_t: + epsilon: fdfield_t, + mu: fdfield_t = None + ) -> fdfield_updater_t: """ Wave operator for use with E-field. See `operators.e_full` for details. Args: omega: Angular frequency of the simulation - dxes: Grid parameters [dx_e, dx_h] as described in meanas.types + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Dielectric constant mu: Magnetic permeability (default 1 everywhere) @@ -54,16 +52,16 @@ def e_full(omega: complex, def eh_full(omega: complex, dxes: dx_lists_t, - epsilon: field_t, - mu: field_t = None - ) -> Callable[[field_t, field_t], Tuple[field_t, field_t]]: + epsilon: fdfield_t, + mu: fdfield_t = None + ) -> Callable[[fdfield_t, fdfield_t], Tuple[fdfield_t, fdfield_t]]: """ Wave operator for full (both E and H) field representation. See `operators.eh_full`. Args: omega: Angular frequency of the simulation - dxes: Grid parameters [dx_e, dx_h] as described in meanas.types + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Dielectric constant mu: Magnetic permeability (default 1 everywhere) @@ -90,15 +88,15 @@ def eh_full(omega: complex, def e2h(omega: complex, dxes: dx_lists_t, - mu: field_t = None, - ) -> field_transform_t: + mu: fdfield_t = None, + ) -> fdfield_updater_t: """ Utility operator for converting the `E` field into the `H` field. For use with `e_full` -- assumes that there is no magnetic current `M`. Args: omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` mu: Magnetic permeability (default 1 everywhere) Return: @@ -121,8 +119,8 @@ def e2h(omega: complex, def m2j(omega: complex, dxes: dx_lists_t, - mu: field_t = None, - ) -> field_transform_t: + mu: fdfield_t = None, + ) -> fdfield_updater_t: """ Utility operator for converting magnetic current `M` distribution into equivalent electric current distribution `J`. @@ -130,7 +128,7 @@ def m2j(omega: complex, Args: omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` mu: Magnetic permeability (default 1 everywhere) Returns: @@ -153,12 +151,12 @@ def m2j(omega: complex, return m2j_mu -def e_tfsf_source(TF_region: field_t, +def e_tfsf_source(TF_region: fdfield_t, omega: complex, dxes: dx_lists_t, - epsilon: field_t, - mu: field_t = None, - ) -> field_transform_t: + epsilon: fdfield_t, + mu: fdfield_t = None, + ) -> fdfield_updater_t: """ Operator that turns an E-field distribution into a total-field/scattered-field (TFSF) source. @@ -168,7 +166,7 @@ def e_tfsf_source(TF_region: field_t, (i.e. in the scattered-field region). Should have the same shape as the simulation grid, e.g. `epsilon[0].shape`. omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Dielectric constant distribution mu: Magnetic permeability (default 1 everywhere) @@ -184,7 +182,7 @@ def e_tfsf_source(TF_region: field_t, return neg_iwj / (-1j * omega) -def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[field_t, field_t], field_t]: +def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[fdfield_t, fdfield_t], fdfield_t]: """ Generates a function that takes the single-frequency `E` and `H` fields and calculates the cross product `E` x `H` = \\( E \\times H \\) as required @@ -201,12 +199,12 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[field_t, field_t], field_t instead. Args: - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` Returns: Function `f` that returns E x H as required for the poynting vector. """ - def exh(e: field_t, h: field_t): + def exh(e: fdfield_t, h: fdfield_t): s = numpy.empty_like(e) ex = e[0] * dxes[0][0][:, None, None] ey = e[1] * dxes[0][1][None, :, None] diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 20cf96a..b90ec67 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -9,7 +9,7 @@ E- and H-field values are defined on a Yee cell; `epsilon` values should be calc cells centered at each E component (`mu` at each H component). Many of these functions require a `dxes` parameter, of type `dx_lists_t`; see -the `meanas.types` submodule for details. +the `meanas.fdmath.types` submodule for details. The following operators are included: @@ -31,7 +31,7 @@ from typing import List, Tuple import numpy import scipy.sparse as sparse -from .. import vec, dx_lists_t, vfield_t +from ..fdmath import vec, dx_lists_t, vfdfield_t from ..fdmath.operators import shift_with_mirror, rotation, curl_forward, curl_back @@ -40,10 +40,10 @@ __author__ = 'Jan Petykiewicz' def e_full(omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - pec: vfield_t = None, - pmc: vfield_t = None, + epsilon: vfdfield_t, + mu: vfdfield_t = None, + pec: vfdfield_t = None, + pmc: vfdfield_t = None, ) -> sparse.spmatrix: """ Wave operator @@ -60,7 +60,7 @@ def e_full(omega: complex, Args: omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Vectorized dielectric constant mu: Vectorized magnetic permeability (default 1 everywhere). pec: Vectorized mask specifying PEC cells. Any cells where `pec != 0` are interpreted @@ -107,7 +107,7 @@ def e_full_preconditioners(dxes: dx_lists_t The preconditioner matrices are diagonal and complex, with `Pr = 1 / Pl` Args: - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` Returns: Preconditioner matrices `(Pl, Pr)`. @@ -124,10 +124,10 @@ def e_full_preconditioners(dxes: dx_lists_t def h_full(omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - pec: vfield_t = None, - pmc: vfield_t = None, + epsilon: vfdfield_t, + mu: vfdfield_t = None, + pec: vfdfield_t = None, + pmc: vfdfield_t = None, ) -> sparse.spmatrix: """ Wave operator @@ -142,7 +142,7 @@ def h_full(omega: complex, Args: omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Vectorized dielectric constant mu: Vectorized magnetic permeability (default 1 everywhere) pec: Vectorized mask specifying PEC cells. Any cells where `pec != 0` are interpreted @@ -180,10 +180,10 @@ def h_full(omega: complex, def eh_full(omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - pec: vfield_t = None, - pmc: vfield_t = None + epsilon: vfdfield_t, + mu: vfdfield_t = None, + pec: vfdfield_t = None, + pmc: vfdfield_t = None ) -> sparse.spmatrix: """ Wave operator for `[E, H]` field representation. This operator implements Maxwell's @@ -210,7 +210,7 @@ def eh_full(omega: complex, Args: omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Vectorized dielectric constant mu: Vectorized magnetic permeability (default 1 everywhere) pec: Vectorized mask specifying PEC cells. Any cells where `pec != 0` are interpreted @@ -249,8 +249,8 @@ def eh_full(omega: complex, def e2h(omega: complex, dxes: dx_lists_t, - mu: vfield_t = None, - pmc: vfield_t = None, + mu: vfdfield_t = None, + pmc: vfdfield_t = None, ) -> sparse.spmatrix: """ Utility operator for converting the E field into the H field. @@ -258,7 +258,7 @@ def e2h(omega: complex, Args: omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` mu: Vectorized magnetic permeability (default 1 everywhere) pmc: Vectorized mask specifying PMC cells. Any cells where `pmc != 0` are interpreted as containing a perfect magnetic conductor (PMC). @@ -280,7 +280,7 @@ def e2h(omega: complex, def m2j(omega: complex, dxes: dx_lists_t, - mu: vfield_t = None + mu: vfdfield_t = None ) -> sparse.spmatrix: """ Operator for converting a magnetic current M into an electric current J. @@ -288,7 +288,7 @@ def m2j(omega: complex, Args: omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` mu: Vectorized magnetic permeability (default 1 everywhere) Returns: @@ -302,14 +302,14 @@ def m2j(omega: complex, return op -def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: +def poynting_e_cross(e: vfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ Operator for computing the Poynting vector, containing the (E x) portion of the Poynting vector. Args: e: Vectorized E-field for the ExH cross product - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` Returns: Sparse matrix containing (E x) portion of Poynting cross product. @@ -331,13 +331,13 @@ def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: return P -def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: +def poynting_h_cross(h: vfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector. Args: h: Vectorized H-field for the HxE cross product - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` Returns: Sparse matrix containing (H x) portion of Poynting cross product. @@ -358,11 +358,11 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: return P -def e_tfsf_source(TF_region: vfield_t, +def e_tfsf_source(TF_region: vfdfield_t, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, + epsilon: vfdfield_t, + mu: vfdfield_t = None, ) -> sparse.spmatrix: """ Operator that turns a desired E-field distribution into a @@ -374,7 +374,7 @@ def e_tfsf_source(TF_region: vfield_t, TF_region: Mask, which is set to 1 inside the total-field region and 0 in the scattered-field region omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Vectorized dielectric constant mu: Vectorized magnetic permeability (default 1 everywhere). @@ -388,11 +388,11 @@ def e_tfsf_source(TF_region: vfield_t, return (A @ Q - Q @ A) / (-1j * omega) -def e_boundary_source(mask: vfield_t, +def e_boundary_source(mask: vfdfield_t, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, + epsilon: vfdfield_t, + mu: vfdfield_t = None, periodic_mask_edges: bool = False, ) -> sparse.spmatrix: """ @@ -405,7 +405,7 @@ def e_boundary_source(mask: vfield_t, i.e. any points where shifting the mask by one cell in any direction would change its value. omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Vectorized dielectric constant mu: Vectorized magnetic permeability (default 1 everywhere). diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py index c9a93e6..7e7694d 100644 --- a/meanas/fdfd/scpml.py +++ b/meanas/fdfd/scpml.py @@ -5,14 +5,14 @@ Functions for creating stretched coordinate perfectly matched layer (PML) absorb from typing import List, Callable import numpy -from .. import dx_lists_t +from ..fdmath import dx_lists_t __author__ = 'Jan Petykiewicz' s_function_t = Callable[[float], float] -"""Typedef for s-functions""" +"""Typedef for s-functions, see `prepare_s_function()`""" def prepare_s_function(ln_R: float = -16, @@ -63,7 +63,7 @@ def uniform_grid_scpml(shape: numpy.ndarray or List[int], Default uses `prepare_s_function()` with no parameters. Returns: - Complex cell widths (dx_lists_t) as discussed in `meanas.types`. + Complex cell widths (dx_lists_t) as discussed in `meanas.fdmath.types`. """ if s_function is None: s_function = prepare_s_function() @@ -102,7 +102,7 @@ def stretch_with_scpml(dxes: dx_lists_t, Stretch dxes to contain a stretched-coordinate PML (SCPML) in one direction along one axis. Args: - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` axis: axis to stretch (0=x, 1=y, 2=z) polarity: direction to stretch (-1 for -ve, +1 for +ve) omega: Angular frequency for the simulation @@ -113,7 +113,7 @@ def stretch_with_scpml(dxes: dx_lists_t, of pml parameters. Default uses `prepare_s_function()` with no parameters. Returns: - Complex cell widths (dx_lists_t) as discussed in `meanas.types`. + Complex cell widths (dx_lists_t) as discussed in `meanas.fdmath.types`. Multiple calls to this function may be necessary if multiple absorpbing boundaries are needed. """ if s_function is None: diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py index aa9633a..ff5bfe3 100644 --- a/meanas/fdfd/solvers.py +++ b/meanas/fdfd/solvers.py @@ -9,6 +9,7 @@ import numpy from numpy.linalg import norm import scipy.sparse.linalg +from ..fdmath import dx_lists_t, vfdfield_t from . import operators @@ -60,16 +61,16 @@ def _scipy_qmr(A: scipy.sparse.csr_matrix, def generic(omega: complex, - dxes: List[List[numpy.ndarray]], - J: numpy.ndarray, - epsilon: numpy.ndarray, - mu: numpy.ndarray = None, - pec: numpy.ndarray = None, - pmc: numpy.ndarray = None, + dxes: dx_lists_t, + J: vfdfield_t, + epsilon: vfdfield_t, + mu: vfdfield_t = None, + pec: vfdfield_t = None, + pmc: vfdfield_t = None, adjoint: bool = False, matrix_solver: Callable[..., numpy.ndarray] = _scipy_qmr, matrix_solver_opts: Dict[str, Any] = None, - ) -> numpy.ndarray: + ) -> vfdfield_t: """ Conjugate gradient FDFD solver using CSR sparse matrices. @@ -78,7 +79,7 @@ def generic(omega: complex, Args: omega: Complex frequency to solve at. dxes: `[[dx_e, dy_e, dz_e], [dx_h, dy_h, dz_h]]` (complex cell sizes) as - discussed in `meanas.types` + discussed in `meanas.fdmath.types` J: Electric current distribution (at E-field locations) epsilon: Dielectric constant distribution (at E-field locations) mu: Magnetic permeability distribution (at H-field locations) diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 6a7f24f..ecd40de 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -14,7 +14,8 @@ import numpy from numpy.linalg import norm import scipy.sparse as sparse -from .. import vec, unvec, dx_lists_t, field_t, vfield_t +from ..fdmath.operators import deriv_forward, deriv_back, curl_forward, curl_back, cross +from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration from . import operators @@ -24,8 +25,8 @@ __author__ = 'Jan Petykiewicz' def operator_e(omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, + epsilon: vfdfield_t, + mu: vfdfield_t = None, ) -> sparse.spmatrix: """ Waveguide operator of the form @@ -66,7 +67,7 @@ def operator_e(omega: complex, Args: omega: The angular frequency of the system. - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid mu: Vectorized magnetic permeability grid (default 1 everywhere) @@ -76,8 +77,8 @@ def operator_e(omega: complex, if numpy.any(numpy.equal(mu, None)): mu = numpy.ones_like(epsilon) - Dfx, Dfy = operators.deriv_forward(dxes[0]) - Dbx, Dby = operators.deriv_back(dxes[1]) + Dfx, Dfy = deriv_forward(dxes[0]) + Dbx, Dby = deriv_back(dxes[1]) eps_parts = numpy.split(epsilon, 3) eps_xy = sparse.diags(numpy.hstack((eps_parts[0], eps_parts[1]))) @@ -95,8 +96,8 @@ def operator_e(omega: complex, def operator_h(omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, + epsilon: vfdfield_t, + mu: vfdfield_t = None, ) -> sparse.spmatrix: """ Waveguide operator of the form @@ -137,7 +138,7 @@ def operator_h(omega: complex, Args: omega: The angular frequency of the system. - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid mu: Vectorized magnetic permeability grid (default 1 everywhere) @@ -169,10 +170,10 @@ def normalized_fields_e(e_xy: numpy.ndarray, wavenumber: complex, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, + epsilon: vfdfield_t, + mu: vfdfield_t = None, prop_phase: float = 0, - ) -> Tuple[vfield_t, vfield_t]: + ) -> Tuple[vfdfield_t, vfdfield_t]: """ Given a vector `e_xy` containing the vectorized E_x and E_y fields, returns normalized, vectorized E and H fields for the system. @@ -182,7 +183,7 @@ def normalized_fields_e(e_xy: numpy.ndarray, wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` omega: The angular frequency of the system - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid mu: Vectorized magnetic permeability grid (default 1 everywhere) prop_phase: Phase shift `(dz * corrected_wavenumber)` over 1 cell in propagation direction. @@ -203,10 +204,10 @@ def normalized_fields_h(h_xy: numpy.ndarray, wavenumber: complex, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, + epsilon: vfdfield_t, + mu: vfdfield_t = None, prop_phase: float = 0, - ) -> Tuple[vfield_t, vfield_t]: + ) -> Tuple[vfdfield_t, vfdfield_t]: """ Given a vector `h_xy` containing the vectorized H_x and H_y fields, returns normalized, vectorized E and H fields for the system. @@ -216,7 +217,7 @@ def normalized_fields_h(h_xy: numpy.ndarray, wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. It should satisfy `operator_h() @ h_xy == wavenumber**2 * h_xy` omega: The angular frequency of the system - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid mu: Vectorized magnetic permeability grid (default 1 everywhere) prop_phase: Phase shift `(dz * corrected_wavenumber)` over 1 cell in propagation direction. @@ -237,10 +238,10 @@ def _normalized_fields(e: numpy.ndarray, h: numpy.ndarray, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, + epsilon: vfdfield_t, + mu: vfdfield_t = None, prop_phase: float = 0, - ) -> Tuple[vfield_t, vfield_t]: + ) -> Tuple[vfdfield_t, vfdfield_t]: # TODO documentation shape = [s.size for s in dxes[0]] dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)] @@ -276,8 +277,8 @@ def _normalized_fields(e: numpy.ndarray, def exy2h(wavenumber: complex, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None + epsilon: vfdfield_t, + mu: vfdfield_t = None ) -> sparse.spmatrix: """ Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, @@ -287,7 +288,7 @@ def exy2h(wavenumber: complex, wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` omega: The angular frequency of the system - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid mu: Vectorized magnetic permeability grid (default 1 everywhere) @@ -301,8 +302,8 @@ def exy2h(wavenumber: complex, def hxy2e(wavenumber: complex, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None + epsilon: vfdfield_t, + mu: vfdfield_t = None ) -> sparse.spmatrix: """ Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields, @@ -312,7 +313,7 @@ def hxy2e(wavenumber: complex, wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. It should satisfy `operator_h() @ h_xy == wavenumber**2 * h_xy` omega: The angular frequency of the system - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid mu: Vectorized magnetic permeability grid (default 1 everywhere) @@ -325,7 +326,7 @@ def hxy2e(wavenumber: complex, def hxy2h(wavenumber: complex, dxes: dx_lists_t, - mu: vfield_t = None + mu: vfdfield_t = None ) -> sparse.spmatrix: """ Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields, @@ -334,13 +335,13 @@ def hxy2h(wavenumber: complex, Args: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`. It should satisfy `operator_h() @ h_xy == wavenumber**2 * h_xy` - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) mu: Vectorized magnetic permeability grid (default 1 everywhere) Returns: Sparse matrix representing the operator. """ - Dfx, Dfy = operators.deriv_forward(dxes[0]) + Dfx, Dfy = deriv_forward(dxes[0]) hxy2hz = sparse.hstack((Dfx, Dfy)) / (1j * wavenumber) if not numpy.any(numpy.equal(mu, None)): @@ -358,7 +359,7 @@ def hxy2h(wavenumber: complex, def exy2e(wavenumber: complex, dxes: dx_lists_t, - epsilon: vfield_t, + epsilon: vfdfield_t, ) -> sparse.spmatrix: """ Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, @@ -367,13 +368,13 @@ def exy2e(wavenumber: complex, Args: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid Returns: Sparse matrix representing the operator. """ - Dbx, Dby = operators.deriv_back(dxes[1]) + Dbx, Dby = deriv_back(dxes[1]) exy2ez = sparse.hstack((Dbx, Dby)) / (1j * wavenumber) if not numpy.any(numpy.equal(epsilon, None)): @@ -392,7 +393,7 @@ def exy2e(wavenumber: complex, def e2h(wavenumber: complex, omega: complex, dxes: dx_lists_t, - mu: vfield_t = None + mu: vfdfield_t = None ) -> sparse.spmatrix: """ Returns an operator which, when applied to a vectorized E eigenfield, produces @@ -401,7 +402,7 @@ def e2h(wavenumber: complex, Args: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` omega: The angular frequency of the system - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) mu: Vectorized magnetic permeability grid (default 1 everywhere) Returns: @@ -416,7 +417,7 @@ def e2h(wavenumber: complex, def h2e(wavenumber: complex, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t + epsilon: vfdfield_t ) -> sparse.spmatrix: """ Returns an operator which, when applied to a vectorized H eigenfield, produces @@ -425,7 +426,7 @@ def h2e(wavenumber: complex, Args: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` omega: The angular frequency of the system - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid Returns: @@ -441,7 +442,7 @@ def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: Args: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) Return: Sparse matrix representation of the operator. @@ -450,9 +451,10 @@ def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: for d in dxes[0]: n *= len(d) + print(wavenumber, n) Bz = -1j * wavenumber * sparse.eye(n) - Dfx, Dfy = operators.deriv_forward(dxes[0]) - return operators.cross([Dfx, Dfy, Bz]) + Dfx, Dfy = deriv_forward(dxes[0]) + return cross([Dfx, Dfy, Bz]) def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: @@ -461,7 +463,7 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: Args: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) Return: Sparse matrix representation of the operator. @@ -471,16 +473,16 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: n *= len(d) Bz = -1j * wavenumber * sparse.eye(n) - Dbx, Dby = operators.deriv_back(dxes[1]) - return operators.cross([Dbx, Dby, Bz]) + Dbx, Dby = deriv_back(dxes[1]) + return cross([Dbx, Dby, Bz]) -def h_err(h: vfield_t, +def h_err(h: vfdfield_t, wavenumber: complex, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None + epsilon: vfdfield_t, + mu: vfdfield_t = None ) -> float: """ Calculates the relative error in the H field @@ -489,7 +491,7 @@ def h_err(h: vfield_t, h: Vectorized H field wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` omega: The angular frequency of the system - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid mu: Vectorized magnetic permeability grid (default 1 everywhere) @@ -509,12 +511,12 @@ def h_err(h: vfield_t, return norm(op) / norm(h) -def e_err(e: vfield_t, +def e_err(e: vfdfield_t, wavenumber: complex, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None + epsilon: vfdfield_t, + mu: vfdfield_t = None ) -> float: """ Calculates the relative error in the E field @@ -523,7 +525,7 @@ def e_err(e: vfield_t, e: Vectorized E field wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` omega: The angular frequency of the system - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid mu: Vectorized magnetic permeability grid (default 1 everywhere) @@ -545,17 +547,17 @@ def e_err(e: vfield_t, def solve_modes(mode_numbers: List[int], omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, + epsilon: vfdfield_t, + mu: vfdfield_t = None, mode_margin: int = 2, - ) -> Tuple[List[vfield_t], List[complex]]: + ) -> Tuple[List[vfdfield_t], List[complex]]: """ Given a 2D region, attempts to solve for the eigenmode with the specified mode numbers. Args: mode_numbers: List of 0-indexed mode numbers to solve for omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Dielectric constant mu: Magnetic permeability (default 1 everywhere) mode_margin: The eigensolver will actually solve for `(max(mode_number) + mode_margin)` @@ -570,17 +572,18 @@ def solve_modes(mode_numbers: List[int], Solve for the largest-magnitude eigenvalue of the real operator ''' dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] - A_r = waveguide.operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) + A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) - eigvals, eigvecs = signed_eigensolve(A_r, max(mode_number) + mode_margin) - e_xys = eigvecs[:, -(numpy.array(mode_number) + 1)] + eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin) + e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)] ''' Now solve for the eigenvector of the full operator, using the real operator's eigenvector as an initial guess for Rayleigh quotient iteration. ''' - A = waveguide.operator_e(omega, dxes, epsilon, mu) - eigvals, e_xys = rayleigh_quotient_iteration(A, e_xys) + A = operator_e(omega, dxes, epsilon, mu) + for nn in range(len(mode_numbers)): + eigvals[nn], e_xys[:, nn] = rayleigh_quotient_iteration(A, e_xys[:, nn]) # Calculate the wave-vector (force the real part to be positive) wavenumbers = numpy.sqrt(eigvals) @@ -592,7 +595,7 @@ def solve_modes(mode_numbers: List[int], def solve_mode(mode_number: int, *args, **kwargs - ) -> Tuple[vfield_t, complex]: + ) -> Tuple[vfdfield_t, complex]: """ Wrapper around `solve_modes()` that solves for a single mode. @@ -604,4 +607,5 @@ def solve_mode(mode_number: int, Returns: (e_xy, wavenumber) """ - return solve_modes(mode_numbers=[mode_number], *args, **kwargs) + e_xys, wavenumbers = solve_modes(mode_numbers=[mode_number], *args, **kwargs) + return e_xys[:, 0], wavenumbers[0] diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index 18727ce..02fb7fd 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -8,7 +8,7 @@ from typing import Dict, List, Tuple import numpy import scipy.sparse as sparse -from .. import vec, unvec, dx_lists_t, vfield_t, field_t +from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, fdfield_t from . import operators, waveguide_2d, functional @@ -18,8 +18,8 @@ def solve_mode(mode_number: int, axis: int, polarity: int, slices: List[slice], - epsilon: field_t, - mu: field_t = None, + epsilon: fdfield_t, + mu: fdfield_t = None, ) -> Dict[str, complex or numpy.ndarray]: """ Given a 3D grid, selects a slice from the grid and attempts to @@ -28,7 +28,7 @@ def solve_mode(mode_number: int, Args: mode_number: Number of the mode, 0-indexed omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` axis: Propagation axis (0=x, 1=y, 2=z) polarity: Propagation direction (+1 for +ve, -1 for -ve) slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use @@ -71,7 +71,7 @@ def solve_mode(mode_number: int, wavenumber = 2/dx_prop * numpy.arcsin(wavenumber_2d * dx_prop/2) shape = [d.size for d in args_2d['dxes'][0]] - ve, vh = waveguide.normalized_fields_e(e_xy, wavenumber=wavenumber_2d, **args_2d, prop_phase=dx_prop * wavenumber) + ve, vh = waveguide_2d.normalized_fields_e(e_xy, wavenumber=wavenumber_2d, **args_2d, prop_phase=dx_prop * wavenumber) e = unvec(ve, shape) h = unvec(vh, shape) @@ -98,16 +98,16 @@ def solve_mode(mode_number: int, return results -def compute_source(E: field_t, +def compute_source(E: fdfield_t, wavenumber: complex, omega: complex, dxes: dx_lists_t, axis: int, polarity: int, slices: List[slice], - epsilon: field_t, - mu: field_t = None, - ) -> field_t: + epsilon: fdfield_t, + mu: fdfield_t = None, + ) -> fdfield_t: """ Given an eigenmode obtained by `solve_mode`, returns the current source distribution necessary to position a unidirectional source at the slice location. @@ -116,7 +116,7 @@ def compute_source(E: field_t, E: E-field of the mode wavenumber: Wavenumber of the mode omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` axis: Propagation axis (0=x, 1=y, 2=z) polarity: Propagation direction (+1 for +ve, -1 for -ve) slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use @@ -143,13 +143,13 @@ def compute_source(E: field_t, return J -def compute_overlap_e(E: field_t, +def compute_overlap_e(E: fdfield_t, wavenumber: complex, dxes: dx_lists_t, axis: int, polarity: int, slices: List[slice], - ) -> field_t: # TODO DOCS + ) -> fdfield_t: # TODO DOCS """ Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) @@ -160,7 +160,7 @@ def compute_overlap_e(E: field_t, H: H-field of the mode (advanced by half of a Yee cell from E) wavenumber: Wavenumber of the mode omega: Angular frequency of the simulation - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` axis: Propagation axis (0=x, 1=y, 2=z) polarity: Propagation direction (+1 for +ve, -1 for -ve) slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use @@ -188,13 +188,13 @@ def compute_overlap_e(E: field_t, return Etgt -def expand_e(E: field_t, +def expand_e(E: fdfield_t, wavenumber: complex, dxes: dx_lists_t, axis: int, polarity: int, slices: List[slice], - ) -> field_t: + ) -> fdfield_t: """ Given an eigenmode obtained by `solve_mode`, expands the E-field from the 2D slice where the mode was calculated to the entire domain (along the propagation @@ -205,7 +205,7 @@ def expand_e(E: field_t, Args: E: E-field of the mode wavenumber: Wavenumber of the mode - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` axis: Propagation axis (0=x, 1=y, 2=z) polarity: Propagation direction (+1 for +ve, -1 for -ve) slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index ebfb41d..0995984 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -13,7 +13,7 @@ import numpy from numpy.linalg import norm import scipy.sparse as sparse -from .. import vec, unvec, dx_lists_t, field_t, vfield_t +from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration from . import operators @@ -23,7 +23,7 @@ __author__ = 'Jan Petykiewicz' def cylindrical_operator(omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, + epsilon: vfdfield_t, r0: float, ) -> sparse.spmatrix: """ @@ -41,7 +41,7 @@ def cylindrical_operator(omega: complex, Args: omega: The angular frequency of the system - dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.types` (2D) + dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) epsilon: Vectorized dielectric constant grid r0: Radius of curvature for the simulation. This should be the minimum value of r within the simulation domain. @@ -83,9 +83,9 @@ def cylindrical_operator(omega: complex, def solve_mode(mode_number: int, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, + epsilon: vfdfield_t, r0: float, - ) -> Dict[str, complex or field_t]: + ) -> Dict[str, complex or fdfield_t]: """ TODO: fixup Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode @@ -94,7 +94,7 @@ def solve_mode(mode_number: int, Args: mode_number: Number of the mode, 0-indexed omega: Angular frequency of the simulation - dxes: Grid parameters [dx_e, dx_h] as described in meanas.types. + dxes: Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types. The first coordinate is assumed to be r, the second is y. epsilon: Dielectric constant r0: Radius of curvature for the simulation. This should be the minimum value of diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 74deff1..f70634f 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -91,4 +91,12 @@ and while \\( \\hat{h} \\) and \\( \\tilde{h} \\) are located at \\((m \pm \\frac{1}{2}, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) with components at \\((m, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) etc. +TODO: Explain fdfield_t vs vfdfield_t / operators vs functional +TODO: explain dxes + """ + +from .types import fdfield_t, vfdfield_t, dx_lists_t, fdfield_updater_t +from .vectorization import vec, unvec +from . import operators, functional, types, vectorization + diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py index 8593d4a..fc5b0ca 100644 --- a/meanas/fdmath/functional.py +++ b/meanas/fdmath/functional.py @@ -6,10 +6,10 @@ Basic discrete calculus etc. from typing import List, Callable, Tuple, Dict import numpy -from .. import field_t, field_updater +from .types import fdfield_t, fdfield_updater_t -def deriv_forward(dx_e: List[numpy.ndarray] = None) -> field_updater: +def deriv_forward(dx_e: List[numpy.ndarray] = None) -> fdfield_updater_t: """ Utility operators for taking discretized derivatives (backward variant). @@ -31,7 +31,7 @@ def deriv_forward(dx_e: List[numpy.ndarray] = None) -> field_updater: return derivs -def deriv_back(dx_h: List[numpy.ndarray] = None) -> field_updater: +def deriv_back(dx_h: List[numpy.ndarray] = None) -> fdfield_updater_t: """ Utility operators for taking discretized derivatives (forward variant). @@ -53,7 +53,7 @@ def deriv_back(dx_h: List[numpy.ndarray] = None) -> field_updater: return derivs -def curl_forward(dx_e: List[numpy.ndarray] = None) -> field_updater: +def curl_forward(dx_e: List[numpy.ndarray] = None) -> fdfield_updater_t: """ Curl operator for use with the E field. @@ -67,7 +67,7 @@ def curl_forward(dx_e: List[numpy.ndarray] = None) -> field_updater: """ Dx, Dy, Dz = deriv_forward(dx_e) - def ce_fun(e: field_t) -> field_t: + def ce_fun(e: fdfield_t) -> fdfield_t: output = numpy.empty_like(e) output[0] = Dy(e[2]) output[1] = Dz(e[0]) @@ -80,7 +80,7 @@ def curl_forward(dx_e: List[numpy.ndarray] = None) -> field_updater: return ce_fun -def curl_back(dx_h: List[numpy.ndarray] = None) -> field_updater: +def curl_back(dx_h: List[numpy.ndarray] = None) -> fdfield_updater_t: """ Create a function which takes the backward curl of a field. @@ -94,7 +94,7 @@ def curl_back(dx_h: List[numpy.ndarray] = None) -> field_updater: """ Dx, Dy, Dz = deriv_back(dx_h) - def ch_fun(h: field_t) -> field_t: + def ch_fun(h: fdfield_t) -> fdfield_t: output = numpy.empty_like(h) output[0] = Dy(h[2]) output[1] = Dz(h[0]) diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py index 64f04aa..3f83c8c 100644 --- a/meanas/fdmath/operators.py +++ b/meanas/fdmath/operators.py @@ -7,7 +7,7 @@ from typing import List, Callable, Tuple, Dict import numpy import scipy.sparse as sparse -from .. import field_t, vfield_t +from .types import fdfield_t, vfdfield_t def rotation(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmatrix: @@ -155,7 +155,7 @@ def cross(B: List[sparse.spmatrix]) -> sparse.spmatrix: [-B[1], B[0], zero]]) -def vec_cross(b: vfield_t) -> sparse.spmatrix: +def vec_cross(b: vfdfield_t) -> sparse.spmatrix: """ Vector cross product operator diff --git a/meanas/types.py b/meanas/fdmath/types.py similarity index 89% rename from meanas/types.py rename to meanas/fdmath/types.py index 2a1351f..6e2fd63 100644 --- a/meanas/types.py +++ b/meanas/fdmath/types.py @@ -8,7 +8,7 @@ from typing import List, Callable # Field types # TODO: figure out a better way to set the docstrings without creating actual subclasses? # Probably not a big issue since they're only used for type hinting -class field_t(numpy.ndarray): +class fdfield_t(numpy.ndarray): """ Vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`) @@ -16,7 +16,7 @@ class field_t(numpy.ndarray): """ pass -class vfield_t(numpy.ndarray): +class vfdfield_t(numpy.ndarray): """ Linearized vector field (single vector of length 3*X*Y*Z) @@ -37,4 +37,4 @@ dx_lists_t = List[List[numpy.ndarray]] ''' -field_updater = Callable[[field_t], field_t] +fdfield_updater_t = Callable[[fdfield_t], fdfield_t] diff --git a/meanas/vectorization.py b/meanas/fdmath/vectorization.py similarity index 90% rename from meanas/vectorization.py rename to meanas/fdmath/vectorization.py index fd6cdcf..8e8099b 100644 --- a/meanas/vectorization.py +++ b/meanas/fdmath/vectorization.py @@ -7,13 +7,13 @@ Vectorized versions of the field use row-major (ie., C-style) ordering. from typing import List import numpy -from .types import field_t, vfield_t +from .types import fdfield_t, vfdfield_t __author__ = 'Jan Petykiewicz' -def vec(f: field_t) -> vfield_t: +def vec(f: fdfield_t) -> vfdfield_t: """ Create a 1D ndarray from a 3D vector field which spans a 1-3D region. @@ -28,7 +28,7 @@ def vec(f: field_t) -> vfield_t: return numpy.ravel(f, order='C') -def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: +def unvec(v: vfdfield_t, shape: numpy.ndarray) -> fdfield_t: """ Perform the inverse of vec(): take a 1D ndarray and output a 3D field of form [f_x, f_y, f_z] where each of f_* is a len(shape)-dimensional diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py index 87a234b..fa478ba 100644 --- a/meanas/fdtd/base.py +++ b/meanas/fdtd/base.py @@ -4,27 +4,33 @@ Basic FDTD field updates from typing import List, Callable, Tuple, Dict import numpy -from .. import dx_lists_t, field_t, field_updater +from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t from ..fdmath.functional import curl_forward, curl_back __author__ = 'Jan Petykiewicz' -def maxwell_e(dt: float, dxes: dx_lists_t = None) -> field_updater: - curl_h_fun = curl_back(dxes[1]) +def maxwell_e(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t: + if dxes is not None: + curl_h_fun = curl_back(dxes[1]) + else: + curl_h_fun = curl_back() - def me_fun(e: field_t, h: field_t, epsilon: field_t): + def me_fun(e: fdfield_t, h: fdfield_t, epsilon: fdfield_t): e += dt * curl_h_fun(h) / epsilon return e return me_fun -def maxwell_h(dt: float, dxes: dx_lists_t = None) -> field_updater: - curl_e_fun = curl_forward(dxes[0]) +def maxwell_h(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t: + if dxes is not None: + curl_e_fun = curl_forward(dxes[0]) + else: + curl_e_fun = curl_forward() - def mh_fun(e: field_t, h: field_t): + def mh_fun(e: fdfield_t, h: fdfield_t): h -= dt * curl_e_fun(e) return h diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py index cba1797..0f0b1a6 100644 --- a/meanas/fdtd/boundaries.py +++ b/meanas/fdtd/boundaries.py @@ -5,12 +5,12 @@ Boundary conditions from typing import List, Callable, Tuple, Dict import numpy -from .. import dx_lists_t, field_t, field_updater +from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t def conducting_boundary(direction: int, polarity: int - ) -> Tuple[field_updater, field_updater]: + ) -> Tuple[fdfield_updater_t, fdfield_updater_t]: dirs = [0, 1, 2] if direction not in dirs: raise Exception('Invalid direction: {}'.format(direction)) @@ -23,13 +23,13 @@ def conducting_boundary(direction: int, boundary_slice[direction] = 0 shifted1_slice[direction] = 1 - def en(e: field_t): + def en(e: fdfield_t): e[direction][boundary_slice] = 0 e[u][boundary_slice] = e[u][shifted1_slice] e[v][boundary_slice] = e[v][shifted1_slice] return e - def hn(h: field_t): + def hn(h: fdfield_t): h[direction][boundary_slice] = h[direction][shifted1_slice] h[u][boundary_slice] = 0 h[v][boundary_slice] = 0 @@ -45,14 +45,14 @@ def conducting_boundary(direction: int, shifted1_slice[direction] = -2 shifted2_slice[direction] = -3 - def ep(e: field_t): + def ep(e: fdfield_t): e[direction][boundary_slice] = -e[direction][shifted2_slice] e[direction][shifted1_slice] = 0 e[u][boundary_slice] = e[u][shifted1_slice] e[v][boundary_slice] = e[v][shifted1_slice] return e - def hp(h: field_t): + def hp(h: fdfield_t): h[direction][boundary_slice] = h[direction][shifted1_slice] h[u][boundary_slice] = -h[u][shifted2_slice] h[u][shifted1_slice] = 0 diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index 608137f..41268b7 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -2,12 +2,13 @@ from typing import List, Callable, Tuple, Dict import numpy -from .. import dx_lists_t, field_t, field_updater, fdmath +from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t +from ..fdmath.functional import deriv_back, deriv_forward -def poynting(e: field_t, - h: field_t, +def poynting(e: fdfield_t, + h: fdfield_t, dxes: dx_lists_t = None, - ) -> field_t: + ) -> fdfield_t: if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) @@ -25,51 +26,51 @@ def poynting(e: field_t, return s -def poynting_divergence(s: field_t = None, +def poynting_divergence(s: fdfield_t = None, *, - e: field_t = None, - h: field_t = None, + e: fdfield_t = None, + h: fdfield_t = None, dxes: dx_lists_t = None, - ) -> field_t: + ) -> fdfield_t: if s is None: s = poynting(e, h, dxes=dxes) - Dx, Dy, Dz = fdmath.functional.deriv_back() + Dx, Dy, Dz = deriv_back() ds = Dx(s[0]) + Dy(s[1]) + Dz(s[2]) return ds -def energy_hstep(e0: field_t, - h1: field_t, - e2: field_t, - epsilon: field_t = None, - mu: field_t = None, +def energy_hstep(e0: fdfield_t, + h1: fdfield_t, + e2: fdfield_t, + epsilon: fdfield_t = None, + mu: fdfield_t = None, dxes: dx_lists_t = None, - ) -> field_t: + ) -> fdfield_t: u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) return u -def energy_estep(h0: field_t, - e1: field_t, - h2: field_t, - epsilon: field_t = None, - mu: field_t = None, +def energy_estep(h0: fdfield_t, + e1: fdfield_t, + h2: fdfield_t, + epsilon: fdfield_t = None, + mu: fdfield_t = None, dxes: dx_lists_t = None, - ) -> field_t: + ) -> fdfield_t: u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) return u def delta_energy_h2e(dt: float, - e0: field_t, - h1: field_t, - e2: field_t, - h3: field_t, - epsilon: field_t = None, - mu: field_t = None, + e0: fdfield_t, + h1: fdfield_t, + e2: fdfield_t, + h3: fdfield_t, + epsilon: fdfield_t = None, + mu: fdfield_t = None, dxes: dx_lists_t = None, - ) -> field_t: + ) -> fdfield_t: """ This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) """ @@ -80,14 +81,14 @@ def delta_energy_h2e(dt: float, def delta_energy_e2h(dt: float, - h0: field_t, - e1: field_t, - h2: field_t, - e3: field_t, - epsilon: field_t = None, - mu: field_t = None, + h0: fdfield_t, + e1: fdfield_t, + h2: fdfield_t, + e3: fdfield_t, + epsilon: fdfield_t = None, + mu: fdfield_t = None, dxes: dx_lists_t = None, - ) -> field_t: + ) -> fdfield_t: """ This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) """ @@ -97,7 +98,7 @@ def delta_energy_e2h(dt: float, return du -def delta_energy_j(j0: field_t, e1: field_t, dxes: dx_lists_t = None) -> field_t: +def delta_energy_j(j0: fdfield_t, e1: fdfield_t, dxes: dx_lists_t = None) -> fdfield_t: if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) @@ -108,12 +109,12 @@ def delta_energy_j(j0: field_t, e1: field_t, dxes: dx_lists_t = None) -> field_t return du -def dxmul(ee: field_t, - hh: field_t, - epsilon: field_t = None, - mu: field_t = None, +def dxmul(ee: fdfield_t, + hh: fdfield_t, + epsilon: fdfield_t = None, + mu: fdfield_t = None, dxes: dx_lists_t = None - ) -> field_t: + ) -> fdfield_t: if epsilon is None: epsilon = 1 if mu is None: diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py index 7d73e38..af15a5a 100644 --- a/meanas/fdtd/pml.py +++ b/meanas/fdtd/pml.py @@ -7,7 +7,7 @@ PML implementations from typing import List, Callable, Tuple, Dict import numpy -from .. import dx_lists_t, field_t, field_updater +from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t __author__ = 'Jan Petykiewicz' @@ -16,7 +16,7 @@ __author__ = 'Jan Petykiewicz' def cpml(direction: int, polarity: int, dt: float, - epsilon: field_t, + epsilon: fdfield_t, thickness: int = 8, ln_R_per_layer: float = -1.6, epsilon_eff: float = 1, @@ -25,7 +25,7 @@ def cpml(direction: int, ma: float = 1, cfs_alpha: float = 0, dtype: numpy.dtype = numpy.float32, - ) -> Tuple[Callable, Callable, Dict[str, field_t]]: + ) -> Tuple[Callable, Callable, Dict[str, fdfield_t]]: if direction not in range(3): raise Exception('Invalid direction: {}'.format(direction)) @@ -58,6 +58,7 @@ def cpml(direction: int, expand_slice = [None] * 3 expand_slice[direction] = slice(None) + expand_slice = tuple(expand_slice) def par(x): scaling = (x / thickness) ** m @@ -79,6 +80,7 @@ def cpml(direction: int, region[direction] = slice(-thickness, None) else: raise Exception('Bad polarity!') + region = tuple(region) se = 1 if direction == 1 else -1 @@ -97,7 +99,7 @@ def cpml(direction: int, # Note that this is kinda slow -- would be faster to reuse dHv*p2h for the original # H update, but then you have multiple arrays and a monolithic (field + pml) update operation - def pml_e(e: field_t, h: field_t, epsilon: field_t) -> Tuple[field_t, field_t]: + def pml_e(e: fdfield_t, h: fdfield_t, epsilon: fdfield_t) -> Tuple[fdfield_t, fdfield_t]: dHv = h[v][region] - numpy.roll(h[v], 1, axis=direction)[region] dHu = h[u][region] - numpy.roll(h[u], 1, axis=direction)[region] psi_e[0] *= p0e @@ -108,7 +110,7 @@ def cpml(direction: int, e[v][region] -= se * dt / epsilon[v][region] * (psi_e[1] + (p2e - 1) * dHu) return e, h - def pml_h(e: field_t, h: field_t) -> Tuple[field_t, field_t]: + def pml_h(e: fdfield_t, h: fdfield_t) -> Tuple[fdfield_t, fdfield_t]: dEv = (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region]) dEu = (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region]) psi_h[0] *= p0h diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index ea6f417..2cf0c44 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -5,7 +5,8 @@ import pytest import numpy #from numpy.testing import assert_allclose, assert_array_equal -from .. import fdfd, vec, unvec +from .. import fdfd +from ..fdmath import vec, unvec from .utils import assert_close, assert_fields_close @@ -20,6 +21,10 @@ def test_poynting_planes(sim): mask = (sim.j != 0).any(axis=0) if mask.sum() != 2: pytest.skip(f'test_poynting_planes will only test 2-point sources, got {mask.sum()}') +# for dxg in sim.dxes: +# for dxa in dxg: +# if not (dxa == sim.dxes[0][0][0]).all(): +# pytest.skip('test_poynting_planes skips nonuniform dxes') points = numpy.where(mask) mask[points[0][0], points[1][0], points[2][0]] = 0 @@ -43,7 +48,6 @@ def test_poynting_planes(sim): assert_close(sum(planes), src_energy.sum()) - ##################################### # Test fixtures ##################################### @@ -102,6 +106,15 @@ def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): # if is3d: # pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)') +# # If no edge currents, add pmls +# src_mask = j_distribution.any(axis=0) +# th = 10 +# #if src_mask.sum() - src_mask[th:-th, th:-th, th:-th].sum() == 0: +# if src_mask.sum() - src_mask[th:-th, :, :].sum() == 0: +# for axis in (0,): +# for polarity in (-1, 1): +# dxes = fdfd.scpml.stretch_with_scpml(dxes, axis=axis, polarity=polarity, + j_vec = vec(j_distribution) eps_vec = vec(epsilon) e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec, diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py index eafa14f..d13d753 100644 --- a/meanas/test/test_fdfd_pml.py +++ b/meanas/test/test_fdfd_pml.py @@ -6,7 +6,8 @@ import pytest import numpy from numpy.testing import assert_allclose, assert_array_equal -from .. import fdfd, vec, unvec +from .. import fdfd +from ..fdmath import vec, unvec from .utils import assert_close, assert_fields_close from .test_fdfd import FDResult From 1a3531946a444945e3fc54e763a4648a6ea0dc41 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 28 Nov 2019 01:27:10 -0800 Subject: [PATCH 202/437] more doc fixes --- meanas/fdfd/farfield.py | 2 +- meanas/fdmath/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index b4b9dbe..d0ec008 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -6,7 +6,7 @@ import numpy from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift from numpy import pi -from .. import fdfield_t +from ..fdmath import fdfield_t def near_to_farfield(E_near: fdfield_t, diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index f70634f..cf5e7d8 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -88,7 +88,7 @@ and where \\( \\hat{g} \\) and \\( \\tilde{g} \\) are located at \\((m,n,p)\\) with components at \\( (m \\pm \\frac{1}{2},n,p) \\) etc., - while \\( \\hat{h} \\) and \\( \\tilde{h} \\) are located at \\((m \pm \\frac{1}{2}, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) + while \\( \\hat{h} \\) and \\( \\tilde{h} \\) are located at \\((m \\pm \\frac{1}{2}, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) with components at \\((m, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) etc. TODO: Explain fdfield_t vs vfdfield_t / operators vs functional From de98d7085052ebacb8b1f2576985811b35e49df8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 28 Nov 2019 01:44:42 -0800 Subject: [PATCH 203/437] specify forward vs reverse --- meanas/fdmath/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index cf5e7d8..5017167 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -25,10 +25,13 @@ The derivatives are shifted by a half-cell relative to the original function: _________________________ | | | | | - | f0 | f1 | f2 | f3 | + | f0 | f1 | f2 | f3 | function |_____|_____|_____|_____| | | | | - | Df0 | Df1 | Df2 | Df3 + | Df0 | Df1 | Df2 | Df3 forward derivative (periodic boundary) + ___|_____|_____|_____|____ + | | | | + | Df1 | Df2 | Df3 | Df0 reverse derivative (periodic boundary) ___|_____|_____|_____|____ Periodic boundaries are used unless otherwise noted. From ec774086808b01c6ac222612e0c421751af7b50c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 28 Nov 2019 01:45:28 -0800 Subject: [PATCH 204/437] add todo --- meanas/fdmath/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 5017167..05a74e1 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -94,6 +94,7 @@ and while \\( \\hat{h} \\) and \\( \\tilde{h} \\) are located at \\((m \\pm \\frac{1}{2}, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) with components at \\((m, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) etc. +TODO: draw diagrams for vector derivatives TODO: Explain fdfield_t vs vfdfield_t / operators vs functional TODO: explain dxes From 2f822ae4a6e315638ce90f52d939e1bf9b5cb1ab Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 30 Nov 2019 01:24:16 -0800 Subject: [PATCH 205/437] add headings and vector diagram --- meanas/fdmath/__init__.py | 42 +++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 05a74e1..3ed02c7 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -1,23 +1,30 @@ """ + Basic discrete calculus for finite difference (fd) simulations. +Discrete calculus +================= + This documentation and approach is roughly based on W.C. Chew's excellent "Electromagnetic Theory on a Lattice" (doi:10.1063/1.355770), which covers a superset of this material with similar notation and more detail. +Derivatives +----------- + Define the discrete forward derivative as Dx_forward(f)[i] = (f[i + 1] - f[i]) / dx[i] -or + or $$ [\\tilde{\\partial}_x f ]_{m + \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m + 1} - f_m) $$ Likewise, discrete reverse derivative is Dx_back(f)[i] = (f[i] - f[i - 1]) / dx[i] -or + or $$ [\\hat{\\partial}_x f ]_{m - \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m} - f_{m - 1}) $$ @@ -37,6 +44,9 @@ The derivatives are shifted by a half-cell relative to the original function: Periodic boundaries are used unless otherwise noted. +Gradients and fore-vectors +-------------------------- + Expanding to three dimensions, we can define two gradients $$ [\\tilde{\\nabla} f]_{n,m,p} = \\vec{x} [\\tilde{\\partial}_x f]_{m + \\frac{1}{2},n,p} + \\vec{y} [\\tilde{\\partial}_y f]_{m,n + \\frac{1}{2},p} + @@ -60,6 +70,24 @@ on the direction of the shift. We write it as \\vec{z} g^z_{m,n,p - \\frac{1}{2}} $$ + (m, n+1, p+1) _____________ (m+1, n+1, p+1) + /: /| + / : / | + / : / | + (m, n, p+1)/____________/ | The derivatives are defined + | : | | at the Dx, Dy, Dz points, + | :........|...| but the gradient fore-vector + Dz / | / is the set of all three + | Dy | / and is said to be "located" at (m,n,p) + | / | / + (m, n, p)|/____Dx_____|/ (m+1, n, p) + + + + +Divergences +----------- + There are also two divergences, $$ d_{n,m,p} = [\\tilde{\\nabla} \\cdot \\hat{g}]_{n,m,p} @@ -77,15 +105,21 @@ is defined at the back-vector's (fore-vectors) location \\( (m,n,p) \\) and not \\( (m \\pm \\frac{1}{2},n,p) \\) etc. +Curls +----- + The two curls are then + $$ \\begin{align} \\hat{h}_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &= \\\\ [\\tilde{\\nabla} \\times \\tilde{g}]_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &= \\vec{x} (\\tilde{\\partial}_y g^z_{m,n,p + \\frac{1}{2}} - \\tilde{\\partial}_z g^y_{m,n + \\frac{1}{2},p}) \\\\ &+ \\vec{y} (\\tilde{\\partial}_z g^x_{m + \\frac{1}{2},n,p} - \\tilde{\\partial}_x g^z_{m,n,p + \\frac{1}{2}}) \\\\ &+ \\vec{z} (\\tilde{\\partial}_x g^y_{m,n + \\frac{1}{2},p} - \\tilde{\\partial}_x g^z_{m + \\frac{1}{2},n,p}) - \\end{align}$$ -and + \\end{align} $$ + + and + $$ \\tilde{h}_{m - \\frac{1}{2}, n - \\frac{1}{2}, p - \\frac{1}{2}} = [\\hat{\\nabla} \\times \\hat{g}]_{m - \\frac{1}{2}, n - \\frac{1}{2}, p - \\frac{1}{2}} $$ From 163aa52420352c1510cccd2c4878b38f9575f4ea Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 1 Dec 2019 02:32:31 -0800 Subject: [PATCH 206/437] lots more docs --- meanas/fdmath/__init__.py | 185 ++++++++++++++++++++++++++++++++------ 1 file changed, 159 insertions(+), 26 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 3ed02c7..10ca329 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -14,22 +14,21 @@ Derivatives ----------- Define the discrete forward derivative as + $$ [\\tilde{\\partial}_x f ]_{m + \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m + 1} - f_m) $$ + or Dx_forward(f)[i] = (f[i + 1] - f[i]) / dx[i] - or - $$ [\\tilde{\\partial}_x f ]_{m + \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m + 1} - f_m) $$ - Likewise, discrete reverse derivative is + $$ [\\hat{\\partial}_x f ]_{m - \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m} - f_{m - 1}) $$ + + or Dx_back(f)[i] = (f[i] - f[i - 1]) / dx[i] - or - $$ [\\hat{\\partial}_x f ]_{m - \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m} - f_{m - 1}) $$ - - -The derivatives are shifted by a half-cell relative to the original function: +The derivatives' arrays are shifted by a half-cell relative to the original function: + [figure: derivatives] _________________________ | | | | | | f0 | f1 | f2 | f3 | function @@ -48,13 +47,30 @@ Gradients and fore-vectors -------------------------- Expanding to three dimensions, we can define two gradients - $$ [\\tilde{\\nabla} f]_{n,m,p} = \\vec{x} [\\tilde{\\partial}_x f]_{m + \\frac{1}{2},n,p} + + $$ [\\tilde{\\nabla} f]_{m,n,p} = \\vec{x} [\\tilde{\\partial}_x f]_{m + \\frac{1}{2},n,p} + \\vec{y} [\\tilde{\\partial}_y f]_{m,n + \\frac{1}{2},p} + \\vec{z} [\\tilde{\\partial}_z f]_{m,n,p + \\frac{1}{2}} $$ $$ [\\hat{\\nabla} f]_{m,n,p} = \\vec{x} [\\hat{\\partial}_x f]_{m + \\frac{1}{2},n,p} + \\vec{y} [\\hat{\\partial}_y f]_{m,n + \\frac{1}{2},p} + \\vec{z} [\\hat{\\partial}_z f]_{m,n,p + \\frac{1}{2}} $$ + or + + [code: gradients] + grad_forward(f)[i,j,k] = [Dx_forward(f)[i, j, k], + Dy_forward(f)[i, j, k], + Dz_forward(f)[i, j, k]] + = [(f[i + 1, j, k] - f[i, j, k]) / dx[i], + (f[i, j + 1, k] - f[i, j, k]) / dy[i], + (f[i, j, k + 1] - f[i, j, k]) / dz[i]] + + grad_back(f)[i,j,k] = [Dx_back(f)[i, j, k], + Dy_back(f)[i, j, k], + Dz_back(f)[i, j, k]] + = [(f[i, j, k] - f[i - 1, j, k]) / dx[i], + (f[i, j, k] - f[i, j - 1, k]) / dy[i], + (f[i, j, k] - f[i, j, k - 1]) / dz[i]] + The three derivatives in the gradient cause shifts in different directions, so the x/y/z components of the resulting "vector" are defined at different points: the x-component is shifted in the x-direction, @@ -70,18 +86,18 @@ on the direction of the shift. We write it as \\vec{z} g^z_{m,n,p - \\frac{1}{2}} $$ - (m, n+1, p+1) _____________ (m+1, n+1, p+1) - /: /| - / : / | - / : / | - (m, n, p+1)/____________/ | The derivatives are defined - | : | | at the Dx, Dy, Dz points, - | :........|...| but the gradient fore-vector - Dz / | / is the set of all three - | Dy | / and is said to be "located" at (m,n,p) - | / | / - (m, n, p)|/____Dx_____|/ (m+1, n, p) - + [figure: gradient / fore-vector] + (m, n+1, p+1) ______________ (m+1, n+1, p+1) + /: /| + / : / | + / : / | + (m, n, p+1)/_____________/ | The forward derivatives are defined + | : | | at the Dx, Dy, Dz points, + | :.........|...| but the forward-gradient fore-vector + Dz / | / is the set of all three + | Dy | / and is said to be "located" at (m,n,p) + | / | / + (m, n, p)|/_____Dx_____|/ (m+1, n, p) @@ -100,23 +116,58 @@ There are also two divergences, [\\hat{\\partial}_y g^y]_{m,n,p} + [\\hat{\\partial}_z g^z]_{m,n,p} $$ + or + + [code: divergences] + div_forward(g)[i,j,k] = Dx_forward(gx)[i, j, k] + + Dy_forward(gy)[i, j, k] + + Dz_forward(gz)[i, j, k] + = (gx[i + 1, j, k] - gx[i, j, k]) / dx[i] + + (gy[i, j + 1, k] - gy[i, j, k]) / dy[i] + + (gz[i, j, k + 1] - gz[i, j, k]) / dz[i] + + div_back(g)[i,j,k] = Dx_back(gx)[i, j, k] + + Dy_back(gy)[i, j, k] + + Dz_back(gz)[i, j, k] + = (gx[i, j, k] - gx[i - 1, j, k]) / dx[i] + + (gy[i, j, k] - gy[i, j - 1, k]) / dy[i] + + (gz[i, j, k] - gz[i, j, k - 1]) / dz[i] + +where `g = [gx, gy, gz]` is a fore- or back-vector field. + Since we applied the forward divergence to the back-vector (and vice-versa), the resulting scalar value is defined at the back-vector's (fore-vectors) location \\( (m,n,p) \\) and not at the locations of its components \\( (m \\pm \\frac{1}{2},n,p) \\) etc. + [figure: divergence] + ^^ + (m-1/2, n+1/2, p+1/2) _____||_______ (m+1/2, n+1/2, p+1/2) + /: || ,, /| + / : || // / | The divergence at (m, n, p) (the center + / : // / | of this cube) of a fore-vector field + (m-1/2, n-1/2, p+1/2)/_____________/ | is the sum of the outward-pointing + | : | | fore-vector components, which are + <==|== :.........|.====> located at the face centers. + | / | / + | / // | / Note that in a nonuniform grid, each + | / // || | / dimension is normalized by the cell width. + (m-1/2, n-1/2, p-1/2)|/___//_______|/ (m+1/2, n-1/2, p-1/2) + '' || + VV + Curls ----- The two curls are then - $$ \\begin{align} + $$ \\begin{align*} \\hat{h}_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &= \\\\ [\\tilde{\\nabla} \\times \\tilde{g}]_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &= \\vec{x} (\\tilde{\\partial}_y g^z_{m,n,p + \\frac{1}{2}} - \\tilde{\\partial}_z g^y_{m,n + \\frac{1}{2},p}) \\\\ &+ \\vec{y} (\\tilde{\\partial}_z g^x_{m + \\frac{1}{2},n,p} - \\tilde{\\partial}_x g^z_{m,n,p + \\frac{1}{2}}) \\\\ - &+ \\vec{z} (\\tilde{\\partial}_x g^y_{m,n + \\frac{1}{2},p} - \\tilde{\\partial}_x g^z_{m + \\frac{1}{2},n,p}) - \\end{align} $$ + &+ \\vec{z} (\\tilde{\\partial}_x g^y_{m,n + \\frac{1}{2},p} - \\tilde{\\partial}_y g^z_{m + \\frac{1}{2},n,p}) + \\end{align*} $$ and @@ -128,8 +179,90 @@ The two curls are then while \\( \\hat{h} \\) and \\( \\tilde{h} \\) are located at \\((m \\pm \\frac{1}{2}, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) with components at \\((m, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) etc. -TODO: draw diagrams for vector derivatives -TODO: Explain fdfield_t vs vfdfield_t / operators vs functional + + [code: curls] + curl_forward(g)[i,j,k] = [Dy_forward(gz)[i, j, k] - Dz_forward(gy)[i, j, k], + Dz_forward(gx)[i, j, k] - Dx_forward(gz)[i, j, k], + Dx_forward(gy)[i, j, k] - Dy_forward(gx)[i, j, k]] + + curl_back(g)[i,j,k] = [Dy_back(gz)[i, j, k] - Dz_back(gy)[i, j, k], + Dz_back(gx)[i, j, k] - Dx_back(gz)[i, j, k], + Dx_back(gy)[i, j, k] - Dy_back(gx)[i, j, k]] + + +For example, consider the forward curl, at (m, n, p), of a back-vector field `g`, defined + on a grid containing (m + 1/2, n + 1/2, p + 1/2). + The curl will be a fore-vector, so its z-component will be defined at (m, n, p + 1/2). + Take the nearest x- and y-components of `g` in the xy plane where the curl's z-component + is located; these are + + [curl components] + (m, n + 1/2, p + 1/2) : x-component of back-vector at (m + 1/2, n + 1/2, p + 1/2) + (m + 1, n + 1/2, p + 1/2) : x-component of back-vector at (m + 3/2, n + 1/2, p + 1/2) + (m + 1/2, n , p + 1/2) : y-component of back-vector at (m + 1/2, n + 1/2, p + 1/2) + (m + 1/2, n + 1 , p + 1/2) : y-component of back-vector at (m + 1/2, n + 3/2, p + 1/2) + + These four xy-components can be used to form a loop around the curl's z-component; its magnitude and sign + is set by their loop-oriented sum (i.e. two have their signs flipped to complete the loop). + + [figure: z-component of curl] + : | + : ^^ | + :....||.<.....| (m, n+1, p+1/2) + / || / + | v || | ^ + | / | / + (m, n, p+1/2) |/_____>______|/ (m+1, n, p+1/2) + + + +Maxwell's Equations +=================== + +If we discretize both space (m,n,p) and time (l), Maxwell's equations become + + $$ \\begin{align*} + \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &=& -&\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} + &+& \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}} \\\\ + \\hat{\\nabla} \\times \\hat{H}_{l,\\vec{r}} &=& &\\hat{\\partial}_t \\tilde{D}_{l, \\vec{r}} + &+& \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\ + \\tilde{\\nabla} \\cdot \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &= 0 \\\\ + \\hat{\\nabla} \\cdot \\tilde{D}_{l,\\vec{r}} &= \\rho_{l,\\vec{r}} + \\end{align*} $$ + + with + + $$ \\begin{align*} + \\hat{B}_\\vec{r} &= \\mu_{\\vec{r} + \\frac{1}{2}} \\cdot \\hat{H}_{\\vec{r} + \\frac{1}{2}} \\\\ + \\tilde{D}_\\vec{r} &= \\epsilon_\\vec{r} \\cdot \\tilde{E}_\\vec{r} + \\end{align*} $$ + +where the spatial subscripts are abbreviated as \\( \\vec{r} = (m, n, p) \\) and +\\( \\vec{r} + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}) \\). +This is Yee's algorithm, written in a form analogous to Maxwell's equations. + +The divergence equations can be derived by taking the divergence of the curl equations +and combining them with charge continuity, + $$ \\hat{\\nabla} \\cdot \\tilde{J} + \\hat{\\partial}_t \\rho = 0 $$ + implying that the discrete Maxwell's equations do not produce spurious charges. + +TODO: Maxwell's equations explanation +TODO: Maxwell's equations plaintext + +Wave equation +------------- + +$$ + \\hat{\\nabla} \\times \\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l, \\vec{r}} + + \\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_\\vec{r} \\cdot \\tilde{E}_{l, \\vec{r}} + = \\tilde{\\partial}_t \\tilde{J}_{l - \\frac{1}{2}, \\vec{r}} $$ + +TODO: wave equation explanation +TODO: wave equation plaintext + + +Grid description +================ TODO: explain dxes """ From b58f8ebb65944e30cb0e06a34448aa2457d26549 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 8 Dec 2019 01:46:47 -0800 Subject: [PATCH 207/437] lots more fdmath documentation --- meanas/fdmath/__init__.py | 175 +++++++++++++++++++++++++++++++------- 1 file changed, 142 insertions(+), 33 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 10ca329..2d66dbf 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -2,6 +2,8 @@ Basic discrete calculus for finite difference (fd) simulations. +TODO: short description of functional vs operator form + Discrete calculus ================= @@ -10,37 +12,69 @@ This documentation and approach is roughly based on W.C. Chew's excellent which covers a superset of this material with similar notation and more detail. -Derivatives ------------ +Derivatives and shifted values +------------------------------ Define the discrete forward derivative as $$ [\\tilde{\\partial}_x f ]_{m + \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m + 1} - f_m) $$ - or + where \\( f \\) is a function defined at discrete locations on the x-axis (labeled using \\( m \\)). + The value at \\( m \\) occupies a length \\( \\Delta_{x, m} \\) along the x-axis. Note that \\( m \\) + is an index along the x-axis, _not_ necessarily an x-coordinate, since each length + \\( \\Delta_{x, m}, \\Delta_{x, m+1}, ...\\) is independently chosen. + +If we treat `f` as a 1D array of values, with the `i`-th value `f[i]` taking up a length `dx[i]` +along the x-axis, the forward derivative is + + deriv_forward(f)[i] = (f[i + 1] - f[i]) / dx[i] - Dx_forward(f)[i] = (f[i + 1] - f[i]) / dx[i] Likewise, discrete reverse derivative is $$ [\\hat{\\partial}_x f ]_{m - \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m} - f_{m - 1}) $$ - or - Dx_back(f)[i] = (f[i] - f[i - 1]) / dx[i] + deriv_back(f)[i] = (f[i] - f[i - 1]) / dx[i] -The derivatives' arrays are shifted by a half-cell relative to the original function: +The derivatives' values are shifted by a half-cell relative to the original function, and +will have different cell widths if all the `dx[i]` ( \\( \\Delta_{x, m} \\) ) are not +identical: - [figure: derivatives] - _________________________ - | | | | | - | f0 | f1 | f2 | f3 | function - |_____|_____|_____|_____| - | | | | - | Df0 | Df1 | Df2 | Df3 forward derivative (periodic boundary) - ___|_____|_____|_____|____ - | | | | - | Df1 | Df2 | Df3 | Df0 reverse derivative (periodic boundary) - ___|_____|_____|_____|____ + [figure: derivatives and cell sizes] + dx0 dx1 dx2 dx3 cell sizes for function + ----- ----- ----------- ----- + ______________________________ + | | | | + f0 | f1 | f2 | f3 | function + _____|_____|___________|_____| + | | | | + | Df0 | Df1 | Df2 | Df3 forward derivative (periodic boundary) + __|_____|________|________|___ -Periodic boundaries are used unless otherwise noted. + dx'3] dx'0 dx'1 dx'2 [dx'3 cell sizes for forward derivative + -- ----- -------- -------- --- + dx'0] dx'1 dx'2 dx'3 [dx'0 cell sizes for reverse derivative + ______________________________ + | | | | + | df1 | df2 | df3 | df0 reverse derivative (periodic boundary) + __|_____|________|________|___ + + Periodic boundaries are used here and elsewhere unless otherwise noted. + +In the above figure, + `f0 =` \\(f_0\\), `f1 =` \\(f_1\\) + `Df0 =` \\([\\tilde{\\partial}f]_{0 + \\frac{1}{2}}\\) + `Df1 =` \\([\\tilde{\\partial}f]_{1 + \\frac{1}{2}}\\) + `df0 =` \\([\\hat{\\partial}f]_{0 - \\frac{1}{2}}\\) + etc. + +The fractional subscript \\( m + \\frac{1}{2} \\) is used to indicate values defined + at shifted locations relative to the original \\( m \\), with corresponding lengths + $$ \\Delta_{x, m + \\frac{1}{2}} = \\frac{1}{2} * (\\Delta_{x, m} + \\Delta_{x, m + 1}) $$ +Just as \\( m \\) is not itself an x-coordinate, neither is \\( m + \\frac{1}{2} \\); +carefully note the positions of the various cells in the above figure vs their labels. + +For the remainder of the `Discrete calculus` section, all figures will show +constant-length cells in order to focus on the vector derivatives themselves. +See the `Grid description` section below for additional information on this topic. Gradients and fore-vectors @@ -222,10 +256,10 @@ Maxwell's Equations If we discretize both space (m,n,p) and time (l), Maxwell's equations become $$ \\begin{align*} - \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &=& -&\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} - &+& \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}} \\\\ - \\hat{\\nabla} \\times \\hat{H}_{l,\\vec{r}} &=& &\\hat{\\partial}_t \\tilde{D}_{l, \\vec{r}} - &+& \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\ + \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &= -\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} + + \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}} \\\\ + \\hat{\\nabla} \\times \\hat{H}_{l,\\vec{r} + \\frac{1}{2}} &= \\hat{\\partial}_t \\tilde{D}_{l, \\vec{r}} + + \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\ \\tilde{\\nabla} \\cdot \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &= 0 \\\\ \\hat{\\nabla} \\cdot \\tilde{D}_{l,\\vec{r}} &= \\rho_{l,\\vec{r}} \\end{align*} $$ @@ -238,31 +272,106 @@ If we discretize both space (m,n,p) and time (l), Maxwell's equations become \\end{align*} $$ where the spatial subscripts are abbreviated as \\( \\vec{r} = (m, n, p) \\) and -\\( \\vec{r} + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}) \\). -This is Yee's algorithm, written in a form analogous to Maxwell's equations. +\\( \\vec{r} + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}) \\), +\\( \\tilde{E} \\) and \\( \\hat{H} \\) are the electric and magnetic fields, +\\( \\tilde{J} \\) and \\( \\hat{M} \\) are the electric and magnetic current distributions, +and \\( \\epsilon \\) and \\( \\mu \\) are the dielectric permittivity and magnetic permeability. + +The above is Yee's algorithm, written in a form analogous to Maxwell's equations. +The time derivatives can be expanded to form the update equations: + + [code: Maxwell's equations] + H[i, j, k] -= (curl_forward(E[t])[i, j, k] - M[t, i, j, k]) / mu[i, j, k] + E[i, j, k] += (curl_back( H[t])[i, j, k] + J[t, i, j, k]) / epsilon[i, j, k] + +Note that the E-field fore-vector and H-field back-vector are offset by a half-cell, resulting +in distinct locations for all six E- and H-field components: + + [figure: Yee cell] + (m, n+1, p+1) _________________________ (m+1, n+1, p+1) + /: /| + / : / | + / : / | Locations of the + / : / | E- and H-field components + / : / | for the E fore-vector at + / : / | r = (m, n, p) and its associated + (m, n, p+1)/________________________/ | H back-vector at r + 1/2 = + | : | | (m + 1/2, n + 1/2, p + 1/2) + | : | | (the large cube's center) + | Hx : | | + | /: :.................|......| (m+1, n+1, p) + |/ : / | / + Ez..........Hy | / + | Ey.......:..Hz | / This is the Yee discretization + | / : / | / scheme ("Yee cell"). + | / : / | / + |/ :/ | / + r=(m, n, p)|___________Ex___________|/ (m+1, n, p) + + +Each component forms its own grid, offset from the others: + + [figure: E-fields for adjacent cells] + ________Ex(p+1, m+1)_____ + /: /| + / : / | + / : / | + Ey(p+1) Ey(m+1, p+1) + / : / | + / Ez(n+1) / Ez(m+1, n+1) + /__________Ex(p+1)_______/ | + | : | | + | : | | This figure shows which fore-vector + | : | | each e-field component belongs to. + | :.........Ex(n+1).|......| Indices are shortened; e.g. Ex(p+1) + | / | / means "Ex for the fore-vector located + Ez / Ez(m+1)/ at (m, n, p+1)". + | Ey | / + | / | Ey(m+1) + | / | / + |/ | / + r=(m, n, p)|___________Ex___________|/ + The divergence equations can be derived by taking the divergence of the curl equations and combining them with charge continuity, $$ \\hat{\\nabla} \\cdot \\tilde{J} + \\hat{\\partial}_t \\rho = 0 $$ implying that the discrete Maxwell's equations do not produce spurious charges. -TODO: Maxwell's equations explanation -TODO: Maxwell's equations plaintext Wave equation ------------- -$$ - \\hat{\\nabla} \\times \\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l, \\vec{r}} - + \\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_\\vec{r} \\cdot \\tilde{E}_{l, \\vec{r}} - = \\tilde{\\partial}_t \\tilde{J}_{l - \\frac{1}{2}, \\vec{r}} $$ +Taking the backward curl of the \\( \\tilde{\\nabla} \\times \\tilde{E} \\) equation and +replacing the resulting \\( \\hat{\\nabla} \\times \\hat{H} \\) term using its respective equation, +and setting \\( \\hat{M} \\) to zero, we can form the discrete wave equation: + +$$ + \\begin{align*} + \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &= + -\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} + + \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}} \\\\ + \\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &= + -\\tilde{\\partial}_t \\hat{H}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} \\\\ + \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &= + \\hat{\\nabla} \\times (-\\tilde{\\partial}_t \\hat{H}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}}) \\\\ + \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &= + -\\tilde{\\partial}_t \\hat{\\nabla} \\times \\hat{H}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} \\\\ + \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &= + -\\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_\\vec{r} \\tilde{E}_{l, \\vec{r}} + \\hat{\\partial}_t \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\ + \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l, \\vec{r}}) + + \\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_\\vec{r} \\cdot \\tilde{E}_{l, \\vec{r}} + &= \\tilde{\\partial}_t \\tilde{J}_{l - \\frac{1}{2}, \\vec{r}} + \\end{align*} +$$ -TODO: wave equation explanation -TODO: wave equation plaintext Grid description ================ + +The + TODO: explain dxes """ From 8bccc69706a5138a435a83804b5910f3d53f73ad Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 9 Dec 2019 21:28:26 -0800 Subject: [PATCH 208/437] more grid work --- meanas/fdmath/__init__.py | 196 ++++++++++++++++++++++++++++++-------- 1 file changed, 158 insertions(+), 38 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 2d66dbf..6eb9401 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -12,8 +12,8 @@ This documentation and approach is roughly based on W.C. Chew's excellent which covers a superset of this material with similar notation and more detail. -Derivatives and shifted values ------------------------------- +Scalar derivatives and cell shifts +---------------------------------- Define the discrete forward derivative as $$ [\\tilde{\\partial}_x f ]_{m + \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m + 1} - f_m) $$ @@ -71,10 +71,14 @@ The fractional subscript \\( m + \\frac{1}{2} \\) is used to indicate values def $$ \\Delta_{x, m + \\frac{1}{2}} = \\frac{1}{2} * (\\Delta_{x, m} + \\Delta_{x, m + 1}) $$ Just as \\( m \\) is not itself an x-coordinate, neither is \\( m + \\frac{1}{2} \\); carefully note the positions of the various cells in the above figure vs their labels. +If the positions labeled with \\( m \\) are considered the "base" or "original" grid, +the positions labeled with \\( m + \\frac{1}{2} \\) are said to lie on a "dual" or +"derived" grid. For the remainder of the `Discrete calculus` section, all figures will show constant-length cells in order to focus on the vector derivatives themselves. -See the `Grid description` section below for additional information on this topic. +See the `Grid description` section below for additional information on this topic +and generalization to three dimensions. Gradients and fore-vectors @@ -128,10 +132,10 @@ on the direction of the shift. We write it as (m, n, p+1)/_____________/ | The forward derivatives are defined | : | | at the Dx, Dy, Dz points, | :.........|...| but the forward-gradient fore-vector - Dz / | / is the set of all three - | Dy | / and is said to be "located" at (m,n,p) - | / | / - (m, n, p)|/_____Dx_____|/ (m+1, n, p) + z y Dz / | / is the set of all three + |/_x | Dy | / and is said to be "located" at (m,n,p) + |/ |/ + (m, n, p)|_____Dx______| (m+1, n, p) @@ -181,11 +185,11 @@ is defined at the back-vector's (fore-vectors) location \\( (m,n,p) \\) and not / : // / | of this cube) of a fore-vector field (m-1/2, n-1/2, p+1/2)/_____________/ | is the sum of the outward-pointing | : | | fore-vector components, which are - <==|== :.........|.====> located at the face centers. - | / | / - | / // | / Note that in a nonuniform grid, each - | / // || | / dimension is normalized by the cell width. - (m-1/2, n-1/2, p-1/2)|/___//_______|/ (m+1/2, n-1/2, p-1/2) + z y <==|== :.........|.====> located at the face centers. + |/_x | / | / + | / // | / Note that in a nonuniform grid, each + |/ // || |/ dimension is normalized by the cell width. + (m-1/2, n-1/2, p-1/2)|____//_______| (m+1/2, n-1/2, p-1/2) '' || VV @@ -240,13 +244,13 @@ For example, consider the forward curl, at (m, n, p), of a back-vector field `g` is set by their loop-oriented sum (i.e. two have their signs flipped to complete the loop). [figure: z-component of curl] - : | - : ^^ | - :....||.<.....| (m, n+1, p+1/2) - / || / - | v || | ^ - | / | / - (m, n, p+1/2) |/_____>______|/ (m+1, n, p+1/2) + : | + z y : ^^ | + |/_x :....||.<.....| (m, n+1, p+1/2) + / || / + | v || | ^ + |/ |/ + (m, n, p+1/2) |_____>______| (m+1, n, p+1/2) @@ -290,8 +294,8 @@ in distinct locations for all six E- and H-field components: [figure: Yee cell] (m, n+1, p+1) _________________________ (m+1, n+1, p+1) /: /| - / : / | - / : / | Locations of the + z y / : / | + |/_x / : / | Locations of the / : / | E- and H-field components / : / | for the E fore-vector at / : / | r = (m, n, p) and its associated @@ -300,21 +304,21 @@ in distinct locations for all six E- and H-field components: | : | | (the large cube's center) | Hx : | | | /: :.................|......| (m+1, n+1, p) - |/ : / | / - Ez..........Hy | / - | Ey.......:..Hz | / This is the Yee discretization - | / : / | / scheme ("Yee cell"). - | / : / | / - |/ :/ | / - r=(m, n, p)|___________Ex___________|/ (m+1, n, p) + |/ : / | / + Ez..........Hy | / + | Ey.......:..Hz | / This is the Yee discretization + | / : / | / scheme ("Yee cell"). + | / : / | / + |/ :/ |/ + r=(m, n, p)|___________Ex___________| (m+1, n, p) Each component forms its own grid, offset from the others: [figure: E-fields for adjacent cells] ________Ex(p+1, m+1)_____ - /: /| - / : / | + z y /: /| + |/_x / : / | / : / | Ey(p+1) Ey(m+1, p+1) / : / | @@ -324,13 +328,13 @@ Each component forms its own grid, offset from the others: | : | | This figure shows which fore-vector | : | | each e-field component belongs to. | :.........Ex(n+1).|......| Indices are shortened; e.g. Ex(p+1) - | / | / means "Ex for the fore-vector located - Ez / Ez(m+1)/ at (m, n, p+1)". - | Ey | / - | / | Ey(m+1) - | / | / - |/ | / - r=(m, n, p)|___________Ex___________|/ + | / | / means "Ex for the fore-vector located + Ez / Ez(m+1) at (m, n, p+1)". + | Ey | / + | / | Ey(m+1) + | / | / + |/ |/ + r=(m, n, p)|___________Ex___________| The divergence equations can be derived by taking the divergence of the curl equations @@ -370,7 +374,123 @@ $$ Grid description ================ -The +As described in the section on scalar discrete derivatives above, cell widths along +each axis can be arbitrary and independently defined. Moreover, all field components +are defined at "derived" or "dual" positions, in between the "base" grid points on +one or more axes. + + [figure: 3D base and derived grids] + _____________________________ _____________________________ + z y /: /: /: /| z y /: /: /: + |/_x / : / : / : / | |/_x / : / : / : + / : / : / : / | / : / : / : + /___________________________/ | dz[1] ________________________/____ + / : / : / : /| | /: : / : /: : dz[1] + /: : / : / : / | | / : : / : / : : + / : :..../......:/......:/..|...| / .:...:../......:/..:...:..... + /___________/_______/_______/ | /| ______/_________/_______/___: : + | : / : | | | | / | | : : | | : : + | : / : | | | |/ | | : : | | : : + | :/ : | | | | | dz[0] | : : | | : : dz[0] + | / : | | | /| | | : : | | : : + | /: :...|.......|.......|./ |...| | ..:...:.|.......|...:...:..... + |/ : / | /| /|/ | / | : / | /| : / + |___________|_______|_______| | / dy[1] ______|_________|_______|___: / dy[1] + | : / | / | / | |/ | :/ | / | :/ + | :/.......|.../...|.../...|...| ..|...:.....|.../...|...:... + | / | / | / | / | / | / | / dy[0] + | / | / | / | / dy[0] | / | / | / + |/ |/ |/ |/ |/ |/ |/ + |___________|_______|_______| ______|_________|_______|___ + dx[0] dx[1] dx[2] dx'[0] dx'[1] dx'[2] + + Base grid Shifted one half-cell right + (e.g. for 1D forward x derivative of all components) + Some lines are omitted for clarity. + + z y : / : / :dz'[1] + |/_x :/ :/ :/ + .......:..........:.......:... + | /: | /: | /: + | / : | / : | / : + |/ : |/ : |/ :dz'[0] + ______________________________ + /| :/ /| :/ /| :/dy'[1] + /.|...:..../.|...:./.|.. :.... + | /: | /: | /: + | / : | / : | /dy'[0] + |/ : |/ : |/ : + _______________________________ + /| /| /| + / | / | / | + | | | + dx'[0] dx'[1] dx'[2] + + All three dimensions shifted by one half- + cell. This is quite hard to visualize + (and probably not entirely to scale). + + +Nevertheless, while the spacing + + + [figure: Component centers] + ___________________________________________ + z y /: /: /| + |/_x / : / : / | + / : / : / | + Ey...........Hz Ey.....Hz / | + / : / / : / / | + / : / / : / / | + / : / / :/ / | + /___________Ex____________/______Ex________/ | + | : | : | | + | : | : | | + | Hx : | Hx : | Hx | + | /: :.................|../:...:........|../:...| + | / : / | / : / | / : / + |/ : / |/ : / |/ : / + Ez...........Hy Ez......Hy Ez :/ + | Ey........:..Hz | Ey...:..Hz | Ey + | / : / | / : / | / + | / : / | / : / | / + |/ :/ |/ :/ |/ + |___________Ex____________|_______Ex_______| + + Part of a nonuniform "base grid", with labels specifying + positions of the various field components. + + z y mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm + |/_x m: m: + m : m : + Ey...........m..:.........Ey......m..:.....Ey + m : m : + m : m : + _____m_____:______________m_____:________ + mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm + | / : | / : + | / : | / : + | / : | / : + wwww|w/wwww:wwwwwwwwwwwww|w/wwww:wwwwwwwwww + |/ w |/ w + ____________|____________________|__________ + Ey.......|...w............Ey..|...w........Ey + | w | w + | w | w + |w |w + wwwwwwwwwwww|wwwwwwwwwwwwwwwwwwww|wwwwwwwwww + + The Ey values are positioned on the y-edges of the base + grid, but they represent the Ey field in a volume that + contains (but isn't necessarily centered on) the points + at which they are defined. + + Here, the 'Ey' labels represent the same points as before; + the grid lines _|:/ are edges of the area represented + by each Ey value, and the lines drawn using m.w represent + areas where a cell's faces extend beyond the drawn area + (i.e. where the drawing is truncated in the z-direction). + TODO: explain dxes From 808a8879ae65b0c7fc5a00eb6ab2aaf60cd4c3f1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 10 Dec 2019 01:14:21 -0800 Subject: [PATCH 209/437] Put H at vertices and label them +1/2 --- meanas/fdmath/__init__.py | 243 +++++++++++++++++++++++--------------- 1 file changed, 146 insertions(+), 97 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 6eb9401..742081e 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -291,50 +291,52 @@ The time derivatives can be expanded to form the update equations: Note that the E-field fore-vector and H-field back-vector are offset by a half-cell, resulting in distinct locations for all six E- and H-field components: - [figure: Yee cell] - (m, n+1, p+1) _________________________ (m+1, n+1, p+1) - /: /| - z y / : / | - |/_x / : / | Locations of the - / : / | E- and H-field components - / : / | for the E fore-vector at - / : / | r = (m, n, p) and its associated - (m, n, p+1)/________________________/ | H back-vector at r + 1/2 = - | : | | (m + 1/2, n + 1/2, p + 1/2) - | : | | (the large cube's center) - | Hx : | | - | /: :.................|......| (m+1, n+1, p) - |/ : / | / - Ez..........Hy | / - | Ey.......:..Hz | / This is the Yee discretization - | / : / | / scheme ("Yee cell"). - | / : / | / - |/ :/ |/ - r=(m, n, p)|___________Ex___________| (m+1, n, p) + [figure: Field components] + (m - 1/2,=> ____________Hx__________[H] <= r + 1/2 = (m + 1/2, + n + 1/2, /: /: /| n + 1/2, + z y p + 1/2) / : / : / | p + 1/2) + |/_x / : / : / | + / : Ez__________Hy | Locations of the E- and + / : : : /| | H-field components for the + (m - 1/2, / : : Ey...../.|..Hz [E] fore-vector at r = (m,n,p) + n - 1/2, =>/________________________/ | /| (the large cube's center) + p + 1/2) | : : / | | / | and [H] back-vector at r + 1/2 + | : :/ | |/ | (the top right corner) + | : [E].......|.Ex | + | :.................|......| <= (m + 1/2, n + 1/2, p + 1/2) + | / | / + | / | / + | / | / This is the Yee discretization + | / | / scheme ("Yee cell"). + r - 1/2 = | / | / + (m - 1/2, |/ |/ + n - 1/2,=> |________________________| <= (m + 1/2, n - 1/2, p - 1/2) + p - 1/2) Each component forms its own grid, offset from the others: [figure: E-fields for adjacent cells] - ________Ex(p+1, m+1)_____ - z y /: /| - |/_x / : / | - / : / | - Ey(p+1) Ey(m+1, p+1) - / : / | - / Ez(n+1) / Ez(m+1, n+1) - /__________Ex(p+1)_______/ | - | : | | - | : | | This figure shows which fore-vector - | : | | each e-field component belongs to. - | :.........Ex(n+1).|......| Indices are shortened; e.g. Ex(p+1) - | / | / means "Ex for the fore-vector located - Ez / Ez(m+1) at (m, n, p+1)". - | Ey | / - | / | Ey(m+1) - | / | / - |/ |/ - r=(m, n, p)|___________Ex___________| + + H1__________Hx0_________H0 + z y /: /| + |/_x / : / | This figure shows H back-vector locations + / : / | H0, H1, etc. and their associated components + Hy1 : Hy0 | H0 = (Hx0, Hy0, Hz0) etc. + / : / | + / Hz1 / Hz0 + H2___________Hx3_________H3 | The equivalent drawing for E would have + | : | | fore-vectors located at the cube's + | : | | center (and the centers of adjacent cubes), + | : | | with components on the cube's faces. + | H5..........Hx4...|......H4 + | / | / + Hz2 / Hz2 / + | / | / + | Hy6 | Hy4 + | / | / + |/ |/ + H6__________Hx7__________H7 The divergence equations can be derived by taking the divergence of the curl equations @@ -376,7 +378,7 @@ Grid description As described in the section on scalar discrete derivatives above, cell widths along each axis can be arbitrary and independently defined. Moreover, all field components -are defined at "derived" or "dual" positions, in between the "base" grid points on +are defined at "derived" or "dual" positions, in-between the "base" grid points on one or more axes. [figure: 3D base and derived grids] @@ -404,11 +406,9 @@ one or more axes. |___________|_______|_______| ______|_________|_______|___ dx[0] dx[1] dx[2] dx'[0] dx'[1] dx'[2] - Base grid Shifted one half-cell right - (e.g. for 1D forward x derivative of all components) - Some lines are omitted for clarity. - - z y : / : / :dz'[1] + Base grid Shifted one half-cell right (e.g. for 1D + forward x derivative of all components). + z y : / : / :dz'[1] Some lines are omitted for clarity. |/_x :/ :/ :/ .......:..........:.......:... | /: | /: | /: @@ -416,7 +416,7 @@ one or more axes. |/ : |/ : |/ :dz'[0] ______________________________ /| :/ /| :/ /| :/dy'[1] - /.|...:..../.|...:./.|.. :.... + /.|...:..../.|...:./.|...:.... | /: | /: | /: | / : | / : | /dy'[0] |/ : |/ : |/ : @@ -428,68 +428,117 @@ one or more axes. All three dimensions shifted by one half- cell. This is quite hard to visualize - (and probably not entirely to scale). + (and probably not entirely to scale); see + later figures for a better representation. Nevertheless, while the spacing [figure: Component centers] - ___________________________________________ - z y /: /: /| - |/_x / : / : / | - / : / : / | - Ey...........Hz Ey.....Hz / | - / : / / : / / | - / : / / : / / | - / : / / :/ / | - /___________Ex____________/______Ex________/ | - | : | : | | - | : | : | | - | Hx : | Hx : | Hx | - | /: :.................|../:...:........|../:...| - | / : / | / : / | / : / - |/ : / |/ : / |/ : / - Ez...........Hy Ez......Hy Ez :/ - | Ey........:..Hz | Ey...:..Hz | Ey - | / : / | / : / | / - | / : / | / : / | / - |/ :/ |/ :/ |/ - |___________Ex____________|_______Ex_______| + [H]__________Hx___________[H]______Hx______[H] + z y /: /: /: /: /| | + |/_x / : / : / : / : / | | + / : / : / : / : / | | + Hy : Ez...........Hy : Ez......Hy | | + /: : : : /: : : : /| | | + / : Hz : Ey....../.:..Hz : Ey./.|..Hz | dz[0] + / : /: : / / : /: : / / | /| | + /_________________________/________________/ | / | | + | :/ : :/ | :/ : :/ | |/ | | + | Ex : [E].......|..Ex : [E]..|..Ex | | + | : | : | | | + | [H]..........Hx....|......[H].....Hx|......[H] + | / | / | / / + | / | / | / / + Hz / Hz / Hz / / + | Hy | Hy | Hy / + | / | / | / / dy[0] + | / | / | / / + |/ |/ |/ / + [H]__________Hx___________[H]______Hx______[H] / + + ------------------------- ---------------- + dx[0] dx[1] Part of a nonuniform "base grid", with labels specifying - positions of the various field components. + positions of the various field components. [E] fore-vectors + are at the cell centers, and [H] back-vectors are at the + vertices. H components along the near (-y) top (+z) edge + have been omitted to make the insides of the cubes easier + to visualize. - z y mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm - |/_x m: m: - m : m : - Ey...........m..:.........Ey......m..:.....Ey - m : m : - m : m : - _____m_____:______________m_____:________ - mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm - | / : | / : - | / : | / : - | / : | / : - wwww|w/wwww:wwwwwwwwwwwww|w/wwww:wwwwwwwwww - |/ w |/ w - ____________|____________________|__________ - Ey.......|...w............Ey..|...w........Ey - | w | w - | w | w - |w |w - wwwwwwwwwwww|wwwwwwwwwwwwwwwwwwww|wwwwwwwwww + <__________________________________________> + z y << /: / /: >> | + |/_x < < / : / / : > > | + < < / : / / : > > | + < < / : / / : > > | + <: < / : : / : >: > | + < : < / : : / : > : > | dz[0] + < : < / : : / : > : > | + <____________/_____________________________> : > | + < : < | : :| : > : > | + < Ex < | : Ex| : > Ex > | + < : < | : :| : > : > | + < : <....|.......:........:|.......:...>...:...> + < : < | / :| / / > : > / + < : < | / :| / / > : > / + < :< | / :|/ / > :> / + < < | / :| / > > / + < < | / | / > > / dy[0] + < < | / | / > > / + << |/ |/ >> / + <____________|_________________|___________> / - The Ey values are positioned on the y-edges of the base - grid, but they represent the Ey field in a volume that - contains (but isn't necessarily centered on) the points - at which they are defined. + ~------------ ----------------- -----------~ + dx'[-1] dx'[0] dx'[1] - Here, the 'Ey' labels represent the same points as before; - the grid lines _|:/ are edges of the area represented - by each Ey value, and the lines drawn using m.w represent - areas where a cell's faces extend beyond the drawn area - (i.e. where the drawing is truncated in the z-direction). + The Ex values are positioned on the x-faces of the base + grid. They represent the Ex field in volumes shifted by + a half-cell in the x-dimension, as shown here. Only the + center cell is fully shown; the other two are truncated + (shown using >< markers). + + Note that the Ex positions are the in the same positions + as the previous figure; only the cell boundaries have moved. + Also note that the points at which Ex is defined are not + necessarily centered in the volumes they represent; non- + uniform cell sizes result in off-center volumes like the + center cell here. + + z y mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm s + |/_x << m: m: >> | + < < m : m : > > | dz'[1] + Hy............m...........Hy........m......Hy > | + < < m : m : > > | + < < m : m : > > | + < _______m_____:_______________m_____:_>______ + mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm > | + < < | / : | / :> > | + < < | / : | / :> > | dz'[0] + < < | / : | / :> > | + < wwwwww|w/wwwwwwwwwwwwwwwwwww|w/wwwww>wwwwwww s + < < |/ w |/ w > > / + _____________|_____________________|________ > / + < Hy........|...w...........Hy....|...w...>..Hy / + < < | w | w > > / dy[0] + < < | w | w > > / + << |w |w >> / + wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww + + ~------------ --------------------- -------~ + dx'[-1] dx'[0] dx'[1] + + The Hy values are positioned on the y-edges of the base + grid. Again here, the 'Hy' labels represent the same points + as in the basic grid figure above; the edges have shifted + by a half-cell along the x- and z-axes. + + The grid lines _|:/ are edges of the area represented by + each Hy value, and the lines drawn using .w represent + edges where a cell's faces extend beyond the drawn area + (i.e. where the drawing is truncated in the x- or z- + directions). TODO: explain dxes From e69201ce24e4f366726b9324586f4a49adf37ce5 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 10 Dec 2019 01:52:07 -0800 Subject: [PATCH 210/437] work on grid text --- meanas/fdmath/__init__.py | 252 +++++++++++++++++++++----------------- 1 file changed, 137 insertions(+), 115 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 742081e..71a1e6e 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -376,10 +376,143 @@ $$ Grid description ================ -As described in the section on scalar discrete derivatives above, cell widths along -each axis can be arbitrary and independently defined. Moreover, all field components -are defined at "derived" or "dual" positions, in-between the "base" grid points on -one or more axes. +As described in the section on scalar discrete derivatives above, cell widths +(`dx[i]`, `dy[j]`, `dz[k]`) along each axis can be arbitrary and independently +defined. Moreover, all field components are actually defined at "derived" or "dual" +positions, in-between the "base" grid points on one or more axes. + +To get a better sense of how this works, let's start by drawing a grid with uniform +`dy` and `dz` and nonuniform `dx`. We will only draw one cell in the y and z dimensions +to make the illustration simpler; we need at least two cells in the x dimension to +demonstrate how nonuniform `dx` affects the various components. + +Place the E fore-vectors at integer indices \\( r = (m, n, p) \\) and the H back-vectors +at fractional indices \\( r + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}. +Remember that these are indices and not coordinates; they can coorespond to arbitrary +(monotonically increasing) coordinates depending on the cell widths. + +Draw lines to denote the planes on which the H components and back-vectors are defined. +For simplicity, don't draw the equivalent planes for the E components and fore-vectors, +except as necessary to show their locations -- it's easiest to just connect them to their +associated H-equivalents. The result looks something like this: + + [figure: Component centers] + p= + [H]__________Hx___________[H]______Hx______[H] __ +1/2 + z y /: /: /: /: /| | | + |/_x / : / : / : / : / | | | + / : / : / : / : / | | | + Hy : Ez...........Hy : Ez......Hy | | | + /: : : : /: : : : /| | | | + / : Hz : Ey....../.:..Hz : Ey./.|..Hz __ 0 | dz[0] + / : /: : / / : /: : / / | /| | | + /_________________________/________________/ | / | | | + | :/ : :/ | :/ : :/ | |/ | | | + | Ex : [E].......|..Ex : [E]..|..Ex | | | + | : | : | | | | + | [H]..........Hx....|......[H].....Hx|......[H] __ --------- (m=+1/2, p=-1/2) + | / | / | / / / + | / | / | / / / + Hz / Hz / Hz / / / + | Hy | Hy | Hy __ 0 / dy[0] + | / | / | / / / + | / | / | / / / + |/ |/ |/ / / + [H]__________Hx___________[H]______Hx______[H] __ -1/2 / + =n + |------------|------------|--------|------| + -1/2 0 +1/2 +1 +3/2 = m + + ------------------------- ---------------- + dx[0] dx[1] + + Part of a nonuniform "base grid", with labels specifying + positions of the various field components. [E] fore-vectors + are at the cell centers, and [H] back-vectors are at the + vertices. H components along the near (-y) top (+z) edge + have been omitted to make the insides of the cubes easier + to visualize. + +This figure shows where all the components are located; however, it is also useful to show +what volumes those components are responsible for representing. Consider the Ex component: +two of its nearest neighbors are E fore-vectors, labeled `[E]` in the figure. + + [figure: Ex volumes] + <__________________________________________> + z y << /: / /: >> | + |/_x < < / : / / : > > | + < < / : / / : > > | + < < / : / / : > > | + <: < / : : / : >: > | + < : < / : : / : > : > | dz[0] + < : < / : : / : > : > | + <____________/_____________________________> : > | + < : < | : :| : > : > | + < Ex < | : Ex| : > Ex > | + < : < | : :| : > : > | + < : <....|.......:........:|.......:...>...:...> + < : < | / :| / / > : > / + < : < | / :| / / > : > / + < :< | / :|/ / > :> / + < < | / :| / > > / + < < | / | / > > / dy[0] + < < | / | / > > / + << |/ |/ >> / + <____________|_________________|___________> / + + ~------------ ----------------- -----------~ + dx'[-1] dx'[0] dx'[1] + + The Ex values are positioned on the x-faces of the base + grid. They represent the Ex field in volumes shifted by + a half-cell in the x-dimension, as shown here. Only the + center cell is fully shown; the other two are truncated + (shown using >< markers). + + Note that the Ex positions are the in the same positions + as the previous figure; only the cell boundaries have moved. + Also note that the points at which Ex is defined are not + necessarily centered in the volumes they represent; non- + uniform cell sizes result in off-center volumes like the + center cell here. + + [figure: Hy volumes] + z y mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm s + |/_x << m: m: >> | + < < m : m : > > | dz'[1] + Hy............m...........Hy........m......Hy > | + < < m : m : > > | + < < m : m : > > | + < _______m_____:_______________m_____:_>______ + mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm > | + < < | / : | / :> > | + < < | / : | / :> > | dz'[0] + < < | / : | / :> > | + < wwwwww|w/wwwwwwwwwwwwwwwwwww|w/wwwww>wwwwwww s + < < |/ w |/ w > > / + _____________|_____________________|________ > / + < Hy........|...w...........Hy....|...w...>..Hy / + < < | w | w > > / dy[0] + < < | w | w > > / + << |w |w >> / + wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww + + ~------------ --------------------- -------~ + dx'[-1] dx'[0] dx'[1] + + The Hy values are positioned on the y-edges of the base + grid. Again here, the 'Hy' labels represent the same points + as in the basic grid figure above; the edges have shifted + by a half-cell along the x- and z-axes. + + The grid lines _|:/ are edges of the area represented by + each Hy value, and the lines drawn using .w represent + edges where a cell's faces extend beyond the drawn area + (i.e. where the drawing is truncated in the x- or z- + directions). + + +TODO: explain dxes [figure: 3D base and derived grids] _____________________________ _____________________________ @@ -432,117 +565,6 @@ one or more axes. later figures for a better representation. -Nevertheless, while the spacing - - - [figure: Component centers] - [H]__________Hx___________[H]______Hx______[H] - z y /: /: /: /: /| | - |/_x / : / : / : / : / | | - / : / : / : / : / | | - Hy : Ez...........Hy : Ez......Hy | | - /: : : : /: : : : /| | | - / : Hz : Ey....../.:..Hz : Ey./.|..Hz | dz[0] - / : /: : / / : /: : / / | /| | - /_________________________/________________/ | / | | - | :/ : :/ | :/ : :/ | |/ | | - | Ex : [E].......|..Ex : [E]..|..Ex | | - | : | : | | | - | [H]..........Hx....|......[H].....Hx|......[H] - | / | / | / / - | / | / | / / - Hz / Hz / Hz / / - | Hy | Hy | Hy / - | / | / | / / dy[0] - | / | / | / / - |/ |/ |/ / - [H]__________Hx___________[H]______Hx______[H] / - - ------------------------- ---------------- - dx[0] dx[1] - - Part of a nonuniform "base grid", with labels specifying - positions of the various field components. [E] fore-vectors - are at the cell centers, and [H] back-vectors are at the - vertices. H components along the near (-y) top (+z) edge - have been omitted to make the insides of the cubes easier - to visualize. - - <__________________________________________> - z y << /: / /: >> | - |/_x < < / : / / : > > | - < < / : / / : > > | - < < / : / / : > > | - <: < / : : / : >: > | - < : < / : : / : > : > | dz[0] - < : < / : : / : > : > | - <____________/_____________________________> : > | - < : < | : :| : > : > | - < Ex < | : Ex| : > Ex > | - < : < | : :| : > : > | - < : <....|.......:........:|.......:...>...:...> - < : < | / :| / / > : > / - < : < | / :| / / > : > / - < :< | / :|/ / > :> / - < < | / :| / > > / - < < | / | / > > / dy[0] - < < | / | / > > / - << |/ |/ >> / - <____________|_________________|___________> / - - ~------------ ----------------- -----------~ - dx'[-1] dx'[0] dx'[1] - - The Ex values are positioned on the x-faces of the base - grid. They represent the Ex field in volumes shifted by - a half-cell in the x-dimension, as shown here. Only the - center cell is fully shown; the other two are truncated - (shown using >< markers). - - Note that the Ex positions are the in the same positions - as the previous figure; only the cell boundaries have moved. - Also note that the points at which Ex is defined are not - necessarily centered in the volumes they represent; non- - uniform cell sizes result in off-center volumes like the - center cell here. - - z y mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm s - |/_x << m: m: >> | - < < m : m : > > | dz'[1] - Hy............m...........Hy........m......Hy > | - < < m : m : > > | - < < m : m : > > | - < _______m_____:_______________m_____:_>______ - mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm > | - < < | / : | / :> > | - < < | / : | / :> > | dz'[0] - < < | / : | / :> > | - < wwwwww|w/wwwwwwwwwwwwwwwwwww|w/wwwww>wwwwwww s - < < |/ w |/ w > > / - _____________|_____________________|________ > / - < Hy........|...w...........Hy....|...w...>..Hy / - < < | w | w > > / dy[0] - < < | w | w > > / - << |w |w >> / - wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww - - ~------------ --------------------- -------~ - dx'[-1] dx'[0] dx'[1] - - The Hy values are positioned on the y-edges of the base - grid. Again here, the 'Hy' labels represent the same points - as in the basic grid figure above; the edges have shifted - by a half-cell along the x- and z-axes. - - The grid lines _|:/ are edges of the area represented by - each Hy value, and the lines drawn using .w represent - edges where a cell's faces extend beyond the drawn area - (i.e. where the drawing is truncated in the x- or z- - directions). - - -TODO: explain dxes - """ from .types import fdfield_t, vfdfield_t, dx_lists_t, fdfield_updater_t From a8364a1e32484d310725c3a99d7674c3670b5682 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 4 Jan 2020 18:19:04 -0800 Subject: [PATCH 211/437] grid figure fixup --- meanas/fdmath/__init__.py | 158 ++++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 73 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 71a1e6e..189b3ce 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -387,41 +387,43 @@ to make the illustration simpler; we need at least two cells in the x dimension demonstrate how nonuniform `dx` affects the various components. Place the E fore-vectors at integer indices \\( r = (m, n, p) \\) and the H back-vectors -at fractional indices \\( r + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}. -Remember that these are indices and not coordinates; they can coorespond to arbitrary -(monotonically increasing) coordinates depending on the cell widths. +at fractional indices \\( r + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, +p + \\frac{1}{2}) \\). Remember that these are indices and not coordinates; they can +correspond to arbitrary (monotonically increasing) coordinates depending on the cell widths. Draw lines to denote the planes on which the H components and back-vectors are defined. For simplicity, don't draw the equivalent planes for the E components and fore-vectors, except as necessary to show their locations -- it's easiest to just connect them to their -associated H-equivalents. The result looks something like this: +associated H-equivalents. + +The result looks something like this: [figure: Component centers] - p= - [H]__________Hx___________[H]______Hx______[H] __ +1/2 - z y /: /: /: /: /| | | - |/_x / : / : / : / : / | | | - / : / : / : / : / | | | - Hy : Ez...........Hy : Ez......Hy | | | - /: : : : /: : : : /| | | | - / : Hz : Ey....../.:..Hz : Ey./.|..Hz __ 0 | dz[0] - / : /: : / / : /: : / / | /| | | - /_________________________/________________/ | / | | | - | :/ : :/ | :/ : :/ | |/ | | | - | Ex : [E].......|..Ex : [E]..|..Ex | | | - | : | : | | | | - | [H]..........Hx....|......[H].....Hx|......[H] __ --------- (m=+1/2, p=-1/2) - | / | / | / / / - | / | / | / / / - Hz / Hz / Hz / / / - | Hy | Hy | Hy __ 0 / dy[0] - | / | / | / / / - | / | / | / / / - |/ |/ |/ / / - [H]__________Hx___________[H]______Hx______[H] __ -1/2 / + p= + [H]__________Hx___________[H]_____Hx______[H] __ +1/2 + z y /: /: /: /: /| | | + |/_x / : / : / : / : / | | | + / : / : / : / : / | | | + Hy : Ez...........Hy : Ez......Hy | | | + /: : : : /: : : : /| | | | + / : Hz : Ey....../.:..Hz : Ey./.|..Hz __ 0 | dz[0] + / : /: : / / : /: : / / | /| | | + /_________________________/_______________/ | / | | | + | :/ : :/ | :/ : :/ | |/ | | | + | Ex : [E].......|..Ex : [E]..|..Ex | | | + | : | : | | | | + | [H]..........Hx....|......[H].....H|x.....[H] __ --------- (n=+1/2, p=-1/2) + | / | / | / / / + Hz / Hz / Hz / / / + | / | / | / / / + | Hy | Hy | Hy __ 0 / dy[0] + | / | / | / / / + | / | / | / / / + |/ |/ |/ / / + [H]__________Hx___________[H]_____Hx______[H] __ -1/2 / =n - |------------|------------|--------|------| - -1/2 0 +1/2 +1 +3/2 = m + |------------|------------|-------|-------| + -1/2 0 +1/2 +1 +3/2 = m ------------------------- ---------------- dx[0] dx[1] @@ -438,36 +440,40 @@ what volumes those components are responsible for representing. Consider the Ex two of its nearest neighbors are E fore-vectors, labeled `[E]` in the figure. [figure: Ex volumes] - <__________________________________________> - z y << /: / /: >> | - |/_x < < / : / / : > > | - < < / : / / : > > | - < < / : / / : > > | - <: < / : : / : >: > | - < : < / : : / : > : > | dz[0] - < : < / : : / : > : > | - <____________/_____________________________> : > | - < : < | : :| : > : > | - < Ex < | : Ex| : > Ex > | - < : < | : :| : > : > | - < : <....|.......:........:|.......:...>...:...> - < : < | / :| / / > : > / - < : < | / :| / / > : > / - < :< | / :|/ / > :> / - < < | / :| / > > / - < < | / | / > > / dy[0] - < < | / | / > > / - << |/ |/ >> / - <____________|_________________|___________> / + p= + <_________________________________________> __ +1/2 + z y << /: / /: >> | | + |/_x < < / : / / : > > | | + < < / : / / : > > | | + < < / : / / : > > | | + <: < / : : / : >: > | | + < : < / : : / : > : > __ 0 | dz[0] + < : < / : : / :> : > | | + <____________/____________________/_______> : > | | + < : < | : : | > : > | | + < Ex < | : Ex | > Ex > | | + < : < | : : | > : > | | + < : <....|.......:........:...|.......>...:...> __ --------- (n=+1/2, p=-1/2) + < : < | / : /| /> : > / / + < : < | / : / | / > : > / / + < :< | / :/ | / > :> / / + < < | / : | / > > _ 0 / dy[0] + < < | / | / > > / / + < < | / | / > > / / + << |/ |/ >> / / + <____________|____________________|_______> __ -1/2 / + =n + |------------|------------|-------|-------| + -1/2 0 +1/2 +1 +3/2 = m - ~------------ ----------------- -----------~ - dx'[-1] dx'[0] dx'[1] + ~------------ -------------------- -------~ + dx'[-1] dx'[0] dx'[1] The Ex values are positioned on the x-faces of the base grid. They represent the Ex field in volumes shifted by a half-cell in the x-dimension, as shown here. Only the - center cell is fully shown; the other two are truncated - (shown using >< markers). + center cell (with width dx'[0]) is fully shown; the + other two are truncated (shown using >< markers). Note that the Ex positions are the in the same positions as the previous figure; only the cell boundaries have moved. @@ -477,28 +483,34 @@ two of its nearest neighbors are E fore-vectors, labeled `[E]` in the figure. center cell here. [figure: Hy volumes] - z y mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm s - |/_x << m: m: >> | - < < m : m : > > | dz'[1] - Hy............m...........Hy........m......Hy > | - < < m : m : > > | - < < m : m : > > | - < _______m_____:_______________m_____:_>______ - mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm > | - < < | / : | / :> > | - < < | / : | / :> > | dz'[0] - < < | / : | / :> > | - < wwwwww|w/wwwwwwwwwwwwwwwwwww|w/wwwww>wwwwwww s - < < |/ w |/ w > > / - _____________|_____________________|________ > / - < Hy........|...w...........Hy....|...w...>..Hy / - < < | w | w > > / dy[0] - < < | w | w > > / - << |w |w >> / - wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww + p= + z y mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm __ +1/2 s + |/_x << m: m: >> | | + < < m : m : > > | | dz'[1] + < < m : m : > > | | + Hy........... m........Hy...........m......Hy > | | + < < m : m : > > | | + < ______ m_____:_______________m_____:_>______ __ 0 + < < m /: m / > > | | + mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm > | | + < < | / : | / > > | | dz'[0] + < < | / : | / > > | | + < < | / : | / > > | | + < wwwww|w/wwwwwwwwwwwwwwwwwww|w/wwwww>wwwwwwww __ s + < < |/ w |/ w> > / / + _____________|_____________________|________ > / / + < < | w | w > > / / + < Hy........|...w........Hy.......|...w...>..Hy _ 0 / dy[0] + < < | w | w > > / / + << | w | w > > / / + < |w |w >> / / + wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww __ -1/2 / + + |------------|------------|--------|-------| + -1/2 0 +1/2 +1 +3/2 = m ~------------ --------------------- -------~ - dx'[-1] dx'[0] dx'[1] + dx'[-1] dx'[0] dx'[1] The Hy values are positioned on the y-edges of the base grid. Again here, the 'Hy' labels represent the same points From 3a29c62de7a47cf46eb4353691e52459d4f0170a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 4 Jan 2020 18:19:26 -0800 Subject: [PATCH 212/437] remove old dxes figures --- meanas/fdmath/__init__.py | 49 --------------------------------------- 1 file changed, 49 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 189b3ce..2c29c6b 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -526,55 +526,6 @@ two of its nearest neighbors are E fore-vectors, labeled `[E]` in the figure. TODO: explain dxes - [figure: 3D base and derived grids] - _____________________________ _____________________________ - z y /: /: /: /| z y /: /: /: - |/_x / : / : / : / | |/_x / : / : / : - / : / : / : / | / : / : / : - /___________________________/ | dz[1] ________________________/____ - / : / : / : /| | /: : / : /: : dz[1] - /: : / : / : / | | / : : / : / : : - / : :..../......:/......:/..|...| / .:...:../......:/..:...:..... - /___________/_______/_______/ | /| ______/_________/_______/___: : - | : / : | | | | / | | : : | | : : - | : / : | | | |/ | | : : | | : : - | :/ : | | | | | dz[0] | : : | | : : dz[0] - | / : | | | /| | | : : | | : : - | /: :...|.......|.......|./ |...| | ..:...:.|.......|...:...:..... - |/ : / | /| /|/ | / | : / | /| : / - |___________|_______|_______| | / dy[1] ______|_________|_______|___: / dy[1] - | : / | / | / | |/ | :/ | / | :/ - | :/.......|.../...|.../...|...| ..|...:.....|.../...|...:... - | / | / | / | / | / | / | / dy[0] - | / | / | / | / dy[0] | / | / | / - |/ |/ |/ |/ |/ |/ |/ - |___________|_______|_______| ______|_________|_______|___ - dx[0] dx[1] dx[2] dx'[0] dx'[1] dx'[2] - - Base grid Shifted one half-cell right (e.g. for 1D - forward x derivative of all components). - z y : / : / :dz'[1] Some lines are omitted for clarity. - |/_x :/ :/ :/ - .......:..........:.......:... - | /: | /: | /: - | / : | / : | / : - |/ : |/ : |/ :dz'[0] - ______________________________ - /| :/ /| :/ /| :/dy'[1] - /.|...:..../.|...:./.|...:.... - | /: | /: | /: - | / : | / : | /dy'[0] - |/ : |/ : |/ : - _______________________________ - /| /| /| - / | / | / | - | | | - dx'[0] dx'[1] dx'[2] - - All three dimensions shifted by one half- - cell. This is quite hard to visualize - (and probably not entirely to scale); see - later figures for a better representation. """ From bb309331092d96590dbde2e3b3df7a549f3f9b39 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 4 Jan 2020 18:19:49 -0800 Subject: [PATCH 213/437] more explanation about grid figures --- meanas/fdmath/__init__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 2c29c6b..16030e4 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -435,9 +435,15 @@ The result looks something like this: have been omitted to make the insides of the cubes easier to visualize. -This figure shows where all the components are located; however, it is also useful to show -what volumes those components are responsible for representing. Consider the Ex component: -two of its nearest neighbors are E fore-vectors, labeled `[E]` in the figure. +The above figure shows where all the components are located; however, it is also useful to show +what volumes those components correspond to. Consider the Ex component at `m = +1/2`: it is +shifted in the x-direction by a half-cell from the E fore-vector at `m = 0` (labeled `[E]` +in the figure). It corresponds to a volume between `m = 0` and `m = +1` (the other +dimensions are not shifted, i.e. they are still bounded by `n, p = +-1/2`). (See figure +below). Since `m` is an index and not an x-coordinate, the Ex component is not necessarily +at the center of the volume it represents, and the x-length of its volume is the derived +quantity `dx'[0] = (dx[0] + dx[1]) / 2` rather than the base `dx`. +(See also `Scalar derivatives and cell shifts`). [figure: Ex volumes] p= @@ -482,6 +488,9 @@ two of its nearest neighbors are E fore-vectors, labeled `[E]` in the figure. uniform cell sizes result in off-center volumes like the center cell here. +The next figure shows the volumes corresponding to the Hy components, which +are shifted in two dimensions (x and z) compared to the base grid. + [figure: Hy volumes] p= z y mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm __ +1/2 s From 2dbd17f332ae10085a0f34b2d38238d6a34ac83e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 4 Jan 2020 18:19:57 -0800 Subject: [PATCH 214/437] explain dx_lists_t --- meanas/fdmath/__init__.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 16030e4..97491d9 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -533,9 +533,43 @@ are shifted in two dimensions (x and z) compared to the base grid. directions). -TODO: explain dxes +Datastructure: dx_lists_t +------------------- +In this documentation, the E fore-vectors are placed on the base grid. An +equivalent formulation could place the H back-vectors on the base grid instead. +However, in the case of a non-uniform grid, the operation to get from the "base" +cell widths to "derived" ones is not its own inverse. +The base grid's cell sizes could be fully described by a list of three 1D arrays, +specifying the cell widths along all three axes: + + [dx, dy, dz] = [[dx[0], dx[1], ...], [dy[0], ...], [dz[0], ...]] + +Note that this is a list-of-arrays rather than a 2D array, as the simulation domain +may have a different number of cells along each axis. + +Knowing the base grid's cell widths and the boundary conditions (periodic unless +otherwise noted) is enough information to calculate the cell widths `dx'`, `dy'`, +and `dz'` for the derived grids. + +However, since most operations are trivially generalized to allow either E or H +to be defined on the base grid, they are written to take the a full set of base +and derived cell widths, distinguished by which field they apply to rather than +their "base" or "derived" status. This removes the need for each function to +generate the derived widths, and makes the "base" vs "derived" distinction +unnecessary in the code. + +The resulting data structure containing all the cell widths takes the form of a +list-of-lists-of-arrays. The first list-of-arrays provides the cell widths for +the E-field fore-vectors, while the second list-of-arrays does the same for the +H-field back-vectors: + + [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...], [dz_e[0], ...]], + [[dx_h[0], dx_h[1], ...], [dy_h[0], ...], [dz_h[0], ...]]] + + where `dx_e[0]` is the x-width of the `m=0` cells, as used when calculating dE/dx, + and `dy_h[0]` is the y-width of the `n=0` cells, as used when calculating dH/dy, etc. """ From 35bd3d36f4c5adfdf50f22777579383a03bca5e8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 4 Jan 2020 18:20:09 -0800 Subject: [PATCH 215/437] remove old print --- meanas/fdfd/waveguide_2d.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index ecd40de..49f33ad 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -451,7 +451,6 @@ def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: for d in dxes[0]: n *= len(d) - print(wavenumber, n) Bz = -1j * wavenumber * sparse.eye(n) Dfx, Dfy = deriv_forward(dxes[0]) return cross([Dfx, Dfy, Bz]) From 8b0faf720d387f8777ee809bad55b05081a2d6b6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 4 Jan 2020 18:46:28 -0800 Subject: [PATCH 216/437] add info about functions vs operators --- meanas/fdmath/__init__.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 97491d9..b6b75d5 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -2,7 +2,34 @@ Basic discrete calculus for finite difference (fd) simulations. -TODO: short description of functional vs operator form + +Fields, Functions, and Operators +================================ + +Discrete fields are stored in one of two forms: + +- The `field_t` form is a multidimensional numpy array + + For a scalar field, this is just `U[m, n, p]`, where `m`, `n`, and `p` are + discrete indices referring to positions on the x, y, and z axes respectively. + + For a vector field, the first index specifies which vector component is accessed: + `E[:, m, n, p] = [Ex[m, n, p], Ey[m, n, p], Ez[m, n, p]]`. +- The `vfield_t` form is simply a vectorzied (i.e. 1D) version of the `field_t`, + as obtained by `meanas.fdmath.vectorization.vec` (effectively just `numpy.ravel`) + +Operators which act on fields also come in two forms: + + Python functions, created by the functions in `meanas.fdmath.functional`. + The generated functions act on fields in the `field_t` form. + + Linear operators, usually 2D sparse matrices using `scipy.sparse`, created + by `meanas.fdmath.operators`. These operators act on vectorized fields in the + `vfield_t` form. + +The operations performed should be equivalent: `functional.op(*args)(E)` should be +equivalent to `unvec(operators.op(*args) @ vec(E), E.shape[1:])`. + +Generally speaking the `field_t` form is easier to work with, but can be harder or less +efficient to compose (e.g. it is easy to generate a single matrix by multiplying a +series of other matrices). + Discrete calculus ================= From f408689ef374fcd4738dc93dd1cd30523db7f7da Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 4 Jan 2020 18:49:20 -0800 Subject: [PATCH 217/437] note about numpy ndarray --- meanas/fdmath/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index b6b75d5..0e165e2 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -8,7 +8,7 @@ Fields, Functions, and Operators Discrete fields are stored in one of two forms: -- The `field_t` form is a multidimensional numpy array +- The `field_t` form is a multidimensional `numpy.ndarray` + For a scalar field, this is just `U[m, n, p]`, where `m`, `n`, and `p` are discrete indices referring to positions on the x, y, and z axes respectively. + For a vector field, the first index specifies which vector component is accessed: From dcc9e7c2ad7ff8f4fa1b9fc5f9c7aa4debc5dbdf Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 6 Jan 2020 00:08:08 -0800 Subject: [PATCH 218/437] Update some type info --- meanas/fdmath/__init__.py | 8 ++++---- meanas/fdmath/types.py | 11 +++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 0e165e2..0803099 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -8,20 +8,20 @@ Fields, Functions, and Operators Discrete fields are stored in one of two forms: -- The `field_t` form is a multidimensional `numpy.ndarray` +- The `fdfield_t` form is a multidimensional `numpy.ndarray` + For a scalar field, this is just `U[m, n, p]`, where `m`, `n`, and `p` are discrete indices referring to positions on the x, y, and z axes respectively. + For a vector field, the first index specifies which vector component is accessed: `E[:, m, n, p] = [Ex[m, n, p], Ey[m, n, p], Ez[m, n, p]]`. -- The `vfield_t` form is simply a vectorzied (i.e. 1D) version of the `field_t`, +- The `vfdfield_t` form is simply a vectorzied (i.e. 1D) version of the `field_t`, as obtained by `meanas.fdmath.vectorization.vec` (effectively just `numpy.ravel`) Operators which act on fields also come in two forms: + Python functions, created by the functions in `meanas.fdmath.functional`. - The generated functions act on fields in the `field_t` form. + The generated functions act on fields in the `fdfield_t` form. + Linear operators, usually 2D sparse matrices using `scipy.sparse`, created by `meanas.fdmath.operators`. These operators act on vectorized fields in the - `vfield_t` form. + `vfdfield_t` form. The operations performed should be equivalent: `functional.op(*args)(E)` should be equivalent to `unvec(operators.op(*args) @ vec(E), E.shape[1:])`. diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py index 6e2fd63..f8719d1 100644 --- a/meanas/fdmath/types.py +++ b/meanas/fdmath/types.py @@ -29,12 +29,15 @@ dx_lists_t = List[List[numpy.ndarray]] ''' 'dxes' datastructure which contains grid cell width information in the following format: - [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]], - [[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]] + [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...], [dz_e[0], ...]], + [[dx_h[0], dx_h[1], ...], [dy_h[0], ...], [dz_h[0], ...]]] - where `dx_e_0` is the x-width of the `x=0` cells, as used when calculating dE/dx, - and `dy_h_0` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc. + where `dx_e[0]` is the x-width of the `x=0` cells, as used when calculating dE/dx, + and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc. ''' fdfield_updater_t = Callable[[fdfield_t], fdfield_t] +''' + Convenience type for functions which take and return an fdfield_t +''' From f69b8c9f1140e90d1dd240995e58940d516c6428 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 7 Jan 2020 21:31:16 -0800 Subject: [PATCH 219/437] Change comments to new format --- examples/fdtd.py | 13 +- meanas/fdfd/bloch.py | 232 +++++++++++++++++++-------------- meanas/fdfd/farfield.py | 84 ++++++------ meanas/fdmath/vectorization.py | 29 +++-- 4 files changed, 208 insertions(+), 150 deletions(-) diff --git a/examples/fdtd.py b/examples/fdtd.py index be3942b..3ffa077 100644 --- a/examples/fdtd.py +++ b/examples/fdtd.py @@ -20,9 +20,10 @@ def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern: """ Generate a masque.Pattern object containing a perturbed L3 cavity. - :param a: Lattice constant. - :param radius: Hole radius, in units of a (lattice constant). - :param kwargs: Keyword arguments: + Args: + a: Lattice constant. + radius: Hole radius, in units of a (lattice constant). + **kwargs: Keyword arguments: hole_dose, trench_dose, hole_layer, trench_layer: Shape properties for Pattern. Defaults *_dose=1, hole_layer=0, trench_layer=1. shifts_a, shifts_r: passed to pcgen.l3_shift; specifies lattice constant (1 - @@ -30,11 +31,13 @@ def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern: holes adjacent to the defect (same row). Defaults are 0.15 shift for first hole, 0.075 shift for third hole, and no radius change. xy_size: [x, y] number of mirror periods in each direction; total size is - 2 * n + 1 holes in each direction. Default [10, 10]. + `2 * n + 1` holes in each direction. Default `[10, 10]`. perturbed_radius: radius of holes perturbed to form an upwards-driected beam (multiplicative factor). Default 1.1. trench width: Width of the undercut trenches. Default 1.2e3. - :return: masque.Pattern object containing the L3 design + + Return: + `masque.Pattern` object containing the L3 design """ default_args = {'hole_dose': 1, diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index 28b82df..db055f6 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -4,47 +4,54 @@ Bloch eigenmode solver/operators This module contains functions for generating and solving the 3D Bloch eigenproblem. The approach is to transform the problem into the (spatial) fourier domain, transforming the equation - 1/mu * curl(1/eps * curl(H)) = (w/c)^2 H + + 1/mu * curl(1/eps * curl(H)) = (w/c)^2 H + into - conv(1/mu_k, ik x conv(1/eps_k, ik x H_k)) = (w/c)^2 H_k + + conv(1/mu_k, ik x conv(1/eps_k, ik x H_k)) = (w/c)^2 H_k + where: - - the _k subscript denotes a 3D fourier transformed field - - each component of H_k corresponds to a plane wave with wavevector k - - x is the cross product - - conv denotes convolution - Since k and H are orthogonal for each plane wave, we can use each - k to create an orthogonal basis (k, m, n), with k x m = n, and - |m| = |n| = 1. The cross products are then simplified with + - the `_k` subscript denotes a 3D fourier transformed field + - each component of `H_k` corresponds to a plane wave with wavevector `k` + - `x` is the cross product + - `conv()` denotes convolution - k @ h = kx hx + ky hy + kz hz = 0 = hk - h = hk + hm + hn = hm + hn - k = kk + km + kn = kk = |k| + Since `k` and `H` are orthogonal for each plane wave, we can use each + `k` to create an orthogonal basis (k, m, n), with `k x m = n`, and + `|m| = |n| = 1`. The cross products are then simplified with - k x h = (ky hz - kz hy, - kz hx - kx hz, - kx hy - ky hx) - = ((k x h) @ k, (k x h) @ m, (k x h) @ n)_kmn - = (0, (m x k) @ h, (n x k) @ h)_kmn # triple product ordering - = (0, kk (-n @ h), kk (m @ h))_kmn # (m x k) = -|k| n, etc. - = |k| (0, -h @ n, h @ m)_kmn + k @ h = kx hx + ky hy + kz hz = 0 = hk + h = hk + hm + hn = hm + hn + k = kk + km + kn = kk = |k| - k x h = (km hn - kn hm, - kn hk - kk hn, - kk hm - km hk)_kmn - = (0, -kk hn, kk hm)_kmn - = (-kk hn)(mx, my, mz) + (kk hm)(nx, ny, nz) - = |k| (hm * (nx, ny, nz) - hn * (mx, my, mz)) + k x h = (ky hz - kz hy, + kz hx - kx hz, + kx hy - ky hx) + = ((k x h) @ k, (k x h) @ m, (k x h) @ n)_kmn + = (0, (m x k) @ h, (n x k) @ h)_kmn # triple product ordering + = (0, kk (-n @ h), kk (m @ h))_kmn # (m x k) = -|k| n, etc. + = |k| (0, -h @ n, h @ m)_kmn - where h is shorthand for H_k, (...)_kmn deontes the (k, m, n) basis, - and e.g. hm is the component of h in the m direction. + k x h = (km hn - kn hm, + kn hk - kk hn, + kk hm - km hk)_kmn + = (0, -kk hn, kk hm)_kmn + = (-kk hn)(mx, my, mz) + (kk hm)(nx, ny, nz) + = |k| (hm * (nx, ny, nz) - hn * (mx, my, mz)) - We can also simplify conv(X_k, Y_k) as fftn(X * ifftn(Y_k)). + where `h` is shorthand for `H_k`, `(...)_kmn` deontes the `(k, m, n)` basis, + and e.g. `hm` is the component of `h` in the `m` direction. + + We can also simplify `conv(X_k, Y_k)` as `fftn(X * ifftn(Y_k))`. + + Using these results and storing `H_k` as `h = (hm, hn)`, we have + + e_xyz = fftn(1/eps * ifftn(|k| (hm * n - hn * m))) + b_mn = |k| (-e_xyz @ n, e_xyz @ m) + h_mn = fftn(1/mu * ifftn(b_m * m + b_n * n)) - Using these results and storing H_k as h = (hm, hn), we have - e_xyz = fftn(1/eps * ifftn(|k| (hm * n - hn * m))) - b_mn = |k| (-e_xyz @ n, e_xyz @ m) - h_mn = fftn(1/mu * ifftn(b_m * m + b_n * n)) which forms the operator from the left side of the equation. We can then use a preconditioned block Rayleigh iteration algorithm, as in @@ -120,12 +127,15 @@ def generate_kmn(k0: numpy.ndarray, """ Generate a (k, m, n) orthogonal basis for each k-vector in the simulation grid. - :param k0: [k0x, k0y, k0z], Bloch wavevector, in G basis. - :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. - :param shape: [nx, ny, nz] shape of the simulation grid. - :return: (|k|, m, n) where |k| has shape tuple(shape) + (1,) - and m, n have shape tuple(shape) + (3,). - All are given in the xyz basis (e.g. |k|[0,0,0] = norm(G_matrix @ k0)). + Args: + k0: [k0x, k0y, k0z], Bloch wavevector, in G basis. + G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + shape: [nx, ny, nz] shape of the simulation grid. + + Returns: + `(|k|, m, n)` where `|k|` has shape `tuple(shape) + (1,)` + and `m`, `n` have shape `tuple(shape) + (3,)`. + All are given in the xyz basis (e.g. `|k|[0,0,0] = norm(G_matrix @ k0)`). """ k0 = numpy.array(k0) @@ -159,21 +169,27 @@ def maxwell_operator(k0: numpy.ndarray, ) -> Callable[[numpy.ndarray], numpy.ndarray]: """ Generate the Maxwell operator + conv(1/mu_k, ik x conv(1/eps_k, ik x ___)) + which is the spatial-frequency-space representation of + 1/mu * curl(1/eps * curl(___)) - The operator is a function that acts on a vector h_mn of size (2 * epsilon[0].size) + The operator is a function that acts on a vector h_mn of size `2 * epsilon[0].size` - See the module-level docstring for more information. + See the `meanas.fdfd.bloch` docstring for more information. - :param k0: Bloch wavevector, [k0x, k0y, k0z]. - :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. - :param epsilon: Dielectric constant distribution for the simulation. - All fields are sampled at cell centers (i.e., NOT Yee-gridded) - :param mu: Magnetic permability distribution for the simulation. - Default None (1 everywhere). - :return: Function which applies the maxwell operator to h_mn. + Args: + k0: Bloch wavevector, `[k0x, k0y, k0z]`. + G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + mu: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + + Returns: + Function which applies the maxwell operator to h_mn. """ shape = epsilon[0].shape + (1,) @@ -189,8 +205,11 @@ def maxwell_operator(k0: numpy.ndarray, h is complex 2-field in (m, n) basis, vectorized - :param h: Raveled h_mn; size (2 * epsilon[0].size). - :return: Raveled conv(1/mu_k, ik x conv(1/eps_k, ik x h_mn)). + Args: + h: Raveled h_mn; size `2 * epsilon[0].size`. + + Returns: + Raveled conv(1/mu_k, ik x conv(1/eps_k, ik x h_mn)). """ hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] @@ -231,18 +250,22 @@ def hmn_2_exyz(k0: numpy.ndarray, ) -> Callable[[numpy.ndarray], fdfield_t]: """ Generate an operator which converts a vectorized spatial-frequency-space - h_mn into an E-field distribution, i.e. + `h_mn` into an E-field distribution, i.e. + ifft(conv(1/eps_k, ik x h_mn)) - The operator is a function that acts on a vector h_mn of size (2 * epsilon[0].size) + The operator is a function that acts on a vector `h_mn` of size `2 * epsilon[0].size`. - See the module-level docstring for more information. + See the `meanas.fdfd.bloch` docstring for more information. - :param k0: Bloch wavevector, [k0x, k0y, k0z]. - :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. - :param epsilon: Dielectric constant distribution for the simulation. - All fields are sampled at cell centers (i.e., NOT Yee-gridded) - :return: Function for converting h_mn into E_xyz + Args: + k0: Bloch wavevector, `[k0x, k0y, k0z]`. + G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + + Returns: + Function for converting `h_mn` into `E_xyz` """ shape = epsilon[0].shape + (1,) epsilon = numpy.stack(epsilon, 3) @@ -266,18 +289,22 @@ def hmn_2_hxyz(k0: numpy.ndarray, ) -> Callable[[numpy.ndarray], fdfield_t]: """ Generate an operator which converts a vectorized spatial-frequency-space - h_mn into an H-field distribution, i.e. + `h_mn` into an H-field distribution, i.e. + ifft(h_mn) - The operator is a function that acts on a vector h_mn of size (2 * epsilon[0].size) + The operator is a function that acts on a vector `h_mn` of size `2 * epsilon[0].size`. - See the module-level docstring for more information. + See the `meanas.fdfd.bloch` docstring for more information. - :param k0: Bloch wavevector, [k0x, k0y, k0z]. - :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. - :param epsilon: Dielectric constant distribution for the simulation. - Only epsilon[0].shape is used. - :return: Function for converting h_mn into H_xyz + Args: + k0: Bloch wavevector, `[k0x, k0y, k0z]`. + G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + epsilon: Dielectric constant distribution for the simulation. + Only `epsilon[0].shape` is used. + + Returns: + Function for converting `h_mn` into `H_xyz` """ shape = epsilon[0].shape + (1,) _k_mag, m, n = generate_kmn(k0, G_matrix, shape) @@ -298,18 +325,23 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, ) -> Callable[[numpy.ndarray], numpy.ndarray]: """ Generate an approximate inverse of the Maxwell operator, + ik x conv(eps_k, ik x conv(mu_k, ___)) + which can be used to improve the speed of ARPACK in shift-invert mode. - See the module-level docstring for more information. + See the `meanas.fdfd.bloch` docstring for more information. - :param k0: Bloch wavevector, [k0x, k0y, k0z]. - :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. - :param epsilon: Dielectric constant distribution for the simulation. - All fields are sampled at cell centers (i.e., NOT Yee-gridded) - :param mu: Magnetic permability distribution for the simulation. - Default None (1 everywhere). - :return: Function which applies the approximate inverse of the maxwell operator to h_mn. + Args: + k0: Bloch wavevector, `[k0x, k0y, k0z]`. + G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + mu: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + + Returns: + Function which applies the approximate inverse of the maxwell operator to `h_mn`. """ shape = epsilon[0].shape + (1,) epsilon = numpy.stack(epsilon, 3) @@ -325,8 +357,11 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, h is complex 2-field in (m, n) basis, vectorized - :param h: Raveled h_mn; size (2 * epsilon[0].size). - :return: Raveled ik x conv(eps_k, ik x conv(mu_k, h_mn)) + Args: + h: Raveled h_mn; size `2 * epsilon[0].size`. + + Returns: + Raveled ik x conv(eps_k, ik x conv(mu_k, h_mn)) """ hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] @@ -376,16 +411,20 @@ def find_k(frequency: float, """ Search for a bloch vector that has a given frequency. - :param frequency: Target frequency. - :param tolerance: Target frequency tolerance. - :param direction: k-vector direction to search along. - :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. - :param epsilon: Dielectric constant distribution for the simulation. - All fields are sampled at cell centers (i.e., NOT Yee-gridded) - :param mu: Magnetic permability distribution for the simulation. - Default None (1 everywhere). - :param band: Which band to search in. Default 0 (lowest frequency). - return: (k, actual_frequency) The found k-vector and its frequency + Args: + frequency: Target frequency. + tolerance: Target frequency tolerance. + direction: k-vector direction to search along. + G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + mu: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + band: Which band to search in. Default 0 (lowest frequency). + + Returns: + `(k, actual_frequency)` + The found k-vector and its frequency. """ direction = numpy.array(direction) / norm(direction) @@ -419,16 +458,19 @@ def eigsolve(num_modes: int, Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector k0 of the specified structure. - :param k0: Bloch wavevector, [k0x, k0y, k0z]. - :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. - :param epsilon: Dielectric constant distribution for the simulation. - All fields are sampled at cell centers (i.e., NOT Yee-gridded) - :param mu: Magnetic permability distribution for the simulation. - Default None (1 everywhere). - :param tolerance: Solver stops when fractional change in the objective - trace(Z.H @ A @ Z @ inv(Z Z.H)) is smaller than the tolerance - :return: (eigenvalues, eigenvectors) where eigenvalues[i] corresponds to the - vector eigenvectors[i, :] + Args: + k0: Bloch wavevector, `[k0x, k0y, k0z]`. + G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + mu: Magnetic permability distribution for the simulation. + Default `None` (1 everywhere). + tolerance: Solver stops when fractional change in the objective + `trace(Z.H @ A @ Z @ inv(Z Z.H))` is smaller than the tolerance + + Returns: + `(eigenvalues, eigenvectors)` where `eigenvalues[i]` corresponds to the + vector `eigenvectors[i, :]` """ h_size = 2 * epsilon[0].size diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index d0ec008..ea11224 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -21,27 +21,31 @@ def near_to_farfield(E_near: fdfield_t, The input fields should be complex phasors. - :param E_near: List of 2 ndarrays containing the 2D phasor field slices for the transverse - E fields (e.g. [Ex, Ey] for calculating the farfield toward the z-direction). - :param H_near: List of 2 ndarrays containing the 2D phasor field slices for the transverse - H fields (e.g. [Hx, hy] for calculating the farfield towrad the z-direction). - :param dx: Cell size along x-dimension, in units of wavelength. - :param dy: Cell size along y-dimension, in units of wavelength. - :param padded_size: Shape of the output. A single integer `n` will be expanded to `(n, n)`. - Powers of 2 are most efficient for FFT computation. - Default is the smallest power of 2 larger than the input, for each axis. - :returns: Dict with keys - 'E_far': Normalized E-field farfield; multiply by - (i k exp(-i k r) / (4 pi r)) to get the actual field value. - 'H_far': Normalized H-field farfield; multiply by - (i k exp(-i k r) / (4 pi r)) to get the actual field value. - 'kx', 'ky': Wavevector values corresponding to the x- and y- axes in E_far and H_far, - normalized to wavelength (dimensionless). - 'dkx', 'dky': step size for kx and ky, normalized to wavelength. - 'theta': arctan2(ky, kx) corresponding to each (kx, ky). - This is the angle in the x-y plane, counterclockwise from above, starting from +x. - 'phi': arccos(kz / k) corresponding to each (kx, ky). - This is the angle away from +z. + Args: + E_near: List of 2 ndarrays containing the 2D phasor field slices for the transverse + E fields (e.g. [Ex, Ey] for calculating the farfield toward the z-direction). + H_near: List of 2 ndarrays containing the 2D phasor field slices for the transverse + H fields (e.g. [Hx, hy] for calculating the farfield towrad the z-direction). + dx: Cell size along x-dimension, in units of wavelength. + dy: Cell size along y-dimension, in units of wavelength. + padded_size: Shape of the output. A single integer `n` will be expanded to `(n, n)`. + Powers of 2 are most efficient for FFT computation. + Default is the smallest power of 2 larger than the input, for each axis. + + Returns: + Dict with keys + + - `E_far`: Normalized E-field farfield; multiply by + (i k exp(-i k r) / (4 pi r)) to get the actual field value. + - `H_far`: Normalized H-field farfield; multiply by + (i k exp(-i k r) / (4 pi r)) to get the actual field value. + - `kx`, `ky`: Wavevector values corresponding to the x- and y- axes in E_far and H_far, + normalized to wavelength (dimensionless). + - `dkx`, `dky`: step size for kx and ky, normalized to wavelength. + - `theta`: arctan2(ky, kx) corresponding to each (kx, ky). + This is the angle in the x-y plane, counterclockwise from above, starting from +x. + - `phi`: arccos(kz / k) corresponding to each (kx, ky). + This is the angle away from +z. """ if not len(E_near) == 2: @@ -129,23 +133,27 @@ def far_to_nearfield(E_far: fdfield_t, The input fields should be complex phasors. - :param E_far: List of 2 ndarrays containing the 2D phasor field slices for the transverse - E fields (e.g. [Ex, Ey] for calculating the nearfield toward the z-direction). - Fields should be normalized so that - E_far = E_far_actual / (i k exp(-i k r) / (4 pi r)) - :param H_far: List of 2 ndarrays containing the 2D phasor field slices for the transverse - H fields (e.g. [Hx, hy] for calculating the nearfield toward the z-direction). - Fields should be normalized so that - H_far = H_far_actual / (i k exp(-i k r) / (4 pi r)) - :param dkx: kx discretization, in units of wavelength. - :param dky: ky discretization, in units of wavelength. - :param padded_size: Shape of the output. A single integer `n` will be expanded to `(n, n)`. - Powers of 2 are most efficient for FFT computation. - Default is the smallest power of 2 larger than the input, for each axis. - :returns: Dict with keys - 'E': E-field nearfield - 'H': H-field nearfield - 'dx', 'dy': spatial discretization, normalized to wavelength (dimensionless) + Args: + E_far: List of 2 ndarrays containing the 2D phasor field slices for the transverse + E fields (e.g. [Ex, Ey] for calculating the nearfield toward the z-direction). + Fields should be normalized so that + E_far = E_far_actual / (i k exp(-i k r) / (4 pi r)) + H_far: List of 2 ndarrays containing the 2D phasor field slices for the transverse + H fields (e.g. [Hx, hy] for calculating the nearfield toward the z-direction). + Fields should be normalized so that + H_far = H_far_actual / (i k exp(-i k r) / (4 pi r)) + dkx: kx discretization, in units of wavelength. + dky: ky discretization, in units of wavelength. + padded_size: Shape of the output. A single integer `n` will be expanded to `(n, n)`. + Powers of 2 are most efficient for FFT computation. + Default is the smallest power of 2 larger than the input, for each axis. + + Returns: + Dict with keys + + - `E`: E-field nearfield + - `H`: H-field nearfield + - `dx`, `dy`: spatial discretization, normalized to wavelength (dimensionless) """ if not len(E_far) == 2: diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py index 8e8099b..63d78ef 100644 --- a/meanas/fdmath/vectorization.py +++ b/meanas/fdmath/vectorization.py @@ -1,6 +1,6 @@ """ -Functions for moving between a vector field (list of 3 ndarrays, [f_x, f_y, f_z]) -and a 1D array representation of that field [f_x0, f_x1, f_x2,... f_y0,... f_z0,...]. +Functions for moving between a vector field (list of 3 ndarrays, `[f_x, f_y, f_z]`) +and a 1D array representation of that field `[f_x0, f_x1, f_x2,... f_y0,... f_z0,...]`. Vectorized versions of the field use row-major (ie., C-style) ordering. """ @@ -17,11 +17,14 @@ def vec(f: fdfield_t) -> vfdfield_t: """ Create a 1D ndarray from a 3D vector field which spans a 1-3D region. - Returns None if called with f=None. + Returns `None` if called with `f=None`. - :param f: A vector field, [f_x, f_y, f_z] where each f_ component is a 1 to - 3D ndarray (f_* should all be the same size). Doesn't fail with f=None. - :return: A 1D ndarray containing the linearized field (or None) + Args: + f: A vector field, `[f_x, f_y, f_z]` where each `f_` component is a 1- to + 3-D ndarray (`f_*` should all be the same size). Doesn't fail with `f=None`. + + Returns: + 1D ndarray containing the linearized field (or `None`) """ if numpy.any(numpy.equal(f, None)): return None @@ -31,15 +34,17 @@ def vec(f: fdfield_t) -> vfdfield_t: def unvec(v: vfdfield_t, shape: numpy.ndarray) -> fdfield_t: """ Perform the inverse of vec(): take a 1D ndarray and output a 3D field - of form [f_x, f_y, f_z] where each of f_* is a len(shape)-dimensional + of form `[f_x, f_y, f_z]` where each of `f_*` is a len(shape)-dimensional ndarray. - Returns None if called with v=None. + Returns `None` if called with `v=None`. - :param v: 1D ndarray representing a 3D vector field of shape shape (or None) - :param shape: shape of the vector field - :return: [f_x, f_y, f_z] where each f_ is a len(shape) dimensional ndarray - (or None) + Args: + v: 1D ndarray representing a 3D vector field of shape shape (or None) + shape: shape of the vector field + + Returns: + `[f_x, f_y, f_z]` where each `f_` is a `len(shape)` dimensional ndarray (or `None`) """ if numpy.any(numpy.equal(v, None)): return None From f9d90378b4c336b3de9e88e6c6cce2dd9103a9af Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 8 Jan 2020 00:51:56 -0800 Subject: [PATCH 220/437] more documentation --- meanas/fdmath/__init__.py | 137 +++++++++++++++++++++++++++++++++++++- meanas/fdmath/types.py | 2 +- meanas/fdtd/__init__.py | 30 +++++++++ meanas/fdtd/base.py | 89 ++++++++++++++++++++++++- meanas/fdtd/boundaries.py | 2 + meanas/fdtd/energy.py | 7 ++ meanas/fdtd/pml.py | 3 + 7 files changed, 266 insertions(+), 4 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 0803099..76bd908 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -392,13 +392,108 @@ $$ -\\tilde{\\partial}_t \\hat{\\nabla} \\times \\hat{H}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} \\\\ \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &= -\\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_\\vec{r} \\tilde{E}_{l, \\vec{r}} + \\hat{\\partial}_t \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\ - \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l, \\vec{r}}) + \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) + \\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_\\vec{r} \\cdot \\tilde{E}_{l, \\vec{r}} &= \\tilde{\\partial}_t \\tilde{J}_{l - \\frac{1}{2}, \\vec{r}} \\end{align*} $$ +Frequency domain +---------------- + +We can substitute in a time-harmonic fields + +$$ + \\begin{align*} + \\tilde{E}_\\vec{r} &= \\tilde{E}_\\vec{r} e^{-\\imath \\omega l \\Delta_t} \\\\ + \\tilde{J}_\\vec{r} &= \\tilde{J}_\\vec{r} e^{-\\imath \\omega (l - \\frac{1}{2}) \\Delta_t} + \\end{align*} +$$ + +resulting in + +$$ + \\begin{align*} + \\tilde{\\partial}_t &\\Rightarrow (e^{ \\imath \\omega \\Delta_t} - 1) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_t} \\sin(\\omega \\Delta_t / 2) e^{-\\imath \\omega \\Delta_t / 2} = -\\imath \\Omega e^{-\\imath \\omega \\Delta_t / 2}\\\\ + \\hat{\\partial}_t &\\Rightarrow (1 - e^{-\\imath \\omega \\Delta_t}) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_t} \\sin(\\omega \\Delta_t / 2) e^{ \\imath \\omega \\Delta_t / 2} = -\\imath \\Omega e^{ \\imath \\omega \\Delta_t / 2}\\\\ + \\Omega &= 2 \\sin(\\omega \\Delta_t / 2) / \\Delta_t + \\end{align*} +$$ + +This gives the frequency-domain wave equation, + +$$ + \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_\\vec{r}) + -\\Omega^2 \\epsilon_\\vec{r} \\cdot \\tilde{E}_\\vec{r} = \\imath \\Omega \\tilde{J}_\\vec{r} +$$ + + +Plane waves and Dispersion relation +------------------------------------ + +With uniform material distribution and no sources + +$$ + \\begin{align*} + \\mu_{\\vec{r} + \\frac{1}{2}} &= \\mu \\\\ + \\epsilon_\\vec{r} &= \\epsilon \\\\ + \\tilde{J}_\\vec{r} &= 0 \\\\ + \\end{align*} +$$ + +the frequency domain wave equation simplifies to + +$$ \\hat{\\nabla} \\times \\tilde{\\nabla} \\times \\tilde{E}_\\vec{r} - \\Omega^2 \\epsilon \\mu \\tilde{E}_\\vec{r} = 0 $$ + +Since \\( \\hat{\\nabla} \\cdot \\tilde{E}_\\vec{r} = 0 \\), we can simplify + +$$ + \\begin{align*} + \\hat{\\nabla} \\times \\tilde{\\nabla} \\times \\tilde{E}_\\vec{r} + &= \\tilde{\\nabla}(\\hat{\\nabla} \\cdot \\tilde{E}_\\vec{r}) - \\hat{\\nabla} \\cdot \\tilde{\\nabla} \\tilde{E}_\\vec{r} \\\\ + &= - \\hat{\\nabla} \\cdot \\tilde{\\nabla} \\tilde{E}_\\vec{r} \\\\ + &= - \\tilde{\\nabla}^2 \\tilde{E}_\\vec{r} + \\end{align*} +$$ + +and we get + +$$ \\tilde{\\nabla}^2 \\tilde{E}_\\vec{r} + \\Omega^2 \\epsilon \\mu \\tilde{E}_\\vec{r} = 0 $$ + +We can convert this to three scalar-wave equations of the form + +$$ (\\tilde{\\nabla}^2 + K^2) \\phi_\\vec{r} = 0 $$ + +with \\( K^2 = \\Omega^2 \\mu \\epsilon \\). Now we let + +$$ \\phi_\\vec{r} = A e^{\\imath (k_x m \\Delta_x + k_y n \\Delta_y + k_z p \\Delta_z)} $$ + +resulting in + +$$ + \\begin{align*} + \\tilde{\\partial}_x &\\Rightarrow (e^{ \\imath k_x \\Delta_x} - 1) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_x} \\sin(k_x \\Delta_x / 2) e^{ \\imath k_x \\Delta_x / 2} = \\imath K_x e^{ \\imath k_x \\Delta_x / 2}\\\\ + \\hat{\\partial}_x &\\Rightarrow (1 - e^{-\\imath k_x \\Delta_x}) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_x} \\sin(k_x \\Delta_x / 2) e^{-\\imath k_x \\Delta_x / 2} = \\imath K_x e^{-\\imath k_x \\Delta_x / 2}\\\\ + K_x &= 2 \\sin(k_x \\Delta_x / 2) / \\Delta_x \\\\ + \\end{align*} +$$ + +with similar expressions for the y and z dimnsions (and \\( K_y, K_z \\)). + +This implies + +$$ + \\tilde{\\nabla}^2 = -(K_x^2 + K_y^2 + K_z^2) \\phi_\\vec{r} \\\\ + K_x^2 + K_y^2 + K_z^2 = \\Omega^2 \\mu \\epsilon = \\Omega^2 / c^2 +$$ + +Assuming real \\( (k_x, k_y, k_z), \\omega \\) will be real only if + +$$ c^2 \\Delta_t^2 = \\frac{\\Delta_t^2}{\\mu \\epsilon} < 1/(\\frac{1}{\\Delta_x^2} + \\frac{1}{\\Delta_y^2} + \\frac{1}{\\Delta_z^2}) $$ + +If \\( \\Delta_x = \\Delta_y = \\Delta_z \\), this simplifies to \\( c \\Delta_t < \\frac{\\Delta_x}{\\sqrt{3}} \\). + Grid description ================ @@ -598,6 +693,46 @@ H-field back-vectors: where `dx_e[0]` is the x-width of the `m=0` cells, as used when calculating dE/dx, and `dy_h[0]` is the y-width of the `n=0` cells, as used when calculating dH/dy, etc. + +Permittivity and Permeability +============================= + +Since each vector component of E and H is defined in a different location and represents +a different volume, the value of the spatially-discrete `epsilon` and `mu` can also be +different for all three field components, even when representing a simple planar interface +between two isotropic materials. + +As a result, `epsilon` and `mu` are taken to have the same dimensions as the field, and +composed of the three diagonal tensor components: + + [equations: epsilon_and_mu] + epsilon = [epsilon_xx, epsilon_yy, epsilon_zz] + mu = [mu_xx, mu_yy, mu_zz] + +or + +$$ + \\epsilon = \\begin{bmatrix} \\epsilon_{xx} & 0 & 0 \\\\ + 0 & \\epsilon_{yy} & 0 \\\\ + 0 & 0 & \\epsilon_{zz} \\end{bmatrix} +$$ +$$ + \\mu = \\begin{bmatrix} \\mu_{xx} & 0 & 0 \\\\ + 0 & \\mu_{yy} & 0 \\\\ + 0 & 0 & \\mu_{zz} \\end{bmatrix} +$$ + +where the off-diagonal terms (e.g. `epsilon_xy`) are assumed to be zero. + +High-accuracy volumetric integration of shapes on multiple grids can be performed +by the [gridlock](https://mpxd.net/code/jan/gridlock) module. + +The values of the vacuum permittivity and permability effectively become scaling +factors that appear in several locations (e.g. between the E and H fields). In +order to limit floating-point inaccuracy and simplify calculations, they are often +set to 1 and relative permittivities and permeabilities are used in their places; +the true values can be multiplied back in after the simulation is complete if non- +normalized results are needed. """ from .types import fdfield_t, vfdfield_t, dx_lists_t, fdfield_updater_t diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py index f8719d1..ea78f8f 100644 --- a/meanas/fdmath/types.py +++ b/meanas/fdmath/types.py @@ -37,7 +37,7 @@ dx_lists_t = List[List[numpy.ndarray]] ''' -fdfield_updater_t = Callable[[fdfield_t], fdfield_t] +fdfield_updater_t = Callable[..., fdfield_t] ''' Convenience type for functions which take and return an fdfield_t ''' diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py index 02cfc05..2d1588e 100644 --- a/meanas/fdtd/__init__.py +++ b/meanas/fdtd/__init__.py @@ -1,5 +1,35 @@ """ Utilities for running finite-difference time-domain (FDTD) simulations + + +Timestep +======== + +From the discussion of "Plane waves and the Dispersion relation" in `meanas.fdmath`, +we have + +$$ c^2 \\Delta_t^2 = \\frac{\\Delta_t^2}{\\mu \\epsilon} < 1/(\\frac{1}{\\Delta_x^2} + \\frac{1}{\\Delta_y^2} + \\frac{1}{\\Delta_z^2}) $$ + +or, if \\( \\Delta_x = \\Delta_y = \\Delta_z \\), then \\( c \\Delta_t < \\frac{\\Delta_x}{\\sqrt{3}} \\). + +Based on this, we can set + + dt = sqrt(mu.min() * epsilon.min()) / sqrt(1/dx_min**2 + 1/dy_min**2 + 1/dz_min**2) + +The `dx_min`, `dy_min`, `dz_min` should be the minimum value across both the base and derived grids. + + +Poynting Vector +=============== +# TODO + +Energy conservation +=================== +# TODO + +Boundary conditions +=================== +# TODO notes about boundaries / PMLs """ from .base import maxwell_e, maxwell_h diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py index fa478ba..fd21642 100644 --- a/meanas/fdtd/base.py +++ b/meanas/fdtd/base.py @@ -1,5 +1,7 @@ """ Basic FDTD field updates + + """ from typing import List, Callable, Tuple, Dict import numpy @@ -12,12 +14,51 @@ __author__ = 'Jan Petykiewicz' def maxwell_e(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t: + """ + Build a function which performs a portion the time-domain E-field update, + + E += curl_back(H[t]) / epsilon + + The full update should be + + E += (curl_back(H[t]) + J) / epsilon + + which requires an additional step of `E += J / epsilon` which is not performed + by the generated function. + + See `meanas.fdmath` for descriptions of + + - This update step: "Maxwell's equations" section + - `dxes`: "Datastructure: dx_lists_t" section + - `epsilon`: "Permittivity and Permeability" section + + Also see the "Timestep" section of `meanas.fdtd` for a discussion of + the `dt` parameter. + + Args: + dt: Timestep. See `meanas.fdtd` for details. + dxes: Grid description; see `meanas.fdmath`. + + Returns: + Function `f(E_old, H_old, epsilon) -> E_new`. + """ if dxes is not None: curl_h_fun = curl_back(dxes[1]) else: curl_h_fun = curl_back() def me_fun(e: fdfield_t, h: fdfield_t, epsilon: fdfield_t): + """ + Update the E-field. + + Args: + e: E-field at time t=0 + h: H-field at time t=0.5 + epsilon: Dielectric constant distribution. + + Returns: + E-field at time t=1 + """ e += dt * curl_h_fun(h) / epsilon return e @@ -25,13 +66,57 @@ def maxwell_e(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t: def maxwell_h(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t: + """ + Build a function which performs part of the time-domain H-field update, + + H -= curl_forward(E[t]) / mu + + The full update should be + + H -= (curl_forward(E[t]) - M) / mu + + which requires an additional step of `H += M / mu` which is not performed + by the generated function; this step can be omitted if there is no magnetic + current `M`. + + See `meanas.fdmath` for descriptions of + + - This update step: "Maxwell's equations" section + - `dxes`: "Datastructure: dx_lists_t" section + - `mu`: "Permittivity and Permeability" section + + Also see the "Timestep" section of `meanas.fdtd` for a discussion of + the `dt` parameter. + + Args: + dt: Timestep. See `meanas.fdtd` for details. + dxes: Grid description; see `meanas.fdmath`. + + Returns: + Function `f(E_old, H_old, epsilon) -> E_new`. + """ if dxes is not None: curl_e_fun = curl_forward(dxes[0]) else: curl_e_fun = curl_forward() - def mh_fun(e: fdfield_t, h: fdfield_t): - h -= dt * curl_e_fun(e) + def mh_fun(e: fdfield_t, h: fdfield_t, mu: fdfield_t = None): + """ + Update the H-field. + + Args: + e: E-field at time t=1 + h: H-field at time t=0.5 + mu: Magnetic permeability. Default is 1 everywhere. + + Returns: + H-field at time t=1.5 + """ + if mu is not None: + h -= dt * curl_e_fun(e) / mu + else: + h -= dt * curl_e_fun(e) + return h return mh_fun diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py index 0f0b1a6..10c966e 100644 --- a/meanas/fdtd/boundaries.py +++ b/meanas/fdtd/boundaries.py @@ -1,5 +1,7 @@ """ Boundary conditions + +#TODO conducting boundary documentation """ from typing import List, Callable, Tuple, Dict diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index 41268b7..dc5848a 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -5,10 +5,14 @@ import numpy from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t from ..fdmath.functional import deriv_back, deriv_forward + def poynting(e: fdfield_t, h: fdfield_t, dxes: dx_lists_t = None, ) -> fdfield_t: + """ + Calculate the poynting vector + """ if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) @@ -32,6 +36,9 @@ def poynting_divergence(s: fdfield_t = None, h: fdfield_t = None, dxes: dx_lists_t = None, ) -> fdfield_t: + """ + Calculate the divergence of the poynting vector + """ if s is None: s = poynting(e, h, dxes=dxes) diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py index af15a5a..2c4ae1e 100644 --- a/meanas/fdtd/pml.py +++ b/meanas/fdtd/pml.py @@ -1,6 +1,9 @@ """ PML implementations +#TODO discussion of PMLs +#TODO cpml documentation + """ # TODO retest pmls! From 034f79eae6c335679fb1d96657d95f44ca8d8e02 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 12 Jan 2020 22:50:01 -0800 Subject: [PATCH 221/437] Poynting vector doc updates --- meanas/fdmath/__init__.py | 21 ++++--- meanas/fdtd/__init__.py | 122 ++++++++++++++++++++++++++++++++++++-- meanas/fdtd/base.py | 4 +- 3 files changed, 131 insertions(+), 16 deletions(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 76bd908..cb47c79 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -288,9 +288,9 @@ If we discretize both space (m,n,p) and time (l), Maxwell's equations become $$ \\begin{align*} \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &= -\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} - + \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}} \\\\ - \\hat{\\nabla} \\times \\hat{H}_{l,\\vec{r} + \\frac{1}{2}} &= \\hat{\\partial}_t \\tilde{D}_{l, \\vec{r}} - + \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\ + - \\hat{M}_{l, \\vec{r} + \\frac{1}{2}} \\\\ + \\hat{\\nabla} \\times \\hat{H}_{l-\\frac{1}{2},\\vec{r} + \\frac{1}{2}} &= \\hat{\\partial}_t \\tilde{D}_{l, \\vec{r}} + + \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\ \\tilde{\\nabla} \\cdot \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &= 0 \\\\ \\hat{\\nabla} \\cdot \\tilde{D}_{l,\\vec{r}} &= \\rho_{l,\\vec{r}} \\end{align*} $$ @@ -311,9 +311,9 @@ and \\( \\epsilon \\) and \\( \\mu \\) are the dielectric permittivity and magne The above is Yee's algorithm, written in a form analogous to Maxwell's equations. The time derivatives can be expanded to form the update equations: - [code: Maxwell's equations] - H[i, j, k] -= (curl_forward(E[t])[i, j, k] - M[t, i, j, k]) / mu[i, j, k] - E[i, j, k] += (curl_back( H[t])[i, j, k] + J[t, i, j, k]) / epsilon[i, j, k] + [code: Maxwell's equations updates] + H[i, j, k] -= dt * (curl_forward(E)[i, j, k] + M[t, i, j, k]) / mu[i, j, k] + E[i, j, k] += dt * (curl_back( H)[i, j, k] + J[t, i, j, k]) / epsilon[i, j, k] Note that the E-field fore-vector and H-field back-vector are offset by a half-cell, resulting in distinct locations for all six E- and H-field components: @@ -383,7 +383,7 @@ $$ \\begin{align*} \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &= -\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} - + \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}} \\\\ + - \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}} \\\\ \\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &= -\\tilde{\\partial}_t \\hat{H}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} \\\\ \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &= @@ -488,11 +488,16 @@ $$ K_x^2 + K_y^2 + K_z^2 = \\Omega^2 \\mu \\epsilon = \\Omega^2 / c^2 $$ +where \\( c = \\sqrt{\\mu \\epsilon} \\). + Assuming real \\( (k_x, k_y, k_z), \\omega \\) will be real only if $$ c^2 \\Delta_t^2 = \\frac{\\Delta_t^2}{\\mu \\epsilon} < 1/(\\frac{1}{\\Delta_x^2} + \\frac{1}{\\Delta_y^2} + \\frac{1}{\\Delta_z^2}) $$ -If \\( \\Delta_x = \\Delta_y = \\Delta_z \\), this simplifies to \\( c \\Delta_t < \\frac{\\Delta_x}{\\sqrt{3}} \\). +If \\( \\Delta_x = \\Delta_y = \\Delta_z \\), this simplifies to \\( c \\Delta_t < \\Delta_x / \\sqrt{3} \\). +This last form can be interpreted as enforcing causality; the distance that light +travels in one timestep (i.e., \\( c \\Delta_t \\)) must be less than the diagonal +of the smallest cell ( \\( \\Delta_x / \\sqrt{3} \\) when on a uniform cubic grid). Grid description diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py index 2d1588e..3d498a9 100644 --- a/meanas/fdtd/__init__.py +++ b/meanas/fdtd/__init__.py @@ -1,6 +1,9 @@ """ Utilities for running finite-difference time-domain (FDTD) simulations +See the discussion of `Maxwell's Equations` in `meanas.fdmath` for basic +mathematical background. + Timestep ======== @@ -19,13 +22,120 @@ Based on this, we can set The `dx_min`, `dy_min`, `dz_min` should be the minimum value across both the base and derived grids. -Poynting Vector -=============== -# TODO +Poynting Vector and Energy Conservation +======================================= + +Let + +$$ \\begin{align*} + \\tilde{S}_{l, l', \\vec{r}} &=& &\\tilde{E}_{l, \\vec{r}} \\otimes \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\\\ + &=& &\\vec{x} (\\tilde{E}^y_{l,m+1,n,p} \\hat{H}^z_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^z_{l,m+1,n,p} \\hat{H}^y_{l', \\vec{r} + \\frac{1}{2}}) \\\\ + & &+ &\\vec{y} (\\tilde{E}^z_{l,m,n+1,p} \\hat{H}^x_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^x_{l,m,n+1,p} \\hat{H}^z_{l', \\vec{r} + \\frac{1}{2}}) \\\\ + & &+ &\\vec{z} (\\tilde{E}^x_{l,m,n,p+1} \\hat{H}^y_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^y_{l,m,n,p+1} \\hat{H}^z_{l', \\vec{r} + \\frac{1}{2}}) + \\end{align*} +$$ + +where \\( \\vec{r} = (m, n, p) \\) and \\( \\otimes \\) is a modified cross product +in which the \\( \\tilde{E} \\) terms are shifted as indicated. + +By taking the divergence and rearranging terms, we can show that + +$$ + \\begin{align*} + \\hat{\\nabla} \\cdot \\tilde{S}_{l, l', \\vec{r}} + &= \\hat{\\nabla} \\cdot (\\tilde{E}_{l, \\vec{r}} \\otimes \\hat{H}_{l', \\vec{r} + \\frac{1}{2}}) \\\\ + &= \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l, \\vec{r}} - + \\tilde{E}_{l, \\vec{r}} \\cdot \\hat{\\nabla} \\times \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\\\ + &= \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\cdot + (-\\tilde{\\partial}_t \\mu_{\\vec{r} + \\frac{1}{2}} \\hat{H}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} - + \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}}) - + \\tilde{E}_{l, \\vec{r}} \\cdot (\\hat{\\partial}_t \\tilde{\\epsilon}_\\vec{r} \\tilde{E}_{l'+\\frac{1}{2}, \\vec{r}} + + \\tilde{J}_{l', \\vec{r}}) \\\\ + &= \\hat{H}_{l'} \\cdot (-\\mu / \\Delta_t)(\\hat{H}_{l + \\frac{1}{2}} - \\hat{H}_{l - \\frac{1}{2}}) - + \\tilde{E}_l \\cdot (\\epsilon / \\Delta_t )(\\tilde{E}_{l'+\\frac{1}{2}} - \\tilde{E}_{l'-\\frac{1}{2}}) + - \\hat{H}_{l'} \\cdot \\hat{M}_{l-1} - \\tilde{E}_l \\cdot \\tilde{J}_{l'} \\\\ + \\end{align*} +$$ + +where in the last line the spatial subscripts have been dropped to emphasize +the time subscripts \\( l, l' \\), i.e. + +$$ + \\begin{align*} + \\tilde{E}_l &= \\tilde{E}_{l, \\vec{r}} \\\\ + \\hat{H}_l &= \\tilde{H}_{l, \\vec{r} + \\frac{1}{2}} \\\\ + \\tilde{\\epsilon} &= \\tilde{\\epsilon}_\\vec{r} \\\\ + \\end{align*} +$$ + +etc. +For \\( l' = l + \\frac{1}{2} \\) we get + +$$ + \\begin{align*} + \\hat{\\nabla} \\cdot \\tilde{S}_{l, l + \\frac{1}{2}} + &= \\hat{H}_{l + \\frac{1}{2}} \\cdot + (-\\mu / \\Delta_t)(\\hat{H}_{l + \\frac{1}{2}} - \\hat{H}_{l - \\frac{1}{2}}) - + \\tilde{E}_l \\cdot (\\epsilon / \\Delta_t)(\\tilde{E}_{l+1} - \\tilde{E}_l) + - \\hat{H}_{l'} \\cdot \\hat{M}_l - \\tilde{E}_l \\cdot \\tilde{J}_{l + \\frac{1}{2}} \\\\ + &= (-\\mu / \\Delta_t)(\\hat{H}^2_{l + \\frac{1}{2}} - \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}}) - + (\\epsilon / \\Delta_t)(\\tilde{E}_{l+1} \\cdot \\tilde{E}_l - \\tilde{E}^2_l) + - \\hat{H}_{l'} \\cdot \\hat{M}_l - \\tilde{E}_l \\cdot \\tilde{J}_{l + \\frac{1}{2}} \\\\ + &= -(\\mu \\hat{H}^2_{l + \\frac{1}{2}} + +\\epsilon \\tilde{E}_{l+1} \\cdot \\tilde{E}_l) / \\Delta_t \\ \\ + +(\\mu \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}} + +\\epsilon \\tilde{E}^2_l) / \\Delta_t \\ \\ + - \\hat{H}_{l+\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\ + - \\tilde{E}_l \\cdot \\tilde{J}_{l+\\frac{1}{2}} \\\\ + \\end{align*} +$$ + +and for \\( l' = l - \\frac{1}{2} \\), + +$$ + \\begin{align*} + \\hat{\\nabla} \\cdot \\tilde{S}_{l, l - \\frac{1}{2}} + &= (\\mu \\hat{H}^2_{l - \\frac{1}{2}} + +\\epsilon \\tilde{E}_{l-1} \\cdot \\tilde{E}_l) / \\Delta_t \\ \\ + -(\\mu \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}} + +\\epsilon \\tilde{E}^2_l) / \\Delta_t \\ \\ + - \\hat{H}_{l-\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\ + - \\tilde{E}_l \\cdot \\tilde{J}_{l-\\frac{1}{2}} \\\\ + \\end{align*} +$$ + +These two results form the discrete time-domain analogue to Poynting's theorem. +They hint at the expressions for the energy, which can be calculated at the same +time-index as either the E or H field: + +$$ + \\begin{align*} + U_l &= \\epsilon \\tilde{E}^2_l + \\mu \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}} \\\\ + U_{l + \\frac{1}{2}} &= \\epsilon \\tilde{E}_l \\cdot \\tilde{E}_{l + 1} + \\mu \\hat{H}^2_{l + \\frac{1}{2}} \\\\ + \\end{align*} +$$ + +Rewriting the Poynting theorem in terms of the energy expressions, + +$$ + \\begin{align*} + (U_{l+\\frac{1}{2}} - U_l) / \\Delta_t + &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l + \\frac{1}{2}} \\ \\ + - \\hat{H}_{l+\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\ + - \\tilde{E}_l \\cdot \\tilde{J}_{l+\\frac{1}{2}} \\\\ + (U_l - U_{l-\\frac{1}{2}}) / \\Delta_t + &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l - \\frac{1}{2}} \\ \\ + - \\hat{H}_{l-\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\ + - \\tilde{E}_l \\cdot \\tilde{J}_{l-\\frac{1}{2}} \\\\ + \\end{align*} +$$ + +This result is exact an should practically hold to within numerical precision. No time- +or spatial-averaging is necessary. + +Note that each value of \\( J \\) contributes to the energy twice (i.e. once per field update) +despite only causing the value of \\( E \\) to change once (same for \\( M \\) and \\( H \\)). -Energy conservation -=================== -# TODO Boundary conditions =================== diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py index fd21642..e16a53f 100644 --- a/meanas/fdtd/base.py +++ b/meanas/fdtd/base.py @@ -73,9 +73,9 @@ def maxwell_h(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t: The full update should be - H -= (curl_forward(E[t]) - M) / mu + H -= (curl_forward(E[t]) + M) / mu - which requires an additional step of `H += M / mu` which is not performed + which requires an additional step of `H -= M / mu` which is not performed by the generated function; this step can be omitted if there is no magnetic current `M`. From 5250501f3e1a305d1222cb3d2fa8e54e4ba3163f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 13 Jan 2020 01:53:24 -0800 Subject: [PATCH 222/437] add waveguide eigenproblem derivation --- meanas/fdfd/__init__.py | 8 +++ meanas/fdfd/waveguide_2d.py | 139 +++++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 1 deletion(-) diff --git a/meanas/fdfd/__init__.py b/meanas/fdfd/__init__.py index d0d58a9..c9eba8c 100644 --- a/meanas/fdfd/__init__.py +++ b/meanas/fdfd/__init__.py @@ -12,6 +12,14 @@ Submodules: - `scpml`: Stretched-coordinate perfectly matched layer (scpml) boundary conditions - `waveguide_2d`: Operators and mode-solver for waveguides with constant cross-section. - `waveguide_3d`: Functions for transforming `waveguide_2d` results into 3D. + + +=========== + +# TODO FDFD? +# TODO PML + + """ from . import solvers, operators, functional, scpml, waveguide_2d, waveguide_3d # from . import farfield, bloch TODO diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 49f33ad..6bb3ade 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -5,7 +5,144 @@ The propagation direction is chosen to be along the z axis, and all fields are given an implicit z-dependence of the form `exp(-1 * wavenumber * z)`. As the z-dependence is known, all the functions in this file assume a 2D grid - (i.e. `dxes = [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dx_h_0, ...], [dy_h_0, ...]]]`). + (i.e. `dxes = [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...]], [[dx_h[0], ...], [dy_h[0], ...]]]`). + + +=============== + +Consider Maxwell's equations in continuous space, in the frequency domain. Assuming +a structure with some (x, y) cross-section extending uniformly into the z dimension, +with a diagonal \\( \\epsilon \\) tensor, we have + +$$ +\\begin{align*} +\\nabla \\times \\vec{E}(x, y, z) &= -\\imath \\omega \\mu \\vec{H} \\\\ +\\nabla \\times \\vec{H}(x, y, z) &= \\imath \\omega \\epsilon \\vec{E} \\\\ +\\vec{E}(x,y,z) = (\\vec{E}_t(x, y) + E_z(x, y)\\vec{z}) e^{-\\gamma z} \\\\ +\\vec{H}(x,y,z) = (\\vec{H}_t(x, y) + H_z(x, y)\\vec{z}) e^{-\\gamma z} \\\\ +\\end{align*} +$$ + +Expanding the first two equations into vector components, we get + +$$ +\\begin{align*} +-\\imath \\omega \\mu_{xx} H_x &= \\partial_y E_z - \\partial_z E_y \\\\ +-\\imath \\omega \\mu_{yy} H_y &= \\partial_z E_x - \\partial_x E_z \\\\ +-\\imath \\omega \\mu_{zz} H_z &= \\partial_x E_y - \\partial_y E_x \\\\ +\\imath \\omega \\epsilon_{xx} E_x &= \\partial_y H_z - \\partial_z H_y \\\\ +\\imath \\omega \\epsilon_{yy} E_y &= \\partial_z H_x - \\partial_x H_z \\\\ +\\imath \\omega \\epsilon_{zz} E_z &= \\partial_x H_y - \\partial_y H_x \\\\ +\\end{align*} +$$ + +Substituting in our expressions for \\( \\vec{E}, \\vec{H} \\) and discretizing: + +$$ +\\begin{align*} +-\\imath \\omega \\mu_{xx} H_x &= \\tilde{\\partial}_y E_z + \\gamma E_y \\\\ +-\\imath \\omega \\mu_{yy} H_y &= -\\gamma E_x - \\tilde{\\partial}_x E_z \\\\ +-\\imath \\omega \\mu_{zz} H_z &= \\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x \\\\ +\\imath \\omega \\epsilon_{xx} E_x &= \\hat{\\partial}_y H_z + \\gamma H_y \\\\ +\\imath \\omega \\epsilon_{yy} E_y &= -\\gamma H_x - \\hat{\\partial}_x H_z \\\\ +\\imath \\omega \\epsilon_{zz} E_z &= \\hat{\\partial}_x H_y - \\hat{\\partial}_y H_x \\\\ +\\end{align*} +$$ + +Rewrite the last three equations as +$$ +\\begin{align*} +\\gamma H_y &= \\imath \\omega \\epsilon_{xx} E_x - \\hat{\\partial}_y H_z \\\\ +\\gamma H_x &= -\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z \\\\ +\\imath \\omega E_z &= \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x H_y - \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y H_x \\\\ +\\end{align*} +$$ + +Now apply \\( \\gamma \\tilde{\\partial}_x \\) to the last equation, +then substitute in for \\( \\gamma H_x \\) and \\( \\gamma H_y \\): + +$$ +\\begin{align*} +\\gamma \\tilde{\\partial}_x \\imath \\omega E_z &= \\gamma \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x H_y + - \\gamma \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y H_x \\\\ + &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x ( \\imath \\omega \\epsilon{xx} E_x - \\hat{\\partial}_y H_z) + - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (-\\imath \\omega \\epsilon{yy} E_y - \\hat{\\partial}_x H_z) \\\\ + &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x ( \\imath \\omega \\epsilon{xx} E_x) + - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (-\\imath \\omega \\epsilon{yy} E_y) \\\\ +\\gamma \\tilde{\\partial}_x E_z &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\ +\\end{align*} +$$ + +With a similar approach (but using \\( \\tilde{\\partial}_y \\) instead), we can get + +$$ +\\begin{align*} +\\gamma \\tilde{\\partial}_y E_z &= \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\ +\\end{align*} +$$ + +We can combine this equation for \\( \\gamma \\tilde{\\partial}_y E_z \\) with +the unused \\( \\imath \\omega \\mu_{xx} H_z \\) and \\( \\imath \\omega \\mu_{yy} H_y \\) equations to get + +$$ +\\begin{align*} +-\\imath \\omega \\mu_{xx} \\gamma H_x &= \\gamma^2 E_y + \\tilde{\\partial}_y ( + \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + + \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\ + ) \\\\ +-\\imath \\omega \\mu_{yy} \\gamma H_y &= -\\gamma^2 E_x - \\tilde{\\partial}_x ( + \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + + \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) + )\\\\ +\\end{align*} +$$ + +However, based on our rewritten equation for \\( \\gamma H_x \\) and the so-far unused +equation for \\( \\imath \\omega \\mu_{zz} H_z \\) we can also write + +$$ +\\begin{align*} +-\\imath \\omega \\mu_{xx} (\\gamma H_x) &= -\\imath \\omega \\mu_{xx} (-\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z) \\\\ + &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y + -\\imath \\omega \\mu_{xx} \\hat{\\partial}_x ( + \\frac{1}{-\\imath \\omega \\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x)) \\\\ + &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y + +\\mu_{xx} \\hat{\\partial}_x \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\ +\\end{align*} +$$ + +and, similarly, + +$$ +\\begin{align*} +-\\imath \\omega \\mu_{yy} (\\gamma H_y) &= -\\omega^2 \\mu_{yy} \\epsilon_{xx} E_x + +\\mu_{yy} \\hat{\\partial}_y \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\ +\\end{align*} +$$ + +Using these, we can construct the eigenvalue problem +$$ \\beta^2 \\begin{bmatrix} E_x \\\\ + E_y \\end{bmatrix} = + (\\omega^2 \\begin{bmatrix} \\mu_{yy} \\epsilon_{xx} & 0 \\\\ + 0 & \\mu_{xx} \\epsilon_{yy} \\end{bmatrix} + + \\begin{bmatrix} -\\mu_{yy} \\hat{\\partial}_y \\\\ + \\mu_{xx} \\hat{\\partial}_x \\end{bmatrix} \\mu_{zz}^{-1} + \\begin{bmatrix} -\\tilde{\\partial}_y & \\tilde{\\partial}_x \\end{bmatrix} + + \\begin{bmatrix} \\tilde{\\partial}_x \\\\ + \\tilde{\\partial}_y \\end{bmatrix} \\epsilon_{zz}^{-1} + \\begin{bmatrix} \\hat{\\partial}_x \\epsilon_{xx} & \\hat{\\partial}_y \\epsilon_{yy} \\end{bmatrix}) + \\begin{bmatrix} E_x \\\\ + E_y \\end{bmatrix} +$$ + +An equivalent eigenvalue problem can be formed using the \\( H_x, H_y \\) fields, if those are more convenient. + +Note that \\( E_z \\) was never discretized, so \\( \\gamma \\) and \\( \\beta \\) will need adjustment +to account for numerical dispersion if the result is introduced into a space with a discretized z-axis. + + """ # TODO update module docs From bae1155c5922ec507a30a6760cf23101a5abbba0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 13 Jan 2020 23:24:20 -0800 Subject: [PATCH 223/437] update waveguide operators to new format --- meanas/fdfd/waveguide_2d.py | 66 ++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 6bb3ade..856c267 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -74,7 +74,7 @@ $$ \\end{align*} $$ -With a similar approach (but using \\( \\tilde{\\partial}_y \\) instead), we can get +With a similar approach (but using \\( \\gamma \\tilde{\\partial}_y \\) instead), we can get $$ \\begin{align*} @@ -90,7 +90,7 @@ $$ \\begin{align*} -\\imath \\omega \\mu_{xx} \\gamma H_x &= \\gamma^2 E_y + \\tilde{\\partial}_y ( \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) - + \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\ + + \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) ) \\\\ -\\imath \\omega \\mu_{yy} \\gamma H_y &= -\\gamma^2 E_x - \\tilde{\\partial}_x ( \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) @@ -175,26 +175,21 @@ def operator_e(omega: complex, for use with a field vector of the form `cat([E_x, E_y])`. More precisely, the operator is - $$ \\omega^2 \\mu_{yx} \\epsilon_{xy} + - \\mu_{yx} \\begin{bmatrix} -D_{by} \\\\ - D_{bx} \\end{bmatrix} \\mu_z^{-1} - \\begin{bmatrix} -D_{fy} & D_{fx} \\end{bmatrix} + - \\begin{bmatrix} D_{fx} \\\\ - D_{fy} \\end{bmatrix} \\epsilon_z^{-1} - \\begin{bmatrix} D_{bx} & D_{by} \\end{bmatrix} \\epsilon_{xy} $$ - where - \\( \\epsilon_{xy} = \\begin{bmatrix} - \\epsilon_x & 0 \\\\ - 0 & \\epsilon_y - \\end{bmatrix} \\), - \\( \\mu_{yx} = \\begin{bmatrix} - \\mu_y & 0 \\\\ - 0 & \\mu_x - \\end{bmatrix} \\), - \\( D_{fx} \\) and \\( D_{bx} \\) are the forward and backward derivatives along x, - and each \\( \\epsilon_x, \\mu_y, \\) etc. is a diagonal matrix representing + $$ + \\omega^2 \\begin{bmatrix} \\mu_{yy} \\epsilon_{xx} & 0 \\\\ + 0 & \\mu_{xx} \\epsilon_{yy} \\end{bmatrix} + + \\begin{bmatrix} -\\mu_{yy} \\hat{\\partial}_y \\\\ + \\mu_{xx} \\hat{\\partial}_x \\end{bmatrix} \\mu_{zz}^{-1} + \\begin{bmatrix} -\\tilde{\\partial}_y & \\tilde{\\partial}_x \\end{bmatrix} + + \\begin{bmatrix} \\tilde{\\partial}_x \\\\ + \\tilde{\\partial}_y \\end{bmatrix} \\epsilon_{zz}^{-1} + \\begin{bmatrix} \\hat{\\partial}_x \\epsilon_{xx} & \\hat{\\partial}_y \\epsilon_{yy} \\end{bmatrix} + $$ + \\( \\tilde{\\parital}_x} \\) and \\( \\hat{\\partial}_x \\) are the forward and backward derivatives along x, + and each \\( \\epsilon_{xx}, \\mu_{yy}, \\) etc. is a diagonal matrix containing the vectorized material + property distribution. This operator can be used to form an eigenvalue problem of the form `operator_e(...) @ [E_x, E_y] = wavenumber**2 * [E_x, E_y]` @@ -246,26 +241,21 @@ def operator_h(omega: complex, for use with a field vector of the form `cat([H_x, H_y])`. More precisely, the operator is - $$ \\omega^2 \\epsilon_{yx} \\mu_{xy} + - \\epsilon_{yx} \\begin{bmatrix} -D_{fy} \\\\ - D_{fx} \\end{bmatrix} \\epsilon_z^{-1} - \\begin{bmatrix} -D_{by} & D_{bx} \\end{bmatrix} + - \\begin{bmatrix} D_{bx} \\\\ - D_{by} \\end{bmatrix} \\mu_z^{-1} - \\begin{bmatrix} D_{fx} & D_{fy} \\end{bmatrix} \\mu_{xy} $$ - where - \\( \\epsilon_{yx} = \\begin{bmatrix} - \\epsilon_y & 0 \\\\ - 0 & \\epsilon_x - \\end{bmatrix} \\), - \\( \\mu_{xy} = \\begin{bmatrix} - \\mu_x & 0 \\\\ - 0 & \\mu_y - \\end{bmatrix} \\), - \\( D_{fx} \\) and \\( D_{bx} \\) are the forward and backward derivatives along x, - and each \\( \\epsilon_x, \\mu_y, \\) etc. is a diagonal matrix. + $$ + \\omega^2 \\begin{bmatrix} \\epsilon_{yy} \\mu_{xx} & 0 \\\\ + 0 & \\epsilon_{xx} \\mu_{yy} \\end{bmatrix} + + \\begin{bmatrix} -\\epsilon_{yy} \\tilde{\\partial}_y \\\\ + \\epsilon_{xx} \\tilde{\\partial}_x \\end{bmatrix} \\epsilon_{zz}^{-1} + \\begin{bmatrix} -\\hat{\\partial}_y & \\hat{\\partial}_x \\end{bmatrix} + + \\begin{bmatrix} \\hat{\\partial}_x \\\\ + \\hat{\\partial}_y \\end{bmatrix} \\mu_{zz}^{-1} + \\begin{bmatrix} \\tilde{\\partial}_x \\mu_{xx} & \\tilde{\\partial}_y \\mu_{yy} \\end{bmatrix} + $$ + \\( \\tilde{\\parital}_x} \\) and \\( \\hat{\\partial}_x \\) are the forward and backward derivatives along x, + and each \\( \\epsilon_{xx}, \\mu_{yy}, \\) etc. is a diagonal matrix containing the vectorized material + property distribution. This operator can be used to form an eigenvalue problem of the form `operator_h(...) @ [H_x, H_y] = wavenumber**2 * [H_x, H_y]` From eb586ea8b7e904f79673123ad54cbde2712faaaf Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 8 Feb 2020 17:43:51 -0800 Subject: [PATCH 224/437] fix parital -> partial --- meanas/fdfd/waveguide_2d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 856c267..abd9935 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -187,7 +187,7 @@ def operator_e(omega: complex, \\begin{bmatrix} \\hat{\\partial}_x \\epsilon_{xx} & \\hat{\\partial}_y \\epsilon_{yy} \\end{bmatrix} $$ - \\( \\tilde{\\parital}_x} \\) and \\( \\hat{\\partial}_x \\) are the forward and backward derivatives along x, + \\( \\tilde{\\partial}_x \\) and \\( \\hat{\\partial}_x \\) are the forward and backward derivatives along x, and each \\( \\epsilon_{xx}, \\mu_{yy}, \\) etc. is a diagonal matrix containing the vectorized material property distribution. @@ -253,7 +253,7 @@ def operator_h(omega: complex, \\begin{bmatrix} \\tilde{\\partial}_x \\mu_{xx} & \\tilde{\\partial}_y \\mu_{yy} \\end{bmatrix} $$ - \\( \\tilde{\\parital}_x} \\) and \\( \\hat{\\partial}_x \\) are the forward and backward derivatives along x, + \\( \\tilde{\\partial}_x \\) and \\( \\hat{\\partial}_x \\) are the forward and backward derivatives along x, and each \\( \\epsilon_{xx}, \\mu_{yy}, \\) etc. is a diagonal matrix containing the vectorized material property distribution. From 69301474607d87b9b6da829a6ad477d615eaf32f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 8 Feb 2020 17:44:03 -0800 Subject: [PATCH 225/437] fix index --- meanas/fdmath/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index cb47c79..7e4d6ca 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -273,7 +273,7 @@ For example, consider the forward curl, at (m, n, p), of a back-vector field `g` [figure: z-component of curl] : | z y : ^^ | - |/_x :....||.<.....| (m, n+1, p+1/2) + |/_x :....||.<.....| (m+1, n+1, p+1/2) / || / | v || | ^ |/ |/ From 26db5e757af37d0a726e07eff7f696faf7983ddd Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 8 Feb 2020 17:44:13 -0800 Subject: [PATCH 226/437] add note about sources --- meanas/fdtd/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py index 3d498a9..2a99f76 100644 --- a/meanas/fdtd/__init__.py +++ b/meanas/fdtd/__init__.py @@ -137,6 +137,23 @@ Note that each value of \\( J \\) contributes to the energy twice (i.e. once per despite only causing the value of \\( E \\) to change once (same for \\( M \\) and \\( H \\)). +Sources +============= + +It is often useful to excite the simulation with an arbitrary broadband pulse and then +extract the frequency-domain response by performing an on-the-fly Fourier transform +of the time-domain fields. + +The Ricker wavelet (normalized second derivative of a Gaussian) is commonly used for the pulse +shape. It can be written + +$$ f_r(t) = (1 - \\frac{1}{2} (\\omega (t - \\tau))^2) e^{-(\\frac{\\omega (t - \\tau)}{2})^2} $$ + +with \\( \\tau > \\frac{2 * \\pi}{\\omega} \\) as a minimum delay to avoid a discontinuity at +t=0 (assuming the source is off for t<0 this gives \\( \\sim 10^{-3} \\) error at t=0). + + + Boundary conditions =================== # TODO notes about boundaries / PMLs From 7d8901539c21d969f9ee2a188f09e254cf0eb8a1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 8 Feb 2020 17:44:28 -0800 Subject: [PATCH 227/437] Use big Omega --- meanas/fdfd/operators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index b90ec67..3d6374a 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -47,12 +47,12 @@ def e_full(omega: complex, ) -> sparse.spmatrix: """ Wave operator - $$ \\nabla \\times (\\frac{1}{\\mu} \\nabla \\times) - \\omega^2 \\epsilon $$ + $$ \\nabla \\times (\\frac{1}{\\mu} \\nabla \\times) - \\Omega^2 \\epsilon $$ del x (1/mu * del x) - omega**2 * epsilon for use with the E-field, with wave equation - $$ (\\nabla \\times (\\frac{1}{\\mu} \\nabla \\times) - \\omega^2 \\epsilon) E = -\\imath \\omega J $$ + $$ (\\nabla \\times (\\frac{1}{\\mu} \\nabla \\times) - \\Omega^2 \\epsilon) E = -\\imath \\omega J $$ (del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J From 6e3cc1c3bd595eed2ed52135bd88e9361f0177d4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 19 Feb 2020 18:42:06 -0800 Subject: [PATCH 228/437] more doc updates --- .gitignore | 3 + make_docs.sh | 14 +- meanas/fdfd/__init__.py | 15 +- meanas/fdfd/functional.py | 4 +- meanas/fdfd/waveguide_2d.py | 74 +++---- meanas/fdmath/__init__.py | 153 ++++++++------- meanas/fdmath/functional.py | 4 +- meanas/fdtd/__init__.py | 52 ++--- pdoc_templates/config.mako | 5 +- pdoc_templates/html.mako | 38 +++- pdoc_templates/pdf.mako | 185 +++++++++++++++++ pdoc_templates/pdoc.css | 381 ++++++++++++++++++++++++++++++++++++ 12 files changed, 774 insertions(+), 154 deletions(-) create mode 100644 pdoc_templates/pdf.mako create mode 100644 pdoc_templates/pdoc.css diff --git a/.gitignore b/.gitignore index 1d95edb..48f9cd7 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ target/ .*.sw[op] + +*.svg +*.html diff --git a/make_docs.sh b/make_docs.sh index f2f2fe2..a29558c 100755 --- a/make_docs.sh +++ b/make_docs.sh @@ -1,3 +1,15 @@ #!/bin/bash cd ~/projects/meanas -pdoc3 --html --force --template-dir pdoc_templates -o doc . + +# Approach 1: pdf to html? +#pdoc3 --pdf --force --template-dir pdoc_templates -o doc . | \ +# pandoc --metadata=title:"meanas" --toc --toc-depth=4 --from=markdown+abbreviations --to=html --output=doc.html --gladtex -s - + +# Approach 2: pdf to html with gladtex +pdoc3 --pdf --force --template-dir pdoc_templates -o doc . > doc.md +pandoc --metadata=title:"meanas" --from=markdown+abbreviations --to=html --output=doc.html --gladtex -s --css pdoc_templates/pdoc.css doc.md +gladtex -a -n -d _doc_mathimg -c white doc.html + +# Approach 3: html with gladtex +#pdoc3 --html --force --template-dir pdoc_templates -o doc . +#find doc -iname '*.html' -exec gladtex -a -n -d _mathimg -c white {} \; diff --git a/meanas/fdfd/__init__.py b/meanas/fdfd/__init__.py index c9eba8c..3e9463e 100644 --- a/meanas/fdfd/__init__.py +++ b/meanas/fdfd/__init__.py @@ -14,7 +14,20 @@ Submodules: - `waveguide_3d`: Functions for transforming `waveguide_2d` results into 3D. -=========== +================================================================ + +From the "Frequency domain" section of `meanas.fdmath`, we have + +$$ + \\begin{aligned} + \\tilde{E}_{l, \\vec{r}} &= \\tilde{E}_{\\vec{r}} e^{-\\imath \\omega l \\Delta_t} \\\\ + \\tilde{J}_{l, \\vec{r}} &= \\tilde{J}_{\\vec{r}} e^{-\\imath \\omega (l - \\frac{1}{2}) \\Delta_t} \\\\ + \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}}) + -\\Omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} &= \\imath \\Omega \\tilde{J}_{\\vec{r}} \\\\ + \\Omega &= 2 \\sin(\\omega \\Delta_t / 2) / \\Delta_t + \\end{aligned} +$$ + # TODO FDFD? # TODO PML diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index d995e7b..33ed134 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -185,8 +185,8 @@ def e_tfsf_source(TF_region: fdfield_t, def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[fdfield_t, fdfield_t], fdfield_t]: """ Generates a function that takes the single-frequency `E` and `H` fields - and calculates the cross product `E` x `H` = \\( E \\times H \\) as required - for the Poynting vector, \\( S = E \\times H \\) + and calculates the cross product `E` x `H` = $E \\times H$ as required + for the Poynting vector, $S = E \\times H$ Note: This function also shifts the input `E` field by one cell as required diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index abd9935..4113f8d 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -12,82 +12,82 @@ As the z-dependence is known, all the functions in this file assume a 2D grid Consider Maxwell's equations in continuous space, in the frequency domain. Assuming a structure with some (x, y) cross-section extending uniformly into the z dimension, -with a diagonal \\( \\epsilon \\) tensor, we have +with a diagonal $\\epsilon$ tensor, we have $$ -\\begin{align*} +\\begin{aligned} \\nabla \\times \\vec{E}(x, y, z) &= -\\imath \\omega \\mu \\vec{H} \\\\ \\nabla \\times \\vec{H}(x, y, z) &= \\imath \\omega \\epsilon \\vec{E} \\\\ \\vec{E}(x,y,z) = (\\vec{E}_t(x, y) + E_z(x, y)\\vec{z}) e^{-\\gamma z} \\\\ \\vec{H}(x,y,z) = (\\vec{H}_t(x, y) + H_z(x, y)\\vec{z}) e^{-\\gamma z} \\\\ -\\end{align*} +\\end{aligned} $$ Expanding the first two equations into vector components, we get $$ -\\begin{align*} +\\begin{aligned} -\\imath \\omega \\mu_{xx} H_x &= \\partial_y E_z - \\partial_z E_y \\\\ -\\imath \\omega \\mu_{yy} H_y &= \\partial_z E_x - \\partial_x E_z \\\\ -\\imath \\omega \\mu_{zz} H_z &= \\partial_x E_y - \\partial_y E_x \\\\ \\imath \\omega \\epsilon_{xx} E_x &= \\partial_y H_z - \\partial_z H_y \\\\ \\imath \\omega \\epsilon_{yy} E_y &= \\partial_z H_x - \\partial_x H_z \\\\ \\imath \\omega \\epsilon_{zz} E_z &= \\partial_x H_y - \\partial_y H_x \\\\ -\\end{align*} +\\end{aligned} $$ -Substituting in our expressions for \\( \\vec{E}, \\vec{H} \\) and discretizing: +Substituting in our expressions for $\\vec{E}$, $\\vec{H}$ and discretizing: $$ -\\begin{align*} +\\begin{aligned} -\\imath \\omega \\mu_{xx} H_x &= \\tilde{\\partial}_y E_z + \\gamma E_y \\\\ -\\imath \\omega \\mu_{yy} H_y &= -\\gamma E_x - \\tilde{\\partial}_x E_z \\\\ -\\imath \\omega \\mu_{zz} H_z &= \\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x \\\\ \\imath \\omega \\epsilon_{xx} E_x &= \\hat{\\partial}_y H_z + \\gamma H_y \\\\ \\imath \\omega \\epsilon_{yy} E_y &= -\\gamma H_x - \\hat{\\partial}_x H_z \\\\ \\imath \\omega \\epsilon_{zz} E_z &= \\hat{\\partial}_x H_y - \\hat{\\partial}_y H_x \\\\ -\\end{align*} +\\end{aligned} $$ Rewrite the last three equations as $$ -\\begin{align*} +\\begin{aligned} \\gamma H_y &= \\imath \\omega \\epsilon_{xx} E_x - \\hat{\\partial}_y H_z \\\\ \\gamma H_x &= -\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z \\\\ \\imath \\omega E_z &= \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x H_y - \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y H_x \\\\ -\\end{align*} +\\end{aligned} $$ -Now apply \\( \\gamma \\tilde{\\partial}_x \\) to the last equation, -then substitute in for \\( \\gamma H_x \\) and \\( \\gamma H_y \\): +Now apply $\\gamma \\tilde{\\partial}_x$ to the last equation, +then substitute in for $\\gamma H_x$ and $\\gamma H_y$: $$ -\\begin{align*} +\\begin{aligned} \\gamma \\tilde{\\partial}_x \\imath \\omega E_z &= \\gamma \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x H_y - \\gamma \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y H_x \\\\ - &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x ( \\imath \\omega \\epsilon{xx} E_x - \\hat{\\partial}_y H_z) - - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (-\\imath \\omega \\epsilon{yy} E_y - \\hat{\\partial}_x H_z) \\\\ - &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x ( \\imath \\omega \\epsilon{xx} E_x) - - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (-\\imath \\omega \\epsilon{yy} E_y) \\\\ + &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x ( \\imath \\omega \\epsilon_{xx} E_x - \\hat{\\partial}_y H_z) + - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (-\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z) \\\\ + &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x ( \\imath \\omega \\epsilon_{xx} E_x) + - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (-\\imath \\omega \\epsilon_{yy} E_y) \\\\ \\gamma \\tilde{\\partial}_x E_z &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\ -\\end{align*} +\\end{aligned} $$ -With a similar approach (but using \\( \\gamma \\tilde{\\partial}_y \\) instead), we can get +With a similar approach (but using $\\gamma \\tilde{\\partial}_y$ instead), we can get $$ -\\begin{align*} +\\begin{aligned} \\gamma \\tilde{\\partial}_y E_z &= \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\ -\\end{align*} +\\end{aligned} $$ -We can combine this equation for \\( \\gamma \\tilde{\\partial}_y E_z \\) with -the unused \\( \\imath \\omega \\mu_{xx} H_z \\) and \\( \\imath \\omega \\mu_{yy} H_y \\) equations to get +We can combine this equation for $\\gamma \\tilde{\\partial}_y E_z$ with +the unused $\\imath \\omega \\mu_{xx} H_z$ and $\\imath \\omega \\mu_{yy} H_y$ equations to get $$ -\\begin{align*} +\\begin{aligned} -\\imath \\omega \\mu_{xx} \\gamma H_x &= \\gamma^2 E_y + \\tilde{\\partial}_y ( \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) @@ -96,30 +96,30 @@ $$ \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) )\\\\ -\\end{align*} +\\end{aligned} $$ -However, based on our rewritten equation for \\( \\gamma H_x \\) and the so-far unused -equation for \\( \\imath \\omega \\mu_{zz} H_z \\) we can also write +However, based on our rewritten equation for $\\gamma H_x$ and the so-far unused +equation for $\\imath \\omega \\mu_{zz} H_z$ we can also write $$ -\\begin{align*} +\\begin{aligned} -\\imath \\omega \\mu_{xx} (\\gamma H_x) &= -\\imath \\omega \\mu_{xx} (-\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z) \\\\ &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y -\\imath \\omega \\mu_{xx} \\hat{\\partial}_x ( \\frac{1}{-\\imath \\omega \\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x)) \\\\ &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y +\\mu_{xx} \\hat{\\partial}_x \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\ -\\end{align*} +\\end{aligned} $$ and, similarly, $$ -\\begin{align*} +\\begin{aligned} -\\imath \\omega \\mu_{yy} (\\gamma H_y) &= -\\omega^2 \\mu_{yy} \\epsilon_{xx} E_x +\\mu_{yy} \\hat{\\partial}_y \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\ -\\end{align*} +\\end{aligned} $$ Using these, we can construct the eigenvalue problem @@ -137,9 +137,9 @@ $$ \\beta^2 \\begin{bmatrix} E_x \\\\ E_y \\end{bmatrix} $$ -An equivalent eigenvalue problem can be formed using the \\( H_x, H_y \\) fields, if those are more convenient. +An equivalent eigenvalue problem can be formed using the $H_x$ and $H_y$ fields, if those are more convenient. -Note that \\( E_z \\) was never discretized, so \\( \\gamma \\) and \\( \\beta \\) will need adjustment +Note that $E_z$ was never discretized, so $\\gamma$ and $\\beta$ will need adjustment to account for numerical dispersion if the result is introduced into a space with a discretized z-axis. @@ -187,8 +187,8 @@ def operator_e(omega: complex, \\begin{bmatrix} \\hat{\\partial}_x \\epsilon_{xx} & \\hat{\\partial}_y \\epsilon_{yy} \\end{bmatrix} $$ - \\( \\tilde{\\partial}_x \\) and \\( \\hat{\\partial}_x \\) are the forward and backward derivatives along x, - and each \\( \\epsilon_{xx}, \\mu_{yy}, \\) etc. is a diagonal matrix containing the vectorized material + $\\tilde{\\partial}_x$ and $\\hat{\\partial}_x$ are the forward and backward derivatives along x, + and each $\\epsilon_{xx}$, $\\mu_{yy}$, etc. is a diagonal matrix containing the vectorized material property distribution. This operator can be used to form an eigenvalue problem of the form @@ -253,8 +253,8 @@ def operator_h(omega: complex, \\begin{bmatrix} \\tilde{\\partial}_x \\mu_{xx} & \\tilde{\\partial}_y \\mu_{yy} \\end{bmatrix} $$ - \\( \\tilde{\\partial}_x \\) and \\( \\hat{\\partial}_x \\) are the forward and backward derivatives along x, - and each \\( \\epsilon_{xx}, \\mu_{yy}, \\) etc. is a diagonal matrix containing the vectorized material + $\\tilde{\\partial}_x$ and $\\hat{\\partial}_x$ are the forward and backward derivatives along x, + and each $\\epsilon_{xx}$, $\\mu_{yy}$, etc. is a diagonal matrix containing the vectorized material property distribution. This operator can be used to form an eigenvalue problem of the form diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index 7e4d6ca..a3b1af3 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -43,11 +43,11 @@ Scalar derivatives and cell shifts ---------------------------------- Define the discrete forward derivative as - $$ [\\tilde{\\partial}_x f ]_{m + \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m + 1} - f_m) $$ - where \\( f \\) is a function defined at discrete locations on the x-axis (labeled using \\( m \\)). - The value at \\( m \\) occupies a length \\( \\Delta_{x, m} \\) along the x-axis. Note that \\( m \\) + $$ [\\tilde{\\partial}_x f]_{m + \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m + 1} - f_m) $$ + where $f$ is a function defined at discrete locations on the x-axis (labeled using $m$). + The value at $m$ occupies a length $\\Delta_{x, m}$ along the x-axis. Note that $m$ is an index along the x-axis, _not_ necessarily an x-coordinate, since each length - \\( \\Delta_{x, m}, \\Delta_{x, m+1}, ...\\) is independently chosen. + $\\Delta_{x, m}, \\Delta_{x, m+1}, ...$ is independently chosen. If we treat `f` as a 1D array of values, with the `i`-th value `f[i]` taking up a length `dx[i]` along the x-axis, the forward derivative is @@ -62,7 +62,7 @@ Likewise, discrete reverse derivative is deriv_back(f)[i] = (f[i] - f[i - 1]) / dx[i] The derivatives' values are shifted by a half-cell relative to the original function, and -will have different cell widths if all the `dx[i]` ( \\( \\Delta_{x, m} \\) ) are not +will have different cell widths if all the `dx[i]` ( $\\Delta_{x, m}$ ) are not identical: [figure: derivatives and cell sizes] @@ -87,19 +87,20 @@ identical: Periodic boundaries are used here and elsewhere unless otherwise noted. In the above figure, - `f0 =` \\(f_0\\), `f1 =` \\(f_1\\) - `Df0 =` \\([\\tilde{\\partial}f]_{0 + \\frac{1}{2}}\\) - `Df1 =` \\([\\tilde{\\partial}f]_{1 + \\frac{1}{2}}\\) - `df0 =` \\([\\hat{\\partial}f]_{0 - \\frac{1}{2}}\\) + `f0 =` $f_0$, `f1 =` $f_1$ + `Df0 =` $[\\tilde{\\partial}f]_{0 + \\frac{1}{2}}$ + `Df1 =` $[\\tilde{\\partial}f]_{1 + \\frac{1}{2}}$ + `df0 =` $[\\hat{\\partial}f]_{0 - \\frac{1}{2}}$ etc. -The fractional subscript \\( m + \\frac{1}{2} \\) is used to indicate values defined - at shifted locations relative to the original \\( m \\), with corresponding lengths +The fractional subscript $m + \\frac{1}{2}$ is used to indicate values defined + at shifted locations relative to the original $m$, with corresponding lengths $$ \\Delta_{x, m + \\frac{1}{2}} = \\frac{1}{2} * (\\Delta_{x, m} + \\Delta_{x, m + 1}) $$ -Just as \\( m \\) is not itself an x-coordinate, neither is \\( m + \\frac{1}{2} \\); + +Just as $m$ is not itself an x-coordinate, neither is $m + \\frac{1}{2}$; carefully note the positions of the various cells in the above figure vs their labels. -If the positions labeled with \\( m \\) are considered the "base" or "original" grid, -the positions labeled with \\( m + \\frac{1}{2} \\) are said to lie on a "dual" or +If the positions labeled with $m$ are considered the "base" or "original" grid, +the positions labeled with $m + \\frac{1}{2}$ are said to lie on a "dual" or "derived" grid. For the remainder of the `Discrete calculus` section, all figures will show @@ -201,8 +202,8 @@ There are also two divergences, where `g = [gx, gy, gz]` is a fore- or back-vector field. Since we applied the forward divergence to the back-vector (and vice-versa), the resulting scalar value -is defined at the back-vector's (fore-vectors) location \\( (m,n,p) \\) and not at the locations of its components -\\( (m \\pm \\frac{1}{2},n,p) \\) etc. +is defined at the back-vector's (fore-vector's) location $(m,n,p)$ and not at the locations of its components +$(m \\pm \\frac{1}{2},n,p)$ etc. [figure: divergence] ^^ @@ -226,23 +227,23 @@ Curls The two curls are then - $$ \\begin{align*} + $$ \\begin{aligned} \\hat{h}_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &= \\\\ [\\tilde{\\nabla} \\times \\tilde{g}]_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &= \\vec{x} (\\tilde{\\partial}_y g^z_{m,n,p + \\frac{1}{2}} - \\tilde{\\partial}_z g^y_{m,n + \\frac{1}{2},p}) \\\\ &+ \\vec{y} (\\tilde{\\partial}_z g^x_{m + \\frac{1}{2},n,p} - \\tilde{\\partial}_x g^z_{m,n,p + \\frac{1}{2}}) \\\\ &+ \\vec{z} (\\tilde{\\partial}_x g^y_{m,n + \\frac{1}{2},p} - \\tilde{\\partial}_y g^z_{m + \\frac{1}{2},n,p}) - \\end{align*} $$ + \\end{aligned} $$ and $$ \\tilde{h}_{m - \\frac{1}{2}, n - \\frac{1}{2}, p - \\frac{1}{2}} = [\\hat{\\nabla} \\times \\hat{g}]_{m - \\frac{1}{2}, n - \\frac{1}{2}, p - \\frac{1}{2}} $$ - where \\( \\hat{g} \\) and \\( \\tilde{g} \\) are located at \\((m,n,p)\\) - with components at \\( (m \\pm \\frac{1}{2},n,p) \\) etc., - while \\( \\hat{h} \\) and \\( \\tilde{h} \\) are located at \\((m \\pm \\frac{1}{2}, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) - with components at \\((m, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})\\) etc. + where $\\hat{g}$ and $\\tilde{g}$ are located at $(m,n,p)$ + with components at $(m \\pm \\frac{1}{2},n,p)$ etc., + while $\\hat{h}$ and $\\tilde{h}$ are located at $(m \\pm \\frac{1}{2}, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})$ + with components at $(m, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})$ etc. [code: curls] @@ -286,27 +287,27 @@ Maxwell's Equations If we discretize both space (m,n,p) and time (l), Maxwell's equations become - $$ \\begin{align*} + $$ \\begin{aligned} \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &= -\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} - \\hat{M}_{l, \\vec{r} + \\frac{1}{2}} \\\\ \\hat{\\nabla} \\times \\hat{H}_{l-\\frac{1}{2},\\vec{r} + \\frac{1}{2}} &= \\hat{\\partial}_t \\tilde{D}_{l, \\vec{r}} + \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\ \\tilde{\\nabla} \\cdot \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &= 0 \\\\ \\hat{\\nabla} \\cdot \\tilde{D}_{l,\\vec{r}} &= \\rho_{l,\\vec{r}} - \\end{align*} $$ + \\end{aligned} $$ with - $$ \\begin{align*} - \\hat{B}_\\vec{r} &= \\mu_{\\vec{r} + \\frac{1}{2}} \\cdot \\hat{H}_{\\vec{r} + \\frac{1}{2}} \\\\ - \\tilde{D}_\\vec{r} &= \\epsilon_\\vec{r} \\cdot \\tilde{E}_\\vec{r} - \\end{align*} $$ + $$ \\begin{aligned} + \\hat{B}_{\\vec{r}} &= \\mu_{\\vec{r} + \\frac{1}{2}} \\cdot \\hat{H}_{\\vec{r} + \\frac{1}{2}} \\\\ + \\tilde{D}_{\\vec{r}} &= \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} + \\end{aligned} $$ -where the spatial subscripts are abbreviated as \\( \\vec{r} = (m, n, p) \\) and -\\( \\vec{r} + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}) \\), -\\( \\tilde{E} \\) and \\( \\hat{H} \\) are the electric and magnetic fields, -\\( \\tilde{J} \\) and \\( \\hat{M} \\) are the electric and magnetic current distributions, -and \\( \\epsilon \\) and \\( \\mu \\) are the dielectric permittivity and magnetic permeability. +where the spatial subscripts are abbreviated as $\\vec{r} = (m, n, p)$ and +$\\vec{r} + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2})$, +$\\tilde{E}$ and $\\hat{H}$ are the electric and magnetic fields, +$\\tilde{J}$ and $\\hat{M}$ are the electric and magnetic current distributions, +and $\\epsilon$ and $\\mu$ are the dielectric permittivity and magnetic permeability. The above is Yee's algorithm, written in a form analogous to Maxwell's equations. The time derivatives can be expanded to form the update equations: @@ -375,12 +376,12 @@ and combining them with charge continuity, Wave equation ------------- -Taking the backward curl of the \\( \\tilde{\\nabla} \\times \\tilde{E} \\) equation and -replacing the resulting \\( \\hat{\\nabla} \\times \\hat{H} \\) term using its respective equation, -and setting \\( \\hat{M} \\) to zero, we can form the discrete wave equation: +Taking the backward curl of the $\\tilde{\\nabla} \\times \\tilde{E}$ equation and +replacing the resulting $\\hat{\\nabla} \\times \\hat{H}$ term using its respective equation, +and setting $\\hat{M}$ to zero, we can form the discrete wave equation: $$ - \\begin{align*} + \\begin{aligned} \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &= -\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} - \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}} \\\\ @@ -391,11 +392,11 @@ $$ \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &= -\\tilde{\\partial}_t \\hat{\\nabla} \\times \\hat{H}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} \\\\ \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &= - -\\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_\\vec{r} \\tilde{E}_{l, \\vec{r}} + \\hat{\\partial}_t \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\ + -\\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_{\\vec{r}} \\tilde{E}_{l, \\vec{r}} + \\hat{\\partial}_t \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\ \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) - + \\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_\\vec{r} \\cdot \\tilde{E}_{l, \\vec{r}} + + \\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{l, \\vec{r}} &= \\tilde{\\partial}_t \\tilde{J}_{l - \\frac{1}{2}, \\vec{r}} - \\end{align*} + \\end{aligned} $$ @@ -405,27 +406,27 @@ Frequency domain We can substitute in a time-harmonic fields $$ - \\begin{align*} - \\tilde{E}_\\vec{r} &= \\tilde{E}_\\vec{r} e^{-\\imath \\omega l \\Delta_t} \\\\ - \\tilde{J}_\\vec{r} &= \\tilde{J}_\\vec{r} e^{-\\imath \\omega (l - \\frac{1}{2}) \\Delta_t} - \\end{align*} + \\begin{aligned} + \\tilde{E}_{l, \\vec{r}} &= \\tilde{E}_{\\vec{r}} e^{-\\imath \\omega l \\Delta_t} \\\\ + \\tilde{J}_{l, \\vec{r}} &= \\tilde{J}_{\\vec{r}} e^{-\\imath \\omega (l - \\frac{1}{2}) \\Delta_t} + \\end{aligned} $$ resulting in $$ - \\begin{align*} + \\begin{aligned} \\tilde{\\partial}_t &\\Rightarrow (e^{ \\imath \\omega \\Delta_t} - 1) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_t} \\sin(\\omega \\Delta_t / 2) e^{-\\imath \\omega \\Delta_t / 2} = -\\imath \\Omega e^{-\\imath \\omega \\Delta_t / 2}\\\\ \\hat{\\partial}_t &\\Rightarrow (1 - e^{-\\imath \\omega \\Delta_t}) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_t} \\sin(\\omega \\Delta_t / 2) e^{ \\imath \\omega \\Delta_t / 2} = -\\imath \\Omega e^{ \\imath \\omega \\Delta_t / 2}\\\\ \\Omega &= 2 \\sin(\\omega \\Delta_t / 2) / \\Delta_t - \\end{align*} + \\end{aligned} $$ This gives the frequency-domain wave equation, $$ - \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_\\vec{r}) - -\\Omega^2 \\epsilon_\\vec{r} \\cdot \\tilde{E}_\\vec{r} = \\imath \\Omega \\tilde{J}_\\vec{r} + \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}}) + -\\Omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} = \\imath \\Omega \\tilde{J}_{\\vec{r}} $$ @@ -435,69 +436,69 @@ Plane waves and Dispersion relation With uniform material distribution and no sources $$ - \\begin{align*} + \\begin{aligned} \\mu_{\\vec{r} + \\frac{1}{2}} &= \\mu \\\\ - \\epsilon_\\vec{r} &= \\epsilon \\\\ - \\tilde{J}_\\vec{r} &= 0 \\\\ - \\end{align*} + \\epsilon_{\\vec{r}} &= \\epsilon \\\\ + \\tilde{J}_{\\vec{r}} &= 0 \\\\ + \\end{aligned} $$ the frequency domain wave equation simplifies to -$$ \\hat{\\nabla} \\times \\tilde{\\nabla} \\times \\tilde{E}_\\vec{r} - \\Omega^2 \\epsilon \\mu \\tilde{E}_\\vec{r} = 0 $$ +$$ \\hat{\\nabla} \\times \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}} - \\Omega^2 \\epsilon \\mu \\tilde{E}_{\\vec{r}} = 0 $$ -Since \\( \\hat{\\nabla} \\cdot \\tilde{E}_\\vec{r} = 0 \\), we can simplify +Since $\\hat{\\nabla} \\cdot \\tilde{E}_{\\vec{r}} = 0$, we can simplify $$ - \\begin{align*} - \\hat{\\nabla} \\times \\tilde{\\nabla} \\times \\tilde{E}_\\vec{r} - &= \\tilde{\\nabla}(\\hat{\\nabla} \\cdot \\tilde{E}_\\vec{r}) - \\hat{\\nabla} \\cdot \\tilde{\\nabla} \\tilde{E}_\\vec{r} \\\\ - &= - \\hat{\\nabla} \\cdot \\tilde{\\nabla} \\tilde{E}_\\vec{r} \\\\ - &= - \\tilde{\\nabla}^2 \\tilde{E}_\\vec{r} - \\end{align*} + \\begin{aligned} + \\hat{\\nabla} \\times \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}} + &= \\tilde{\\nabla}(\\hat{\\nabla} \\cdot \\tilde{E}_{\\vec{r}}) - \\hat{\\nabla} \\cdot \\tilde{\\nabla} \\tilde{E}_{\\vec{r}} \\\\ + &= - \\hat{\\nabla} \\cdot \\tilde{\\nabla} \\tilde{E}_{\\vec{r}} \\\\ + &= - \\tilde{\\nabla}^2 \\tilde{E}_{\\vec{r}} + \\end{aligned} $$ and we get -$$ \\tilde{\\nabla}^2 \\tilde{E}_\\vec{r} + \\Omega^2 \\epsilon \\mu \\tilde{E}_\\vec{r} = 0 $$ +$$ \\tilde{\\nabla}^2 \\tilde{E}_{\\vec{r}} + \\Omega^2 \\epsilon \\mu \\tilde{E}_{\\vec{r}} = 0 $$ We can convert this to three scalar-wave equations of the form -$$ (\\tilde{\\nabla}^2 + K^2) \\phi_\\vec{r} = 0 $$ +$$ (\\tilde{\\nabla}^2 + K^2) \\phi_{\\vec{r}} = 0 $$ -with \\( K^2 = \\Omega^2 \\mu \\epsilon \\). Now we let +with $K^2 = \\Omega^2 \\mu \\epsilon$. Now we let -$$ \\phi_\\vec{r} = A e^{\\imath (k_x m \\Delta_x + k_y n \\Delta_y + k_z p \\Delta_z)} $$ +$$ \\phi_{\\vec{r}} = A e^{\\imath (k_x m \\Delta_x + k_y n \\Delta_y + k_z p \\Delta_z)} $$ resulting in $$ - \\begin{align*} + \\begin{aligned} \\tilde{\\partial}_x &\\Rightarrow (e^{ \\imath k_x \\Delta_x} - 1) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_x} \\sin(k_x \\Delta_x / 2) e^{ \\imath k_x \\Delta_x / 2} = \\imath K_x e^{ \\imath k_x \\Delta_x / 2}\\\\ \\hat{\\partial}_x &\\Rightarrow (1 - e^{-\\imath k_x \\Delta_x}) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_x} \\sin(k_x \\Delta_x / 2) e^{-\\imath k_x \\Delta_x / 2} = \\imath K_x e^{-\\imath k_x \\Delta_x / 2}\\\\ K_x &= 2 \\sin(k_x \\Delta_x / 2) / \\Delta_x \\\\ - \\end{align*} + \\end{aligned} $$ -with similar expressions for the y and z dimnsions (and \\( K_y, K_z \\)). +with similar expressions for the y and z dimnsions (and $K_y, K_z$). This implies $$ - \\tilde{\\nabla}^2 = -(K_x^2 + K_y^2 + K_z^2) \\phi_\\vec{r} \\\\ + \\tilde{\\nabla}^2 = -(K_x^2 + K_y^2 + K_z^2) \\phi_{\\vec{r}} \\\\ K_x^2 + K_y^2 + K_z^2 = \\Omega^2 \\mu \\epsilon = \\Omega^2 / c^2 $$ -where \\( c = \\sqrt{\\mu \\epsilon} \\). +where $c = \\sqrt{\\mu \\epsilon}$. -Assuming real \\( (k_x, k_y, k_z), \\omega \\) will be real only if +Assuming real $(k_x, k_y, k_z), \\omega$ will be real only if $$ c^2 \\Delta_t^2 = \\frac{\\Delta_t^2}{\\mu \\epsilon} < 1/(\\frac{1}{\\Delta_x^2} + \\frac{1}{\\Delta_y^2} + \\frac{1}{\\Delta_z^2}) $$ -If \\( \\Delta_x = \\Delta_y = \\Delta_z \\), this simplifies to \\( c \\Delta_t < \\Delta_x / \\sqrt{3} \\). +If $\\Delta_x = \\Delta_y = \\Delta_z$, this simplifies to $c \\Delta_t < \\Delta_x / \\sqrt{3}$. This last form can be interpreted as enforcing causality; the distance that light -travels in one timestep (i.e., \\( c \\Delta_t \\)) must be less than the diagonal -of the smallest cell ( \\( \\Delta_x / \\sqrt{3} \\) when on a uniform cubic grid). +travels in one timestep (i.e., $c \\Delta_t$) must be less than the diagonal +of the smallest cell ( $\\Delta_x / \\sqrt{3}$ when on a uniform cubic grid). Grid description @@ -513,9 +514,9 @@ To get a better sense of how this works, let's start by drawing a grid with unif to make the illustration simpler; we need at least two cells in the x dimension to demonstrate how nonuniform `dx` affects the various components. -Place the E fore-vectors at integer indices \\( r = (m, n, p) \\) and the H back-vectors -at fractional indices \\( r + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, -p + \\frac{1}{2}) \\). Remember that these are indices and not coordinates; they can +Place the E fore-vectors at integer indices $r = (m, n, p)$ and the H back-vectors +at fractional indices $r + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, +p + \\frac{1}{2})$. Remember that these are indices and not coordinates; they can correspond to arbitrary (monotonically increasing) coordinates depending on the cell widths. Draw lines to denote the planes on which the H components and back-vectors are defined. diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py index fc5b0ca..7de046a 100644 --- a/meanas/fdmath/functional.py +++ b/meanas/fdmath/functional.py @@ -63,7 +63,7 @@ def curl_forward(dx_e: List[numpy.ndarray] = None) -> fdfield_updater_t: Returns: Function `f` for taking the discrete forward curl of a field, - `f(E)` -> curlE \\( = \\nabla_f \\times E \\) + `f(E)` -> curlE $= \\nabla_f \\times E$ """ Dx, Dy, Dz = deriv_forward(dx_e) @@ -90,7 +90,7 @@ def curl_back(dx_h: List[numpy.ndarray] = None) -> fdfield_updater_t: Returns: Function `f` for taking the discrete backward curl of a field, - `f(H)` -> curlH \\( = \\nabla_b \\times H \\) + `f(H)` -> curlH $= \\nabla_b \\times H$ """ Dx, Dy, Dz = deriv_back(dx_h) diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py index 2a99f76..64656b7 100644 --- a/meanas/fdtd/__init__.py +++ b/meanas/fdtd/__init__.py @@ -13,7 +13,7 @@ we have $$ c^2 \\Delta_t^2 = \\frac{\\Delta_t^2}{\\mu \\epsilon} < 1/(\\frac{1}{\\Delta_x^2} + \\frac{1}{\\Delta_y^2} + \\frac{1}{\\Delta_z^2}) $$ -or, if \\( \\Delta_x = \\Delta_y = \\Delta_z \\), then \\( c \\Delta_t < \\frac{\\Delta_x}{\\sqrt{3}} \\). +or, if $\\Delta_x = \\Delta_y = \\Delta_z$, then $c \\Delta_t < \\frac{\\Delta_x}{\\sqrt{3}}$. Based on this, we can set @@ -27,21 +27,21 @@ Poynting Vector and Energy Conservation Let -$$ \\begin{align*} +$$ \\begin{aligned} \\tilde{S}_{l, l', \\vec{r}} &=& &\\tilde{E}_{l, \\vec{r}} \\otimes \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\\\ &=& &\\vec{x} (\\tilde{E}^y_{l,m+1,n,p} \\hat{H}^z_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^z_{l,m+1,n,p} \\hat{H}^y_{l', \\vec{r} + \\frac{1}{2}}) \\\\ & &+ &\\vec{y} (\\tilde{E}^z_{l,m,n+1,p} \\hat{H}^x_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^x_{l,m,n+1,p} \\hat{H}^z_{l', \\vec{r} + \\frac{1}{2}}) \\\\ & &+ &\\vec{z} (\\tilde{E}^x_{l,m,n,p+1} \\hat{H}^y_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^y_{l,m,n,p+1} \\hat{H}^z_{l', \\vec{r} + \\frac{1}{2}}) - \\end{align*} + \\end{aligned} $$ -where \\( \\vec{r} = (m, n, p) \\) and \\( \\otimes \\) is a modified cross product -in which the \\( \\tilde{E} \\) terms are shifted as indicated. +where $\\vec{r} = (m, n, p)$ and $\\otimes$ is a modified cross product +in which the $\\tilde{E}$ terms are shifted as indicated. By taking the divergence and rearranging terms, we can show that $$ - \\begin{align*} + \\begin{aligned} \\hat{\\nabla} \\cdot \\tilde{S}_{l, l', \\vec{r}} &= \\hat{\\nabla} \\cdot (\\tilde{E}_{l, \\vec{r}} \\otimes \\hat{H}_{l', \\vec{r} + \\frac{1}{2}}) \\\\ &= \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l, \\vec{r}} - @@ -49,30 +49,30 @@ $$ &= \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\cdot (-\\tilde{\\partial}_t \\mu_{\\vec{r} + \\frac{1}{2}} \\hat{H}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} - \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}}) - - \\tilde{E}_{l, \\vec{r}} \\cdot (\\hat{\\partial}_t \\tilde{\\epsilon}_\\vec{r} \\tilde{E}_{l'+\\frac{1}{2}, \\vec{r}} + + \\tilde{E}_{l, \\vec{r}} \\cdot (\\hat{\\partial}_t \\tilde{\\epsilon}_{\\vec{r}} \\tilde{E}_{l'+\\frac{1}{2}, \\vec{r}} + \\tilde{J}_{l', \\vec{r}}) \\\\ &= \\hat{H}_{l'} \\cdot (-\\mu / \\Delta_t)(\\hat{H}_{l + \\frac{1}{2}} - \\hat{H}_{l - \\frac{1}{2}}) - \\tilde{E}_l \\cdot (\\epsilon / \\Delta_t )(\\tilde{E}_{l'+\\frac{1}{2}} - \\tilde{E}_{l'-\\frac{1}{2}}) - \\hat{H}_{l'} \\cdot \\hat{M}_{l-1} - \\tilde{E}_l \\cdot \\tilde{J}_{l'} \\\\ - \\end{align*} + \\end{aligned} $$ where in the last line the spatial subscripts have been dropped to emphasize -the time subscripts \\( l, l' \\), i.e. +the time subscripts $l, l'$, i.e. $$ - \\begin{align*} + \\begin{aligned} \\tilde{E}_l &= \\tilde{E}_{l, \\vec{r}} \\\\ \\hat{H}_l &= \\tilde{H}_{l, \\vec{r} + \\frac{1}{2}} \\\\ - \\tilde{\\epsilon} &= \\tilde{\\epsilon}_\\vec{r} \\\\ - \\end{align*} + \\tilde{\\epsilon} &= \\tilde{\\epsilon}_{\\vec{r}} \\\\ + \\end{aligned} $$ etc. -For \\( l' = l + \\frac{1}{2} \\) we get +For $l' = l + \\frac{1}{2}$ we get $$ - \\begin{align*} + \\begin{aligned} \\hat{\\nabla} \\cdot \\tilde{S}_{l, l + \\frac{1}{2}} &= \\hat{H}_{l + \\frac{1}{2}} \\cdot (-\\mu / \\Delta_t)(\\hat{H}_{l + \\frac{1}{2}} - \\hat{H}_{l - \\frac{1}{2}}) - @@ -87,13 +87,13 @@ $$ +\\epsilon \\tilde{E}^2_l) / \\Delta_t \\ \\ - \\hat{H}_{l+\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\ - \\tilde{E}_l \\cdot \\tilde{J}_{l+\\frac{1}{2}} \\\\ - \\end{align*} + \\end{aligned} $$ -and for \\( l' = l - \\frac{1}{2} \\), +and for $l' = l - \\frac{1}{2}$, $$ - \\begin{align*} + \\begin{aligned} \\hat{\\nabla} \\cdot \\tilde{S}_{l, l - \\frac{1}{2}} &= (\\mu \\hat{H}^2_{l - \\frac{1}{2}} +\\epsilon \\tilde{E}_{l-1} \\cdot \\tilde{E}_l) / \\Delta_t \\ \\ @@ -101,7 +101,7 @@ $$ +\\epsilon \\tilde{E}^2_l) / \\Delta_t \\ \\ - \\hat{H}_{l-\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\ - \\tilde{E}_l \\cdot \\tilde{J}_{l-\\frac{1}{2}} \\\\ - \\end{align*} + \\end{aligned} $$ These two results form the discrete time-domain analogue to Poynting's theorem. @@ -109,16 +109,16 @@ They hint at the expressions for the energy, which can be calculated at the same time-index as either the E or H field: $$ - \\begin{align*} + \\begin{aligned} U_l &= \\epsilon \\tilde{E}^2_l + \\mu \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}} \\\\ U_{l + \\frac{1}{2}} &= \\epsilon \\tilde{E}_l \\cdot \\tilde{E}_{l + 1} + \\mu \\hat{H}^2_{l + \\frac{1}{2}} \\\\ - \\end{align*} + \\end{aligned} $$ Rewriting the Poynting theorem in terms of the energy expressions, $$ - \\begin{align*} + \\begin{aligned} (U_{l+\\frac{1}{2}} - U_l) / \\Delta_t &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l + \\frac{1}{2}} \\ \\ - \\hat{H}_{l+\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\ @@ -127,14 +127,14 @@ $$ &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l - \\frac{1}{2}} \\ \\ - \\hat{H}_{l-\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\ - \\tilde{E}_l \\cdot \\tilde{J}_{l-\\frac{1}{2}} \\\\ - \\end{align*} + \\end{aligned} $$ This result is exact an should practically hold to within numerical precision. No time- or spatial-averaging is necessary. -Note that each value of \\( J \\) contributes to the energy twice (i.e. once per field update) -despite only causing the value of \\( E \\) to change once (same for \\( M \\) and \\( H \\)). +Note that each value of $J$ contributes to the energy twice (i.e. once per field update) +despite only causing the value of $E$ to change once (same for $M$ and $H$). Sources @@ -149,8 +149,8 @@ shape. It can be written $$ f_r(t) = (1 - \\frac{1}{2} (\\omega (t - \\tau))^2) e^{-(\\frac{\\omega (t - \\tau)}{2})^2} $$ -with \\( \\tau > \\frac{2 * \\pi}{\\omega} \\) as a minimum delay to avoid a discontinuity at -t=0 (assuming the source is off for t<0 this gives \\( \\sim 10^{-3} \\) error at t=0). +with $\\tau > \\frac{2 * \\pi}{\\omega}$ as a minimum delay to avoid a discontinuity at +t=0 (assuming the source is off for t<0 this gives $\\sim 10^{-3}$ error at t=0). diff --git a/pdoc_templates/config.mako b/pdoc_templates/config.mako index 0642566..3010348 100644 --- a/pdoc_templates/config.mako +++ b/pdoc_templates/config.mako @@ -18,8 +18,9 @@ #git_link_template = 'https://github.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' #git_link_template = 'https://gitlab.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' #git_link_template = 'https://bitbucket.org/USER/PROJECT/src/{commit}/{path}#lines-{start_line}:{end_line}' - #git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start-line}' - git_link_template = None + #git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start_line}' + #git_link_template = None + git_link_template = 'https://mpxd.net/code/jan/fdfd_tools/src/commit/{commit}/{path}#L{start_line}-L{end_line}' # A prefix to use for every HTML hyperlink in the generated documentation. # No prefix results in all links being relative. diff --git a/pdoc_templates/html.mako b/pdoc_templates/html.mako index 9cf1137..6b3326f 100644 --- a/pdoc_templates/html.mako +++ b/pdoc_templates/html.mako @@ -2,7 +2,10 @@ import os import pdoc - from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link + from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link, _md, to_markdown + + from markdown.inlinepatterns import InlineProcessor + from markdown.util import AtomicString, etree def link(d, name=None, fmt='{}'): @@ -14,8 +17,33 @@ return '{}'.format(d.refname, url, name) - def to_html(text): - return _to_html(text, module=module, link=link, latex_math=latex_math) + # Altered latex delimeters (allow inline $...$, wrap in ) + class _MathPattern(InlineProcessor): + NAME = 'pdoc-math' + PATTERN = r'(? <%def name="ident(name)">${name} @@ -377,10 +405,6 @@ % endif - % if latex_math: - - % endif - <%include file="head.mako"/> diff --git a/pdoc_templates/pdf.mako b/pdoc_templates/pdf.mako new file mode 100644 index 0000000..50e7989 --- /dev/null +++ b/pdoc_templates/pdf.mako @@ -0,0 +1,185 @@ +<%! + import re + import pdoc + from pdoc.html_helpers import to_markdown, format_git_link + + def link(d, fmt='{}'): + name = fmt.format(d.qualname + ('()' if isinstance(d, pdoc.Function) else '')) + if isinstance(d, pdoc.External): + return name + return '[{}](#{})'.format(name, d.refname) + + def _to_md(text, module): + text = to_markdown(text, module=module, link=link) + # Setext H2 headings to atx H2 headings + text = re.sub(r'\n(.+)\n-{3,}\n', r'\n## \1\n\n', text) + # Convert admonitions into simpler paragraphs, dedent contents + text = re.sub(r'^(?P( *))!!! \w+ \"([^\"]*)\"(.*(?:\n(?P=indent) +.*)*)', + lambda m: '{}**{}:** {}'.format(m.group(2), m.group(3), + re.sub('\n {,4}', '\n', m.group(4))), + text, flags=re.MULTILINE) + return text + + def subh(text, level=2): + # Deepen heading levels so H2 becomes H4 etc. + return re.sub(r'\n(#+) +(.+)\n', r'\n%s\1 \2\n' % ('#' * level), text) +%> + +<%def name="title(level, string, id=None)"> + <% id = ' {#%s}' % id if id is not None else '' %> +${('#' * level) + ' ' + string + id} + + +<%def name="funcdef(f)"> + <% + returns = show_type_annotations and f.return_annotation() or '' + if returns: + returns = ' -> ' + returns + %> +> `${f.funcdef()} ${f.name}(${', '.join(f.params(annotate=show_type_annotations))})${returns}` + + +<%def name="classdef(c)"> +> `class ${c.name}(${', '.join(c.params(annotate=show_type_annotations))})` + + +<%def name="show_source(d)"> + % if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None): + <% git_link = format_git_link(git_link_template, d) %> +[[view code]](${git_link}) + %endif + + +--- +description: | + API documentation for modules: ${', '.join(m.name for m in modules)}. + +lang: en + +classoption: oneside +geometry: margin=1in +papersize: a4 + +linkcolor: blue +links-as-notes: true +... +% for module in modules: +<% + submodules = module.submodules() + variables = module.variables() + functions = module.functions() + classes = module.classes() + + def to_md(text): + return _to_md(text, module) +%> + +------------------------------------------- + +${title(1, ('Namespace' if module.is_namespace else 'Module') + ' `%s`' % module.name, module.refname)} +${module.docstring | to_md} + +% if submodules: +${title(2, 'Sub-modules')} + % for m in submodules: +* [${m.name}](#${m.refname}) + % endfor +% endif + +% if variables: +${title(2, 'Variables')} + % for v in variables: +${title(3, 'Variable `%s`' % v.name, v.refname)} +${show_source(v)} +${v.docstring | to_md, subh, subh} + % endfor +% endif + +% if functions: +${title(2, 'Functions')} + % for f in functions: +${title(3, 'Function `%s`' % f.name, f.refname)} +${show_source(f)} + +${funcdef(f)} + +${f.docstring | to_md, subh, subh} + % endfor +% endif + +% if classes: +${title(2, 'Classes')} + % for cls in classes: +${title(3, 'Class `%s`' % cls.name, cls.refname)} +${show_source(cls)} + +${classdef(cls)} + +${cls.docstring | to_md, subh} +<% + class_vars = cls.class_variables(show_inherited_members, sort=sort_identifiers) + static_methods = cls.functions(show_inherited_members, sort=sort_identifiers) + inst_vars = cls.instance_variables(show_inherited_members, sort=sort_identifiers) + methods = cls.methods(show_inherited_members, sort=sort_identifiers) + mro = cls.mro() + subclasses = cls.subclasses() +%> + % if mro: +${title(4, 'Ancestors (in MRO)')} + % for c in mro: +* [${c.refname}](#${c.refname}) + % endfor + % endif + + % if subclasses: +${title(4, 'Descendants')} + % for c in subclasses: +* [${c.refname}](#${c.refname}) + % endfor + % endif + + % if class_vars: +${title(4, 'Class variables')} + % for v in class_vars: +${title(5, 'Variable `%s`' % v.name, v.refname)} +${v.docstring | to_md, subh, subh} + % endfor + % endif + + % if inst_vars: +${title(4, 'Instance variables')} + % for v in inst_vars: +${title(5, 'Variable `%s`' % v.name, v.refname)} +${v.docstring | to_md, subh, subh} + % endfor + % endif + + % if static_methods: +${title(4, 'Static methods')} + % for f in static_methods: +${title(5, '`Method %s`' % f.name, f.refname)} + +${funcdef(f)} + +${f.docstring | to_md, subh, subh} + % endfor + % endif + + % if methods: +${title(4, 'Methods')} + % for f in methods: +${title(5, 'Method `%s`' % f.name, f.refname)} + +${funcdef(f)} + +${f.docstring | to_md, subh, subh} + % endfor + % endif + % endfor +% endif + +##\## for module in modules: +% endfor + +----- +Generated by *pdoc* ${pdoc.__version__} (). diff --git a/pdoc_templates/pdoc.css b/pdoc_templates/pdoc.css new file mode 100644 index 0000000..a563b44 --- /dev/null +++ b/pdoc_templates/pdoc.css @@ -0,0 +1,381 @@ + .flex { + display: flex !important; + } + + body { + line-height: 1.5em; + background: black; + color: #DDD; + max-width: 140ch; + } + + #content { + padding: 20px; + } + + #sidebar { + padding: 30px; + overflow: hidden; + } + + .http-server-breadcrumbs { + font-size: 130%; + margin: 0 0 15px 0; + } + + #footer { + font-size: .75em; + padding: 5px 30px; + border-top: 1px solid #ddd; + text-align: right; + } + #footer p { + margin: 0 0 0 1em; + display: inline-block; + } + #footer p:last-child { + margin-right: 30px; + } + + h1, h2, h3, h4, h5 { + font-weight: 300; + } + h1 { + font-size: 2.5em; + line-height: 1.1em; + border-top: 20px white; + } + h2 { + font-size: 1.75em; + margin: 1em 0 .50em 0; + } + h3 { + font-size: 1.4em; + margin: 25px 0 10px 0; + } + h4 { + margin: 0; + font-size: 105%; + } + + a { + color: #999; + text-decoration: none; + transition: color .3s ease-in-out; + } + a:hover { + color: #18d; + } + + .title code { + font-weight: bold; + } + h2[id^="header-"] { + margin-top: 2em; + } + .ident { + color: #7ff; + } + + pre code { + background: transparent; + font-size: .8em; + line-height: 1.4em; + } + code { + background: #0d0d0e; + padding: 1px 4px; + overflow-wrap: break-word; + } + h1 code { background: transparent } + + pre { + background: #111; + border: 0; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + margin: 1em 0; + padding: 1ex; + } + + #http-server-module-list { + display: flex; + flex-flow: column; + } + #http-server-module-list div { + display: flex; + } + #http-server-module-list dt { + min-width: 10%; + } + #http-server-module-list p { + margin-top: 0; + } + + .toc ul, + #index { + list-style-type: none; + margin: 0; + padding: 0; + } + #index code { + background: transparent; + } + #index h3 { + border-bottom: 1px solid #ddd; + } + #index ul { + padding: 0; + } + #index h4 { + font-weight: bold; + } + #index h4 + ul { + margin-bottom:.6em; + } + /* Make TOC lists have 2+ columns when viewport is wide enough. + Assuming ~20-character identifiers and ~30% wide sidebar. */ + @media (min-width: 200ex) { #index .two-column { column-count: 2 } } + @media (min-width: 300ex) { #index .two-column { column-count: 3 } } + + dl { + margin-bottom: 2em; + } + dl dl:last-child { + margin-bottom: 4em; + } + dd { + margin: 0 0 1em 3em; + } + #header-classes + dl > dd { + margin-bottom: 3em; + } + dd dd { + margin-left: 2em; + } + dd p { + margin: 10px 0; + } + blockquote code { + background: #111; + font-weight: bold; + font-size: .85em; + padding: 5px 10px; + display: inline-block; + min-width: 40%; + } + blockquote code:hover { + background: #101010; + } + .name > span:first-child { + white-space: nowrap; + } + .name.class > span:nth-child(2) { + margin-left: .4em; + } + .inherited { + color: #777; + border-left: 5px solid #eee; + padding-left: 1em; + } + .inheritance em { + font-style: normal; + font-weight: bold; + } + + /* Docstrings titles, e.g. in numpydoc format */ + .desc h2 { + font-weight: 400; + font-size: 1.25em; + } + .desc h3 { + font-size: 1em; + } + .desc dt code { + background: inherit; /* Don't grey-back parameters */ + } + + .source summary, + .git-link-div { + color: #aaa; + text-align: right; + font-weight: 400; + font-size: .8em; + text-transform: uppercase; + } + .source summary > * { + white-space: nowrap; + cursor: pointer; + } + .git-link { + color: inherit; + margin-left: 1em; + } + .source pre { + max-height: 500px; + overflow: auto; + margin: 0; + } + .source pre code { + font-size: 12px; + overflow: visible; + } + .hlist { + list-style: none; + } + .hlist li { + display: inline; + } + .hlist li:after { + content: ',\2002'; + } + .hlist li:last-child:after { + content: none; + } + .hlist .hlist { + display: inline; + padding-left: 1em; + } + + img { + max-width: 100%; + } + + .admonition { + padding: .1em .5em; + margin-bottom: 1em; + } + .admonition-title { + font-weight: bold; + } + .admonition.note, + .admonition.info, + .admonition.important { + background: #610; + } + .admonition.todo, + .admonition.versionadded, + .admonition.tip, + .admonition.hint { + background: #202; + } + .admonition.warning, + .admonition.versionchanged, + .admonition.deprecated { + background: #02b; + } + .admonition.error, + .admonition.danger, + .admonition.caution { + background: darkpink; + } + + @media screen and (min-width: 700px) { + #sidebar { + width: 30%; + } + #content { + width: 70%; + max-width: 100ch; + padding: 3em 4em; + border-left: 1px solid #ddd; + } + pre code { + font-size: 1em; + } + .item .name { + font-size: 1em; + } + main { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + } + .toc ul ul, + #index ul { + padding-left: 1.5em; + } + .toc > ul > li { + margin-top: .5em; + } + } + +@media print { + #sidebar h1 { + page-break-before: always; + } + .source { + display: none; + } +} +@media print { + * { + background: transparent !important; + color: #000 !important; /* Black prints faster: h5bp.com/s */ + box-shadow: none !important; + text-shadow: none !important; + } + + a[href]:after { + content: " (" attr(href) ")"; + font-size: 90%; + } + /* Internal, documentation links, recognized by having a title, + don't need the URL explicity stated. */ + a[href][title]:after { + content: none; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + /* + * Don't show links for images, or javascript/internal links + */ + + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; /* h5bp.com/t */ + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + @page { + margin: 0.5cm; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + page-break-after: avoid; + } +} From 18203f3dad68bdb00a08fec5db876ede602ac2c0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 19 Feb 2020 18:56:56 -0800 Subject: [PATCH 229/437] Update repo location --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index d127258..91bf355 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,7 @@ those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html). Your solver will need the ability to solve complex symmetric (non-Hermitian) linear systems, ideally with double precision. -- [WIP Source repository](https://mpxd.net/code/jan/fdfd_tools/src/branch/wip) -- *TODO* [Source repository](https://mpxd.net/code/jan/meanas) +- [Source repository](https://mpxd.net/code/jan/meanas) - PyPI *TBD* @@ -81,9 +80,6 @@ virtualenv -p python3.7 venv In-place development install: ```bash # Download using git -git clone --branch wip https://mpxd.net/code/jan/fdfd_tools.git meanas/ - -# NOTE: In the future this will become #git clone https://mpxd.net/code/jan/meanas.git # If you are using a virtualenv, activate it From 2fceff99306e5a74af39b7c6767705f2c77efd26 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 19 Feb 2020 19:12:31 -0800 Subject: [PATCH 230/437] bump version to v0.6 --- meanas/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/VERSION b/meanas/VERSION index 2eb3c4f..5a2a580 100644 --- a/meanas/VERSION +++ b/meanas/VERSION @@ -1 +1 @@ -0.5 +0.6 From b7d4bbbd959e873f068b9c9e4b3628a540cee582 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 11 Jun 2020 19:26:17 -0700 Subject: [PATCH 231/437] type annotation fixup --- meanas/eigensolvers.py | 10 +++---- meanas/fdfd/operators.py | 30 ++++++++++---------- meanas/fdfd/scpml.py | 20 +++++++------- meanas/fdfd/waveguide_2d.py | 28 ++++++++++--------- meanas/fdfd/waveguide_3d.py | 28 +++++++++---------- meanas/fdfd/waveguide_cyl.py | 4 +-- meanas/fdmath/__init__.py | 2 +- meanas/fdmath/functional.py | 28 ++++++++++--------- meanas/fdmath/operators.py | 20 +++++++------- meanas/fdmath/types.py | 9 ++++-- meanas/fdmath/vectorization.py | 20 +++++++++++--- meanas/fdtd/base.py | 6 ++-- meanas/fdtd/boundaries.py | 8 +++--- meanas/fdtd/energy.py | 50 +++++++++++++++++++--------------- meanas/fdtd/pml.py | 8 +++--- 15 files changed, 149 insertions(+), 122 deletions(-) diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py index 51aeb6e..67cecea 100644 --- a/meanas/eigensolvers.py +++ b/meanas/eigensolvers.py @@ -1,7 +1,7 @@ """ Solvers for eigenvalue / eigenvector problems """ -from typing import Tuple, List +from typing import Tuple, Callable, Optional, Union import numpy from numpy.linalg import norm from scipy import sparse @@ -9,7 +9,7 @@ import scipy.sparse.linalg as spalg def power_iteration(operator: sparse.spmatrix, - guess_vector: numpy.ndarray = None, + guess_vector: Optional[numpy.ndarray] = None, iterations: int = 20, ) -> Tuple[complex, numpy.ndarray]: """ @@ -36,11 +36,11 @@ def power_iteration(operator: sparse.spmatrix, return lm_eigval, v -def rayleigh_quotient_iteration(operator: sparse.spmatrix or spalg.LinearOperator, +def rayleigh_quotient_iteration(operator: Union[sparse.spmatrix, spalg.LinearOperator], guess_vector: numpy.ndarray, iterations: int = 40, tolerance: float = 1e-13, - solver = None, + solver: Optional[Callable[..., numpy.ndarray]] = None, ) -> Tuple[complex, numpy.ndarray]: """ Use Rayleigh quotient iteration to refine an eigenvector guess. @@ -83,7 +83,7 @@ def rayleigh_quotient_iteration(operator: sparse.spmatrix or spalg.LinearOperato return eigval, v -def signed_eigensolve(operator: sparse.spmatrix or spalg.LinearOperator, +def signed_eigensolve(operator: Union[sparse.spmatrix, spalg.LinearOperator], how_many: int, negative: bool = False, ) -> Tuple[numpy.ndarray, numpy.ndarray]: diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 3d6374a..212ed4c 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -27,7 +27,7 @@ The following operators are included: - Cross product matrices """ -from typing import List, Tuple +from typing import Tuple, Optional import numpy import scipy.sparse as sparse @@ -41,9 +41,9 @@ __author__ = 'Jan Petykiewicz' def e_full(omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None, - pec: vfdfield_t = None, - pmc: vfdfield_t = None, + mu: Optional[vfdfield_t] = None, + pec: Optional[vfdfield_t] = None, + pmc: Optional[vfdfield_t] = None, ) -> sparse.spmatrix: """ Wave operator @@ -125,9 +125,9 @@ def e_full_preconditioners(dxes: dx_lists_t def h_full(omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None, - pec: vfdfield_t = None, - pmc: vfdfield_t = None, + mu: Optional[vfdfield_t] = None, + pec: Optional[vfdfield_t] = None, + pmc: Optional[vfdfield_t] = None, ) -> sparse.spmatrix: """ Wave operator @@ -181,9 +181,9 @@ def h_full(omega: complex, def eh_full(omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None, - pec: vfdfield_t = None, - pmc: vfdfield_t = None + mu: Optional[vfdfield_t] = None, + pec: Optional[vfdfield_t] = None, + pmc: Optional[vfdfield_t] = None ) -> sparse.spmatrix: """ Wave operator for `[E, H]` field representation. This operator implements Maxwell's @@ -249,8 +249,8 @@ def eh_full(omega: complex, def e2h(omega: complex, dxes: dx_lists_t, - mu: vfdfield_t = None, - pmc: vfdfield_t = None, + mu: Optional[vfdfield_t] = None, + pmc: Optional[vfdfield_t] = None, ) -> sparse.spmatrix: """ Utility operator for converting the E field into the H field. @@ -280,7 +280,7 @@ def e2h(omega: complex, def m2j(omega: complex, dxes: dx_lists_t, - mu: vfdfield_t = None + mu: Optional[vfdfield_t] = None ) -> sparse.spmatrix: """ Operator for converting a magnetic current M into an electric current J. @@ -362,7 +362,7 @@ def e_tfsf_source(TF_region: vfdfield_t, omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None, + mu: Optional[vfdfield_t] = None, ) -> sparse.spmatrix: """ Operator that turns a desired E-field distribution into a @@ -392,7 +392,7 @@ def e_boundary_source(mask: vfdfield_t, omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None, + mu: Optional[vfdfield_t] = None, periodic_mask_edges: bool = False, ) -> sparse.spmatrix: """ diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py index 7e7694d..099fac2 100644 --- a/meanas/fdfd/scpml.py +++ b/meanas/fdfd/scpml.py @@ -2,10 +2,10 @@ Functions for creating stretched coordinate perfectly matched layer (PML) absorbers. """ -from typing import List, Callable +from typing import Sequence, Union, Callable, Optional import numpy -from ..fdmath import dx_lists_t +from ..fdmath import dx_lists_t, dx_lists_mut __author__ = 'Jan Petykiewicz' @@ -37,12 +37,12 @@ def prepare_s_function(ln_R: float = -16, return s_factor -def uniform_grid_scpml(shape: numpy.ndarray or List[int], - thicknesses: numpy.ndarray or List[int], +def uniform_grid_scpml(shape: Union[numpy.ndarray, Sequence[int]], + thicknesses: Union[numpy.ndarray, Sequence[int]], omega: float, epsilon_effective: float = 1.0, - s_function: s_function_t = None, - ) -> dx_lists_t: + s_function: Optional[s_function_t] = None, + ) -> dx_lists_mut: """ Create dx arrays for a uniform grid with a cell width of 1 and a pml. @@ -63,7 +63,7 @@ def uniform_grid_scpml(shape: numpy.ndarray or List[int], Default uses `prepare_s_function()` with no parameters. Returns: - Complex cell widths (dx_lists_t) as discussed in `meanas.fdmath.types`. + Complex cell widths (dx_lists_mut) as discussed in `meanas.fdmath.types`. """ if s_function is None: s_function = prepare_s_function() @@ -90,13 +90,13 @@ def uniform_grid_scpml(shape: numpy.ndarray or List[int], return [dx_a, dx_b] -def stretch_with_scpml(dxes: dx_lists_t, +def stretch_with_scpml(dxes: dx_lists_mut, axis: int, polarity: int, omega: float, epsilon_effective: float = 1.0, thickness: int = 10, - s_function: s_function_t = None, + s_function: Optional[s_function_t] = None, ) -> dx_lists_t: """ Stretch dxes to contain a stretched-coordinate PML (SCPML) in one direction along one axis. @@ -113,7 +113,7 @@ def stretch_with_scpml(dxes: dx_lists_t, of pml parameters. Default uses `prepare_s_function()` with no parameters. Returns: - Complex cell widths (dx_lists_t) as discussed in `meanas.fdmath.types`. + Complex cell widths (dx_lists_mut) as discussed in `meanas.fdmath.types`. Multiple calls to this function may be necessary if multiple absorpbing boundaries are needed. """ if s_function is None: diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 4113f8d..21bb404 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -146,7 +146,7 @@ to account for numerical dispersion if the result is introduced into a space wit """ # TODO update module docs -from typing import List, Tuple +from typing import List, Tuple, Optional import numpy from numpy.linalg import norm import scipy.sparse as sparse @@ -163,7 +163,7 @@ __author__ = 'Jan Petykiewicz' def operator_e(omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None, + mu: Optional[vfdfield_t] = None, ) -> sparse.spmatrix: """ Waveguide operator of the form @@ -229,7 +229,7 @@ def operator_e(omega: complex, def operator_h(omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None, + mu: Optional[vfdfield_t] = None, ) -> sparse.spmatrix: """ Waveguide operator of the form @@ -298,7 +298,7 @@ def normalized_fields_e(e_xy: numpy.ndarray, omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None, + mu: Optional[vfdfield_t] = None, prop_phase: float = 0, ) -> Tuple[vfdfield_t, vfdfield_t]: """ @@ -332,7 +332,7 @@ def normalized_fields_h(h_xy: numpy.ndarray, omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None, + mu: Optional[vfdfield_t] = None, prop_phase: float = 0, ) -> Tuple[vfdfield_t, vfdfield_t]: """ @@ -366,7 +366,7 @@ def _normalized_fields(e: numpy.ndarray, omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None, + mu: Optional[vfdfield_t] = None, prop_phase: float = 0, ) -> Tuple[vfdfield_t, vfdfield_t]: # TODO documentation @@ -405,7 +405,7 @@ def exy2h(wavenumber: complex, omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None + mu: Optional[vfdfield_t] = None ) -> sparse.spmatrix: """ Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, @@ -430,7 +430,7 @@ def hxy2e(wavenumber: complex, omega: complex, dxes: dx_lists_t, epsilon: vfdfield_t, - mu: vfdfield_t = None + mu: Optional[vfdfield_t] = None ) -> sparse.spmatrix: """ Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields, @@ -453,7 +453,7 @@ def hxy2e(wavenumber: complex, def hxy2h(wavenumber: complex, dxes: dx_lists_t, - mu: vfdfield_t = None + mu: Optional[vfdfield_t] = None ) -> sparse.spmatrix: """ Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields, @@ -520,7 +520,7 @@ def exy2e(wavenumber: complex, def e2h(wavenumber: complex, omega: complex, dxes: dx_lists_t, - mu: vfdfield_t = None + mu: Optional[vfdfield_t] = None ) -> sparse.spmatrix: """ Returns an operator which, when applied to a vectorized E eigenfield, produces @@ -676,7 +676,7 @@ def solve_modes(mode_numbers: List[int], epsilon: vfdfield_t, mu: vfdfield_t = None, mode_margin: int = 2, - ) -> Tuple[List[vfdfield_t], List[complex]]: + ) -> Tuple[numpy.ndarray, List[complex]]: """ Given a 2D region, attempts to solve for the eigenmode with the specified mode numbers. @@ -691,7 +691,8 @@ def solve_modes(mode_numbers: List[int], ability to find the correct mode. Default 2. Returns: - (e_xys, wavenumbers) + e_xys: list of vfdfield_t specifying fields + wavenumbers: list of wavenumbers """ ''' @@ -733,5 +734,6 @@ def solve_mode(mode_number: int, Returns: (e_xy, wavenumber) """ - e_xys, wavenumbers = solve_modes(mode_numbers=[mode_number], *args, **kwargs) + kwargs['mode_numbers'] = [mode_number] + e_xys, wavenumbers = solve_modes(*args, **kwargs) return e_xys[:, 0], wavenumbers[0] diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index 02fb7fd..847986d 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -4,7 +4,7 @@ Tools for working with waveguide modes in 3D domains. This module relies heavily on `waveguide_2d` and mostly just transforms its parameters into 2D equivalents and expands the results back into 3D. """ -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Optional, Sequence, Union import numpy import scipy.sparse as sparse @@ -17,10 +17,10 @@ def solve_mode(mode_number: int, dxes: dx_lists_t, axis: int, polarity: int, - slices: List[slice], + slices: Sequence[slice], epsilon: fdfield_t, - mu: fdfield_t = None, - ) -> Dict[str, complex or numpy.ndarray]: + mu: Optional[fdfield_t] = None, + ) -> Dict[str, Union[complex, numpy.ndarray]]: """ Given a 3D grid, selects a slice from the grid and attempts to solve for an eigenmode propagating through that slice. @@ -104,9 +104,9 @@ def compute_source(E: fdfield_t, dxes: dx_lists_t, axis: int, polarity: int, - slices: List[slice], + slices: Sequence[slice], epsilon: fdfield_t, - mu: fdfield_t = None, + mu: Optional[fdfield_t] = None, ) -> fdfield_t: """ Given an eigenmode obtained by `solve_mode`, returns the current source distribution @@ -148,7 +148,7 @@ def compute_overlap_e(E: fdfield_t, dxes: dx_lists_t, axis: int, polarity: int, - slices: List[slice], + slices: Sequence[slice], ) -> fdfield_t: # TODO DOCS """ Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the @@ -177,9 +177,9 @@ def compute_overlap_e(E: fdfield_t, start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity)) - slices2 = list(slices) - slices2[axis] = slice(start, stop) - slices2 = (slice(None), *slices2) + slices2_l = list(slices) + slices2_l[axis] = slice(start, stop) + slices2 = (slice(None), *slices2_l) Etgt = numpy.zeros_like(Ee) Etgt[slices2] = Ee[slices2] @@ -193,7 +193,7 @@ def expand_e(E: fdfield_t, dxes: dx_lists_t, axis: int, polarity: int, - slices: List[slice], + slices: Sequence[slice], ) -> fdfield_t: """ Given an eigenmode obtained by `solve_mode`, expands the E-field from the 2D @@ -226,9 +226,9 @@ def expand_e(E: fdfield_t, # Expand our slice to the entire grid using the phase factors E_expanded = numpy.zeros_like(E) - slices_exp = list(slices) - slices_exp[axis] = slice(E.shape[axis + 1]) - slices_exp = (slice(None), *slices_exp) + slices_exp_l = list(slices) + slices_exp_l[axis] = slice(E.shape[axis + 1]) + slices_exp = (slice(None), *slices_exp_l) slices_in = (slice(None), *slices) diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index 0995984..8ac55bd 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -8,7 +8,7 @@ As the z-dependence is known, all the functions in this file assume a 2D grid """ # TODO update module docs -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Union import numpy from numpy.linalg import norm import scipy.sparse as sparse @@ -85,7 +85,7 @@ def solve_mode(mode_number: int, dxes: dx_lists_t, epsilon: vfdfield_t, r0: float, - ) -> Dict[str, complex or fdfield_t]: + ) -> Dict[str, Union[complex, fdfield_t]]: """ TODO: fixup Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index a3b1af3..4b629f9 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -741,7 +741,7 @@ the true values can be multiplied back in after the simulation is complete if no normalized results are needed. """ -from .types import fdfield_t, vfdfield_t, dx_lists_t, fdfield_updater_t +from .types import fdfield_t, vfdfield_t, dx_lists_t, dx_lists_mut, fdfield_updater_t from .vectorization import vec, unvec from . import operators, functional, types, vectorization diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py index 7de046a..379c310 100644 --- a/meanas/fdmath/functional.py +++ b/meanas/fdmath/functional.py @@ -3,13 +3,14 @@ Math functions for finite difference simulations Basic discrete calculus etc. """ -from typing import List, Callable, Tuple, Dict +from typing import Sequence, Tuple, Dict, Optional import numpy from .types import fdfield_t, fdfield_updater_t -def deriv_forward(dx_e: List[numpy.ndarray] = None) -> fdfield_updater_t: +def deriv_forward(dx_e: Optional[Sequence[numpy.ndarray]] = None + ) -> Tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]: """ Utility operators for taking discretized derivatives (backward variant). @@ -21,17 +22,18 @@ def deriv_forward(dx_e: List[numpy.ndarray] = None) -> fdfield_updater_t: List of functions for taking forward derivatives along each axis. """ if dx_e: - derivs = [lambda f: (numpy.roll(f, -1, axis=0) - f) / dx_e[0][:, None, None], + derivs = (lambda f: (numpy.roll(f, -1, axis=0) - f) / dx_e[0][:, None, None], lambda f: (numpy.roll(f, -1, axis=1) - f) / dx_e[1][None, :, None], - lambda f: (numpy.roll(f, -1, axis=2) - f) / dx_e[2][None, None, :]] + lambda f: (numpy.roll(f, -1, axis=2) - f) / dx_e[2][None, None, :]) else: - derivs = [lambda f: numpy.roll(f, -1, axis=0) - f, + derivs = (lambda f: numpy.roll(f, -1, axis=0) - f, lambda f: numpy.roll(f, -1, axis=1) - f, - lambda f: numpy.roll(f, -1, axis=2) - f] + lambda f: numpy.roll(f, -1, axis=2) - f) return derivs -def deriv_back(dx_h: List[numpy.ndarray] = None) -> fdfield_updater_t: +def deriv_back(dx_h: Optional[Sequence[numpy.ndarray]] = None + ) -> Tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]: """ Utility operators for taking discretized derivatives (forward variant). @@ -43,17 +45,17 @@ def deriv_back(dx_h: List[numpy.ndarray] = None) -> fdfield_updater_t: List of functions for taking forward derivatives along each axis. """ if dx_h: - derivs = [lambda f: (f - numpy.roll(f, 1, axis=0)) / dx_h[0][:, None, None], + derivs = (lambda f: (f - numpy.roll(f, 1, axis=0)) / dx_h[0][:, None, None], lambda f: (f - numpy.roll(f, 1, axis=1)) / dx_h[1][None, :, None], - lambda f: (f - numpy.roll(f, 1, axis=2)) / dx_h[2][None, None, :]] + lambda f: (f - numpy.roll(f, 1, axis=2)) / dx_h[2][None, None, :]) else: - derivs = [lambda f: f - numpy.roll(f, 1, axis=0), + derivs = (lambda f: f - numpy.roll(f, 1, axis=0), lambda f: f - numpy.roll(f, 1, axis=1), - lambda f: f - numpy.roll(f, 1, axis=2)] + lambda f: f - numpy.roll(f, 1, axis=2)) return derivs -def curl_forward(dx_e: List[numpy.ndarray] = None) -> fdfield_updater_t: +def curl_forward(dx_e: Optional[Sequence[numpy.ndarray]] = None) -> fdfield_updater_t: """ Curl operator for use with the E field. @@ -80,7 +82,7 @@ def curl_forward(dx_e: List[numpy.ndarray] = None) -> fdfield_updater_t: return ce_fun -def curl_back(dx_h: List[numpy.ndarray] = None) -> fdfield_updater_t: +def curl_back(dx_h: Optional[Sequence[numpy.ndarray]] = None) -> fdfield_updater_t: """ Create a function which takes the backward curl of a field. diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py index 3f83c8c..30a6708 100644 --- a/meanas/fdmath/operators.py +++ b/meanas/fdmath/operators.py @@ -3,14 +3,14 @@ Matrix operators for finite difference simulations Basic discrete calculus etc. """ -from typing import List, Callable, Tuple, Dict +from typing import Sequence, List, Callable, Tuple, Dict import numpy import scipy.sparse as sparse from .types import fdfield_t, vfdfield_t -def rotation(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmatrix: +def rotation(axis: int, shape: Sequence[int], shift_distance: int=1) -> sparse.spmatrix: """ Utility operator for performing a circular shift along a specified axis by a specified number of elements. @@ -46,7 +46,7 @@ def rotation(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmat return d -def shift_with_mirror(axis: int, shape: List[int], shift_distance: int=1) -> sparse.spmatrix: +def shift_with_mirror(axis: int, shape: Sequence[int], shift_distance: int=1) -> sparse.spmatrix: """ Utility operator for performing an n-element shift along a specified axis, with mirror boundary conditions applied to the cells beyond the receding edge. @@ -87,7 +87,7 @@ def shift_with_mirror(axis: int, shape: List[int], shift_distance: int=1) -> spa return d -def deriv_forward(dx_e: List[numpy.ndarray]) -> List[sparse.spmatrix]: +def deriv_forward(dx_e: Sequence[numpy.ndarray]) -> List[sparse.spmatrix]: """ Utility operators for taking discretized derivatives (forward variant). @@ -112,7 +112,7 @@ def deriv_forward(dx_e: List[numpy.ndarray]) -> List[sparse.spmatrix]: return Ds -def deriv_back(dx_h: List[numpy.ndarray]) -> List[sparse.spmatrix]: +def deriv_back(dx_h: Sequence[numpy.ndarray]) -> List[sparse.spmatrix]: """ Utility operators for taking discretized derivatives (backward variant). @@ -137,7 +137,7 @@ def deriv_back(dx_h: List[numpy.ndarray]) -> List[sparse.spmatrix]: return Ds -def cross(B: List[sparse.spmatrix]) -> sparse.spmatrix: +def cross(B: Sequence[sparse.spmatrix]) -> sparse.spmatrix: """ Cross product operator @@ -171,7 +171,7 @@ def vec_cross(b: vfdfield_t) -> sparse.spmatrix: return cross(B) -def avg_forward(axis: int, shape: List[int]) -> sparse.spmatrix: +def avg_forward(axis: int, shape: Sequence[int]) -> sparse.spmatrix: """ Forward average operator `(x4 = (x4 + x5) / 2)` @@ -189,7 +189,7 @@ def avg_forward(axis: int, shape: List[int]) -> sparse.spmatrix: return 0.5 * (sparse.eye(n) + rotation(axis, shape)) -def avg_back(axis: int, shape: List[int]) -> sparse.spmatrix: +def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix: """ Backward average operator `(x4 = (x4 + x3) / 2)` @@ -203,7 +203,7 @@ def avg_back(axis: int, shape: List[int]) -> sparse.spmatrix: return avg_forward(axis, shape).T -def curl_forward(dx_e: List[numpy.ndarray]) -> sparse.spmatrix: +def curl_forward(dx_e: Sequence[numpy.ndarray]) -> sparse.spmatrix: """ Curl operator for use with the E field. @@ -217,7 +217,7 @@ def curl_forward(dx_e: List[numpy.ndarray]) -> sparse.spmatrix: return cross(deriv_forward(dx_e)) -def curl_back(dx_h: List[numpy.ndarray]) -> sparse.spmatrix: +def curl_back(dx_h: Sequence[numpy.ndarray]) -> sparse.spmatrix: """ Curl operator for use with the H field. diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py index ea78f8f..8215fed 100644 --- a/meanas/fdmath/types.py +++ b/meanas/fdmath/types.py @@ -2,7 +2,7 @@ Types shared across multiple submodules """ import numpy -from typing import List, Callable +from typing import Sequence, Callable, MutableSequence # Field types @@ -25,7 +25,7 @@ class vfdfield_t(numpy.ndarray): pass -dx_lists_t = List[List[numpy.ndarray]] +dx_lists_t = Sequence[Sequence[numpy.ndarray]] ''' 'dxes' datastructure which contains grid cell width information in the following format: @@ -36,6 +36,11 @@ dx_lists_t = List[List[numpy.ndarray]] and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc. ''' +dx_lists_mut = MutableSequence[MutableSequence[numpy.ndarray]] +''' + Mutable version of `dx_lists_t` +''' + fdfield_updater_t = Callable[..., fdfield_t] ''' diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py index 63d78ef..807fc5d 100644 --- a/meanas/fdmath/vectorization.py +++ b/meanas/fdmath/vectorization.py @@ -4,16 +4,20 @@ and a 1D array representation of that field `[f_x0, f_x1, f_x2,... f_y0,... f_z0 Vectorized versions of the field use row-major (ie., C-style) ordering. """ -from typing import List +from typing import Optional, TypeVar, overload, Union, List import numpy from .types import fdfield_t, vfdfield_t +@overload +def vec(f: None) -> None: + pass -__author__ = 'Jan Petykiewicz' +@overload +def vec(f: Union[fdfield_t, List[numpy.ndarray]]) -> vfdfield_t: + pass - -def vec(f: fdfield_t) -> vfdfield_t: +def vec(f: Optional[Union[fdfield_t, List[numpy.ndarray]]]) -> Optional[vfdfield_t]: """ Create a 1D ndarray from a 3D vector field which spans a 1-3D region. @@ -31,7 +35,15 @@ def vec(f: fdfield_t) -> vfdfield_t: return numpy.ravel(f, order='C') +@overload +def unvec(v: None, shape: numpy.ndarray) -> None: + pass + +@overload def unvec(v: vfdfield_t, shape: numpy.ndarray) -> fdfield_t: + pass + +def unvec(v: Optional[vfdfield_t], shape: numpy.ndarray) -> Optional[fdfield_t]: """ Perform the inverse of vec(): take a 1D ndarray and output a 3D field of form `[f_x, f_y, f_z]` where each of `f_*` is a len(shape)-dimensional diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py index e16a53f..a632ca2 100644 --- a/meanas/fdtd/base.py +++ b/meanas/fdtd/base.py @@ -3,7 +3,7 @@ Basic FDTD field updates """ -from typing import List, Callable, Tuple, Dict +from typing import List, Callable, Dict, Union import numpy from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t @@ -47,7 +47,7 @@ def maxwell_e(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t: else: curl_h_fun = curl_back() - def me_fun(e: fdfield_t, h: fdfield_t, epsilon: fdfield_t): + def me_fun(e: fdfield_t, h: fdfield_t, epsilon: Union[fdfield_t, float]) -> fdfield_t: """ Update the E-field. @@ -100,7 +100,7 @@ def maxwell_h(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t: else: curl_e_fun = curl_forward() - def mh_fun(e: fdfield_t, h: fdfield_t, mu: fdfield_t = None): + def mh_fun(e: fdfield_t, h: fdfield_t, mu: Union[fdfield_t, float, None] = None) -> fdfield_t: """ Update the H-field. diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py index 10c966e..dbc1d93 100644 --- a/meanas/fdtd/boundaries.py +++ b/meanas/fdtd/boundaries.py @@ -4,7 +4,7 @@ Boundary conditions #TODO conducting boundary documentation """ -from typing import List, Callable, Tuple, Dict +from typing import Callable, Tuple, Dict, Any, List import numpy from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t @@ -20,8 +20,8 @@ def conducting_boundary(direction: int, u, v = dirs if polarity < 0: - boundary_slice = [slice(None)] * 3 - shifted1_slice = [slice(None)] * 3 + boundary_slice = [slice(None)] * 3 # type: List[Any] + shifted1_slice = [slice(None)] * 3 # type: List[Any] boundary_slice[direction] = 0 shifted1_slice[direction] = 1 @@ -42,7 +42,7 @@ def conducting_boundary(direction: int, if polarity > 0: boundary_slice = [slice(None)] * 3 shifted1_slice = [slice(None)] * 3 - shifted2_slice = [slice(None)] * 3 + shifted2_slice = [slice(None)] * 3 # type: List[Any] boundary_slice[direction] = -1 shifted1_slice[direction] = -2 shifted2_slice[direction] = -3 diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index dc5848a..443176b 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -1,5 +1,5 @@ # pylint: disable=unsupported-assignment-operation -from typing import List, Callable, Tuple, Dict +from typing import Callable, Tuple, Dict, Optional, Union import numpy from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t @@ -8,7 +8,7 @@ from ..fdmath.functional import deriv_back, deriv_forward def poynting(e: fdfield_t, h: fdfield_t, - dxes: dx_lists_t = None, + dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: """ Calculate the poynting vector @@ -30,16 +30,19 @@ def poynting(e: fdfield_t, return s -def poynting_divergence(s: fdfield_t = None, +def poynting_divergence(s: Optional[fdfield_t] = None, *, - e: fdfield_t = None, - h: fdfield_t = None, - dxes: dx_lists_t = None, + e: Optional[fdfield_t] = None, + h: Optional[fdfield_t] = None, + dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: """ Calculate the divergence of the poynting vector """ if s is None: + assert(e is not None) + assert(h is not None) + assert(dxes is not None) s = poynting(e, h, dxes=dxes) Dx, Dy, Dz = deriv_back() @@ -50,9 +53,9 @@ def poynting_divergence(s: fdfield_t = None, def energy_hstep(e0: fdfield_t, h1: fdfield_t, e2: fdfield_t, - epsilon: fdfield_t = None, - mu: fdfield_t = None, - dxes: dx_lists_t = None, + epsilon: Optional[fdfield_t] = None, + mu: Optional[fdfield_t] = None, + dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) return u @@ -61,9 +64,9 @@ def energy_hstep(e0: fdfield_t, def energy_estep(h0: fdfield_t, e1: fdfield_t, h2: fdfield_t, - epsilon: fdfield_t = None, - mu: fdfield_t = None, - dxes: dx_lists_t = None, + epsilon: Optional[fdfield_t] = None, + mu: Optional[fdfield_t] = None, + dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) return u @@ -74,9 +77,9 @@ def delta_energy_h2e(dt: float, h1: fdfield_t, e2: fdfield_t, h3: fdfield_t, - epsilon: fdfield_t = None, - mu: fdfield_t = None, - dxes: dx_lists_t = None, + epsilon: Optional[fdfield_t] = None, + mu: Optional[fdfield_t] = None, + dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: """ This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) @@ -92,9 +95,9 @@ def delta_energy_e2h(dt: float, e1: fdfield_t, h2: fdfield_t, e3: fdfield_t, - epsilon: fdfield_t = None, - mu: fdfield_t = None, - dxes: dx_lists_t = None, + epsilon: Optional[fdfield_t] = None, + mu: Optional[fdfield_t] = None, + dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: """ This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) @@ -105,7 +108,10 @@ def delta_energy_e2h(dt: float, return du -def delta_energy_j(j0: fdfield_t, e1: fdfield_t, dxes: dx_lists_t = None) -> fdfield_t: +def delta_energy_j(j0: fdfield_t, + e1: fdfield_t, + dxes: Optional[dx_lists_t] = None, + ) -> fdfield_t: if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) @@ -118,9 +124,9 @@ def delta_energy_j(j0: fdfield_t, e1: fdfield_t, dxes: dx_lists_t = None) -> fdf def dxmul(ee: fdfield_t, hh: fdfield_t, - epsilon: fdfield_t = None, - mu: fdfield_t = None, - dxes: dx_lists_t = None + epsilon: Optional[Union[fdfield_t, float]] = None, + mu: Optional[Union[fdfield_t, float]] = None, + dxes: Optional[dx_lists_t] = None ) -> fdfield_t: if epsilon is None: epsilon = 1 diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py index 2c4ae1e..0edd01a 100644 --- a/meanas/fdtd/pml.py +++ b/meanas/fdtd/pml.py @@ -7,7 +7,7 @@ PML implementations """ # TODO retest pmls! -from typing import List, Callable, Tuple, Dict +from typing import List, Callable, Tuple, Dict, Any import numpy from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t @@ -59,9 +59,9 @@ def cpml(direction: int, else: raise Exception('Bad polarity!') - expand_slice = [None] * 3 - expand_slice[direction] = slice(None) - expand_slice = tuple(expand_slice) + expand_slice_l: List[Any] = [None] * 3 + expand_slice_l[direction] = slice(None) + expand_slice = tuple(expand_slice_l) def par(x): scaling = (x / thickness) ** m From a312a3085cb239cc7a72f79a3ee1845877faaf08 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 11 Jun 2020 19:26:51 -0700 Subject: [PATCH 232/437] hmn_2_exyz should return an ndarray --- meanas/fdfd/bloch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index db055f6..e6ff2bb 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -278,7 +278,7 @@ def hmn_2_exyz(k0: numpy.ndarray, m * hin_n) * k_mag # divide by epsilon - return [ei for ei in numpy.rollaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3)] + return numpy.array([ei for ei in numpy.rollaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3)]) #TODO avoid copy return operator From d906514623898684a6a14de546a431b974e3d3ae Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 11 Jun 2020 19:27:20 -0700 Subject: [PATCH 233/437] add missing return --- meanas/fdfd/functional.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index 33ed134..c0f72ab 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -180,6 +180,7 @@ def e_tfsf_source(TF_region: fdfield_t, def op(e): neg_iwj = A(TF_region * e) - TF_region * A(e) return neg_iwj / (-1j * omega) + return op def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[fdfield_t, fdfield_t], fdfield_t]: From 77e7781705817d560ed0cd4984398f3bce4ecf7a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 11 Jun 2020 19:27:35 -0700 Subject: [PATCH 234/437] fix imports --- meanas/fdfd/waveguide_2d.py | 5 ++--- meanas/fdfd/waveguide_cyl.py | 13 +++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 21bb404..15f9ccc 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -154,7 +154,6 @@ import scipy.sparse as sparse from ..fdmath.operators import deriv_forward, deriv_back, curl_forward, curl_back, cross from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration -from . import operators __author__ = 'Jan Petykiewicz' @@ -275,8 +274,8 @@ def operator_h(omega: complex, if numpy.any(numpy.equal(mu, None)): mu = numpy.ones_like(epsilon) - Dfx, Dfy = operators.deriv_forward(dxes[0]) - Dbx, Dby = operators.deriv_back(dxes[1]) + Dfx, Dfy = deriv_forward(dxes[0]) + Dbx, Dby = deriv_back(dxes[1]) eps_parts = numpy.split(epsilon, 3) eps_yx = sparse.diags(numpy.hstack((eps_parts[1], eps_parts[0]))) diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index 8ac55bd..cfc09c1 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -14,11 +14,8 @@ from numpy.linalg import norm import scipy.sparse as sparse from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t +from ..fdmath.operators import deriv_forward, deriv_back from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration -from . import operators - - -__author__ = 'Jan Petykiewicz' def cylindrical_operator(omega: complex, @@ -50,8 +47,8 @@ def cylindrical_operator(omega: complex, Sparse matrix representation of the operator """ - Dfx, Dfy = operators.deriv_forward(dxes[0]) - Dbx, Dby = operators.deriv_back(dxes[1]) + Dfx, Dfy = deriv_forward(dxes[0]) + Dbx, Dby = deriv_back(dxes[1]) rx = r0 + numpy.cumsum(dxes[0][0]) ry = r0 + dxes[0][0]/2.0 + numpy.cumsum(dxes[1][0]) @@ -109,7 +106,7 @@ def solve_mode(mode_number: int, ''' dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] - A_r = waveguide.cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0) + A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0) eigvals, eigvecs = signed_eigensolve(A_r, mode_number + 3) e_xy = eigvecs[:, -(mode_number+1)] @@ -117,7 +114,7 @@ def solve_mode(mode_number: int, Now solve for the eigenvector of the full operator, using the real operator's eigenvector as an initial guess for Rayleigh quotient iteration. ''' - A = waveguide.cylindrical_operator(omega, dxes, epsilon, r0) + A = cylindrical_operator(omega, dxes, epsilon, r0) eigval, e_xy = rayleigh_quotient_iteration(A, e_xy) # Calculate the wave-vector (force the real part to be positive) From fa3f8db2676276baf9045bd6ba0b16dcb9212197 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 11 Jun 2020 19:28:37 -0700 Subject: [PATCH 235/437] add .mypy_cache to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 48f9cd7..ff695f0 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ target/ .idea/ +.mypy_cache/ .*.sw[op] From 0e04f5ca77dac78b9403374e796adb9a845a5764 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 11 Jun 2020 19:29:25 -0700 Subject: [PATCH 236/437] zero k=0 values --- meanas/fdfd/farfield.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index ea11224..5e6f98e 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -188,6 +188,11 @@ def far_to_nearfield(E_far: fdfield_t, sin_th[numpy.logical_and(kx == 0, ky == 0)] = 0 cos_th[numpy.logical_and(kx == 0, ky == 0)] = 1 + theta = numpy.arctan2(ky, kx) + phi = numpy.arccos(cos_phi) + theta[numpy.logical_and(kx == 0, ky == 0)] = 0 + phi[numpy.logical_and(kx == 0, ky == 0)] = 0 + # Zero fields beyond valid (phi, theta) invalid_ind = kxy2 >= k * k theta[invalid_ind] = 0 From d13a3796a939f16333b6b6ce74f14c115a29ea7e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 16 Oct 2020 19:16:13 -0700 Subject: [PATCH 237/437] style and type fixes (per mypy and flake8) --- .flake8 | 26 ++++++++++++++ meanas/eigensolvers.py | 28 +++++++++------ meanas/fdfd/bloch.py | 62 ++++++++++++++++++---------------- meanas/fdfd/farfield.py | 31 ++++++++--------- meanas/fdfd/functional.py | 4 +-- meanas/fdfd/operators.py | 29 ++++++++-------- meanas/fdfd/scpml.py | 8 ++--- meanas/fdfd/solvers.py | 18 +++++----- meanas/fdfd/waveguide_2d.py | 18 +++++----- meanas/fdfd/waveguide_3d.py | 19 +++++------ meanas/fdfd/waveguide_cyl.py | 19 +++++------ meanas/fdmath/functional.py | 4 +-- meanas/fdmath/operators.py | 12 +++---- meanas/fdmath/types.py | 2 +- meanas/fdmath/vectorization.py | 7 ++-- meanas/fdtd/__init__.py | 2 +- meanas/fdtd/base.py | 9 +++-- meanas/fdtd/boundaries.py | 5 ++- meanas/fdtd/energy.py | 33 +++++++++--------- meanas/fdtd/pml.py | 16 ++++----- meanas/test/conftest.py | 22 ++++++------ meanas/test/test_fdfd.py | 16 +++++---- meanas/test/test_fdfd_pml.py | 25 +++++--------- meanas/test/test_fdtd.py | 35 ++++++++++--------- meanas/test/utils.py | 9 ++--- 25 files changed, 242 insertions(+), 217 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..e18673e --- /dev/null +++ b/.flake8 @@ -0,0 +1,26 @@ +[flake8] +ignore = + # E501 line too long + E501, + # W391 newlines at EOF + W391, + # E241 multiple spaces after comma + E241, + # E302 expected 2 newlines + E302, + # W503 line break before binary operator (to be deprecated) + W503, + # E265 block comment should start with '# ' + E265, + # E123 closing bracket does not match indentation of opening bracket's line + E123, + # E124 closing bracket does not match visual indentation + E124, + # E221 multiple spaces before operator + E221, + # E201 whitespace after '[' + E201, + +per-file-ignores = + # F401 import without use + */__init__.py: F401, diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py index 67cecea..a15a06d 100644 --- a/meanas/eigensolvers.py +++ b/meanas/eigensolvers.py @@ -2,10 +2,10 @@ Solvers for eigenvalue / eigenvector problems """ from typing import Tuple, Callable, Optional, Union -import numpy -from numpy.linalg import norm -from scipy import sparse -import scipy.sparse.linalg as spalg +import numpy # type: ignore +from numpy.linalg import norm # type: ignore +from scipy import sparse # type: ignore +import scipy.sparse.linalg as spalg # type: ignore def power_iteration(operator: sparse.spmatrix, @@ -30,7 +30,8 @@ def power_iteration(operator: sparse.spmatrix, for _ in range(iterations): v = operator @ v - v /= norm(v) + v /= numpy.abs(v).sum() # faster than true norm + v /= norm(v) lm_eigval = v.conj() @ (operator @ v) return lm_eigval, v @@ -59,16 +60,21 @@ def rayleigh_quotient_iteration(operator: Union[sparse.spmatrix, spalg.LinearOpe (eigenvalues, eigenvectors) """ try: - _test = operator - sparse.eye(operator.shape[0]) - shift = lambda eigval: eigval * sparse.eye(operator.shape[0]) + (operator - sparse.eye(operator.shape[0])) + + def shift(eigval: float) -> sparse: + return eigval * sparse.eye(operator.shape[0]) + if solver is None: solver = spalg.spsolve except TypeError: - shift = lambda eigval: spalg.LinearOperator(shape=operator.shape, - dtype=operator.dtype, - matvec=lambda v: eigval * v) + def shift(eigval: float) -> spalg.LinearOperator: + return spalg.LinearOperator(shape=operator.shape, + dtype=operator.dtype, + matvec=lambda v: eigval * v) if solver is None: - solver = lambda A, b: spalg.bicgstab(A, b)[0] + def solver(A, b): + return spalg.bicgstab(A, b)[0] v = numpy.squeeze(guess_vector) v /= norm(v) diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index e6ff2bb..33c5c13 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -82,13 +82,13 @@ This module contains functions for generating and solving the from typing import Tuple, Callable import logging -import numpy -from numpy import pi, real, trace -from numpy.fft import fftfreq -import scipy -import scipy.optimize -from scipy.linalg import norm -import scipy.sparse.linalg as spalg +import numpy # type: ignore +from numpy import pi, real, trace # type: ignore +from numpy.fft import fftfreq # type: ignore +import scipy # type: ignore +import scipy.optimize # type: ignore +from scipy.linalg import norm # type: ignore +import scipy.sparse.linalg as spalg # type: ignore from ..fdmath import fdfield_t @@ -96,8 +96,8 @@ logger = logging.getLogger(__name__) try: - import pyfftw.interfaces.numpy_fft - import pyfftw.interfaces + import pyfftw.interfaces.numpy_fft # type: ignore + import pyfftw.interfaces # type: ignore import multiprocessing logger.info('Using pyfftw') @@ -116,7 +116,7 @@ try: return pyfftw.interfaces.numpy_fft.ifftn(*args, **kwargs, **fftw_args) except ImportError: - from numpy.fft import fftn, ifftn + from numpy.fft import fftn, ifftn # type: ignore logger.info('Using numpy fft') @@ -139,7 +139,7 @@ def generate_kmn(k0: numpy.ndarray, """ k0 = numpy.array(k0) - Gi_grids = numpy.meshgrid(*(fftfreq(n, 1/n) for n in shape[:3]), indexing='ij') + Gi_grids = numpy.meshgrid(*(fftfreq(n, 1 / n) for n in shape[:3]), indexing='ij') Gi = numpy.stack(Gi_grids, axis=3) k_G = k0[None, None, None, :] - Gi @@ -216,8 +216,8 @@ def maxwell_operator(k0: numpy.ndarray, #{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis # cross product and transform into xyz basis - d_xyz = (n * hin_m - - m * hin_n) * k_mag + d_xyz = (n * hin_m + - m * hin_n) * k_mag # divide by epsilon e_xyz = fftn(ifftn(d_xyz, axes=range(3)) / epsilon, axes=range(3)) @@ -230,8 +230,8 @@ def maxwell_operator(k0: numpy.ndarray, h_m, h_n = b_m, b_n else: # transform from mn to xyz - b_xyz = (m * b_m[:, :, :, None] + - n * b_n[:, :, :, None]) + b_xyz = (m * b_m[:, :, :, None] + + n * b_n[:, :, :, None]) # divide by mu h_xyz = fftn(ifftn(b_xyz, axes=range(3)) / mu, axes=range(3)) @@ -274,11 +274,11 @@ def hmn_2_exyz(k0: numpy.ndarray, def operator(h: numpy.ndarray) -> fdfield_t: hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] - d_xyz = (n * hin_m - - m * hin_n) * k_mag + d_xyz = (n * hin_m + - m * hin_n) * k_mag # divide by epsilon - return numpy.array([ei for ei in numpy.rollaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3)]) #TODO avoid copy + return numpy.array([ei for ei in numpy.rollaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3)]) # TODO avoid copy return operator @@ -311,8 +311,8 @@ def hmn_2_hxyz(k0: numpy.ndarray, def operator(h: numpy.ndarray): hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] - h_xyz = (m * hin_m + - n * hin_n) + h_xyz = (m * hin_m + + n * hin_n) return [ifftn(hi) for hi in numpy.rollaxis(h_xyz, 3)] return operator @@ -371,8 +371,8 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, b_m, b_n = hin_m, hin_n else: # transform from mn to xyz - h_xyz = (m * hin_m[:, :, :, None] + - n * hin_n[:, :, :, None]) + h_xyz = (m * hin_m[:, :, :, None] + + n * hin_n[:, :, :, None]) # multiply by mu b_xyz = fftn(ifftn(h_xyz, axes=range(3)) * mu, axes=range(3)) @@ -382,8 +382,8 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, b_n = numpy.sum(b_xyz * n, axis=3) # cross product and transform into xyz basis - e_xyz = (n * b_m - - m * b_n) / k_mag + e_xyz = (n * b_m + - m * b_n) / k_mag # multiply by epsilon d_xyz = fftn(ifftn(e_xyz, axes=range(3)) * epsilon, axes=range(3)) @@ -553,6 +553,7 @@ def eigsolve(num_modes: int, symZtAD = _symmetrize(Z.conj().T @ AD) Qi_memo = [None, None] + def Qi_func(theta): nonlocal Qi_memo if Qi_memo[0] == theta: @@ -560,7 +561,7 @@ def eigsolve(num_modes: int, c = numpy.cos(theta) s = numpy.sin(theta) - Q = c*c * ZtZ + s*s * DtD + 2*s*c * symZtD + Q = c * c * ZtZ + s * s * DtD + 2 * s * c * symZtD try: Qi = numpy.linalg.inv(Q) except numpy.linalg.LinAlgError: @@ -568,10 +569,10 @@ def eigsolve(num_modes: int, # if c or s small, taylor expand if c < 1e-4 * s and c != 0: DtDi = numpy.linalg.inv(DtD) - Qi = DtDi / (s*s) - 2*c/(s*s*s) * (DtDi @ (DtDi @ symZtD).conj().T) + Qi = DtDi / (s * s) - 2 * c / (s * s * s) * (DtDi @ (DtDi @ symZtD).conj().T) elif s < 1e-4 * c and s != 0: ZtZi = numpy.linalg.inv(ZtZ) - Qi = ZtZi / (c*c) - 2*s/(c*c*c) * (ZtZi @ (ZtZi @ symZtD).conj().T) + Qi = ZtZi / (c * c) - 2 * s / (c * c * c) * (ZtZi @ (ZtZi @ symZtD).conj().T) else: raise Exception('Inexplicable singularity in trace_func') Qi_memo[0] = theta @@ -582,7 +583,7 @@ def eigsolve(num_modes: int, c = numpy.cos(theta) s = numpy.sin(theta) Qi = Qi_func(theta) - R = c*c * ZtAZ + s*s * DtAD + 2*s*c * symZtAD + R = c * c * ZtAZ + s * s * DtAD + 2 * s * c * symZtAD trace = _rtrace_AtB(R, Qi) return numpy.abs(trace) @@ -646,15 +647,16 @@ def eigsolve(num_modes: int, v = eigvecs[:, i] n = eigvals[i] v /= norm(v) - eigness = norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v ) + eigness = norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v) f = numpy.sqrt(-numpy.real(n)) df = numpy.sqrt(-numpy.real(n + eigness)) - neff_err = kmag * (1/df - 1/f) + neff_err = kmag * (1 / df - 1 / f) logger.info('eigness {}: {}\n neff_err: {}'.format(i, eigness, neff_err)) order = numpy.argsort(numpy.abs(eigvals)) return eigvals[order], eigvecs.T[order] + ''' def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): if df0 > 0: diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index 5e6f98e..eb53b24 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -2,9 +2,9 @@ Functions for performing near-to-farfield transformation (and the reverse). """ from typing import Dict, List, Any -import numpy -from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift -from numpy import pi +import numpy # type: ignore +from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift # type: ignore +from numpy import pi # type: ignore from ..fdmath import fdfield_t @@ -60,7 +60,7 @@ def near_to_farfield(E_near: fdfield_t, if padded_size is None: padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int) if not hasattr(padded_size, '__len__'): - padded_size = (padded_size, padded_size) + padded_size = (padded_size, padded_size) # type: ignore # checked if sequence En_fft = [fftshift(fft2(fftshift(Eni), s=padded_size)) for Eni in E_near] Hn_fft = [fftshift(fft2(fftshift(Hni), s=padded_size)) for Hni in H_near] @@ -109,8 +109,8 @@ def near_to_farfield(E_near: fdfield_t, outputs = { 'E': E_far, 'H': H_far, - 'dkx': kx[1]-kx[0], - 'dky': ky[1]-ky[0], + 'dkx': kx[1] - kx[0], + 'dky': ky[1] - ky[0], 'kx': kx, 'ky': ky, 'theta': theta, @@ -120,7 +120,6 @@ def near_to_farfield(E_near: fdfield_t, return outputs - def far_to_nearfield(E_far: fdfield_t, H_far: fdfield_t, dkx: float, @@ -166,14 +165,13 @@ def far_to_nearfield(E_far: fdfield_t, raise Exception('All fields must be the same shape!') if padded_size is None: - padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int) + padded_size = (2 ** numpy.ceil(numpy.log2(s))).astype(int) if not hasattr(padded_size, '__len__'): - padded_size = (padded_size, padded_size) - + padded_size = (padded_size, padded_size) # type: ignore # checked if sequence k = 2 * pi - kxs = fftshift(fftfreq(s[0], 1/(s[0] * dkx))) - kys = fftshift(fftfreq(s[0], 1/(s[1] * dky))) + kxs = fftshift(fftfreq(s[0], 1 / (s[0] * dkx))) + kys = fftshift(fftfreq(s[0], 1 / (s[1] * dky))) kx, ky = numpy.meshgrid(kxs, kys, indexing='ij') kxy2 = kx * kx + ky * ky @@ -201,18 +199,17 @@ def far_to_nearfield(E_far: fdfield_t, E_far[i][invalid_ind] = 0 H_far[i][invalid_ind] = 0 - # Normalized vector potentials N, L L = [0.5 * E_far[1], -0.5 * E_far[0]] N = [L[1], -L[0]] - En_fft = [-( L[0] * sin_th + L[1] * cos_phi * cos_th)/cos_phi, - -(-L[0] * cos_th + L[1] * cos_phi * sin_th)/cos_phi] + En_fft = [-( L[0] * sin_th + L[1] * cos_phi * cos_th) / cos_phi, + -(-L[0] * cos_th + L[1] * cos_phi * sin_th) / cos_phi] - Hn_fft = [( N[0] * sin_th + N[1] * cos_phi * cos_th)/cos_phi, - (-N[0] * cos_th + N[1] * cos_phi * sin_th)/cos_phi] + Hn_fft = [( N[0] * sin_th + N[1] * cos_phi * cos_th) / cos_phi, + (-N[0] * cos_th + N[1] * cos_phi * sin_th) / cos_phi] for i in range(2): En_fft[i][cos_phi == 0] = 0 diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index c0f72ab..488d58e 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -5,8 +5,8 @@ Functional versions of many FDFD operators. These can be useful for performing The functions generated here expect `fdfield_t` inputs with shape (3, X, Y, Z), e.g. E = [E_x, E_y, E_z] where each component has shape (X, Y, Z) """ -from typing import List, Callable, Tuple -import numpy +from typing import Callable, Tuple +import numpy # type: ignore from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t from ..fdmath.functional import curl_forward, curl_back diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 212ed4c..ef2fd57 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -28,8 +28,8 @@ The following operators are included: """ from typing import Tuple, Optional -import numpy -import scipy.sparse as sparse +import numpy # type: ignore +import scipy.sparse as sparse # type: ignore from ..fdmath import vec, dx_lists_t, vfdfield_t from ..fdmath.operators import shift_with_mirror, rotation, curl_forward, curl_back @@ -90,7 +90,7 @@ def e_full(omega: complex, if numpy.any(numpy.equal(mu, None)): m_div = sparse.eye(epsilon.size) else: - m_div = sparse.diags(1 / mu) + m_div = sparse.diags(1 / mu) # type: ignore # checked mu is not None op = pe @ (ch @ pm @ m_div @ ce - omega**2 * e) @ pe return op @@ -270,7 +270,7 @@ def e2h(omega: complex, op = curl_forward(dxes[0]) / (-1j * omega) if not numpy.any(numpy.equal(mu, None)): - op = sparse.diags(1 / mu) @ op + op = sparse.diags(1 / mu) @ op # type: ignore # checked mu is not None if not numpy.any(numpy.equal(pmc, None)): op = sparse.diags(numpy.where(pmc, 0, 1)) @ op @@ -297,7 +297,7 @@ def m2j(omega: complex, op = curl_back(dxes[1]) / (1j * omega) if not numpy.any(numpy.equal(mu, None)): - op = op @ sparse.diags(1 / mu) + op = op @ sparse.diags(1 / mu) # type: ignore # checked mu is not None return op @@ -319,14 +319,13 @@ def poynting_e_cross(e: vfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: fx, fy, fz = [rotation(i, shape, 1) for i in range(3)] dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] - dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] Ex, Ey, Ez = [ei * da for ei, da in zip(numpy.split(e, 3), dxag)] block_diags = [[ None, fx @ -Ez, fx @ Ey], [ fy @ Ez, None, fy @ -Ex], [ fz @ -Ey, fz @ Ex, None]] block_matrix = sparse.bmat([[sparse.diags(x) if x is not None else None for x in row] - for row in block_diags]) + for row in block_diags]) P = block_matrix @ sparse.diags(numpy.concatenate(dxag)) return P @@ -351,10 +350,10 @@ def poynting_h_cross(h: vfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: Hx, Hy, Hz = [sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg)] P = (sparse.bmat( - [[ None, -Hz @ fx, Hy @ fx], - [ Hz @ fy, None, -Hx @ fy], - [-Hy @ fz, Hx @ fz, None]]) - @ sparse.diags(numpy.concatenate(dxag))) + [[ None, -Hz @ fx, Hy @ fx], + [ Hz @ fy, None, -Hx @ fy], + [-Hy @ fz, Hx @ fz, None]]) + @ sparse.diags(numpy.concatenate(dxag))) return P @@ -418,15 +417,17 @@ def e_boundary_source(mask: vfdfield_t, jmask = numpy.zeros_like(mask, dtype=bool) if periodic_mask_edges: - shift = lambda axis, polarity: rotation(axis=axis, shape=shape, shift_distance=polarity) + def shift(axis, polarity): + return rotation(axis=axis, shape=shape, shift_distance=polarity) else: - shift = lambda axis, polarity: shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity) + def shift(axis, polarity): + return shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity) for axis in (0, 1, 2): if shape[axis] == 1: continue for polarity in (-1, +1): - r = shift(axis, polarity) - sparse.eye(numpy.prod(shape)) # shifted minus original + r = shift(axis, polarity) - sparse.eye(numpy.prod(shape)) # shifted minus original r3 = sparse.block_diag((r, r, r)) jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask)) diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py index 099fac2..00587f7 100644 --- a/meanas/fdfd/scpml.py +++ b/meanas/fdfd/scpml.py @@ -3,7 +3,7 @@ Functions for creating stretched coordinate perfectly matched layer (PML) absorb """ from typing import Sequence, Union, Callable, Optional -import numpy +import numpy # type: ignore from ..fdmath import dx_lists_t, dx_lists_mut @@ -69,7 +69,7 @@ def uniform_grid_scpml(shape: Union[numpy.ndarray, Sequence[int]], s_function = prepare_s_function() # Normalized distance to nearest boundary - def l(u, n, t): + def ll(u, n, t): return ((t - u).clip(0) + (u - (n - t)).clip(0)) / t dx_a = [numpy.array(numpy.inf)] * 3 @@ -82,8 +82,8 @@ def uniform_grid_scpml(shape: Union[numpy.ndarray, Sequence[int]], s = shape[k] if th > 0: sr = numpy.arange(s) - dx_a[k] = 1 + 1j * s_function(l(sr, s, th)) / s_correction - dx_b[k] = 1 + 1j * s_function(l(sr+0.5, s, th)) / s_correction + dx_a[k] = 1 + 1j * s_function(ll(sr, s, th)) / s_correction + dx_b[k] = 1 + 1j * s_function(ll(sr + 0.5, s, th)) / s_correction else: dx_a[k] = numpy.ones((s,)) dx_b[k] = numpy.ones((s,)) diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py index ff5bfe3..73548ca 100644 --- a/meanas/fdfd/solvers.py +++ b/meanas/fdfd/solvers.py @@ -2,12 +2,12 @@ Solvers and solver interface for FDFD problems. """ -from typing import List, Callable, Dict, Any +from typing import Callable, Dict, Any import logging -import numpy -from numpy.linalg import norm -import scipy.sparse.linalg +import numpy # type: ignore +from numpy.linalg import norm # type: ignore +import scipy.sparse.linalg # type: ignore from ..fdmath import dx_lists_t, vfdfield_t from . import operators @@ -35,13 +35,13 @@ def _scipy_qmr(A: scipy.sparse.csr_matrix, ''' Report on our progress ''' - iter = 0 + ii = 0 def log_residual(xk): - nonlocal iter - iter += 1 - if iter % 100 == 0: - logger.info('Solver residual at iteration {} : {}'.format(iter, norm(A @ xk - b))) + nonlocal ii + ii += 1 + if ii % 100 == 0: + logger.info('Solver residual at iteration {} : {}'.format(ii, norm(A @ xk - b))) if 'callback' in kwargs: def augmented_callback(xk): diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 15f9ccc..e5c3775 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -147,12 +147,12 @@ to account for numerical dispersion if the result is introduced into a space wit # TODO update module docs from typing import List, Tuple, Optional -import numpy -from numpy.linalg import norm -import scipy.sparse as sparse +import numpy # type: ignore +from numpy.linalg import norm # type: ignore +import scipy.sparse as sparse # type: ignore -from ..fdmath.operators import deriv_forward, deriv_back, curl_forward, curl_back, cross -from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t +from ..fdmath.operators import deriv_forward, deriv_back, cross +from ..fdmath import unvec, dx_lists_t, vfdfield_t from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration @@ -390,7 +390,9 @@ def _normalized_fields(e: numpy.ndarray, # Try to break symmetry to assign a consistent sign [experimental TODO] E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape) - sign = numpy.sign(E_weighted[:, :max(shape[0]//2, 1), :max(shape[1]//2, 1)].real.sum()) + sign = numpy.sign(E_weighted[:, + :max(shape[0] // 2, 1), + :max(shape[1] // 2, 1)].real.sum()) norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) @@ -536,7 +538,7 @@ def e2h(wavenumber: complex, """ op = curl_e(wavenumber, dxes) / (-1j * omega) if not numpy.any(numpy.equal(mu, None)): - op = sparse.diags(1 / mu) @ op + op = sparse.diags(1 / mu) @ op # type: ignore # checked that mu is not None return op @@ -663,7 +665,7 @@ def e_err(e: vfdfield_t, if numpy.any(numpy.equal(mu, None)): op = ch @ ce @ e - omega ** 2 * (epsilon * e) else: - mu_inv = sparse.diags(1 / mu) + mu_inv = sparse.diags(1 / mu) # type: ignore # checked that mu is not None op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e) return norm(op) / norm(e) diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index 847986d..8f466de 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -4,12 +4,11 @@ Tools for working with waveguide modes in 3D domains. This module relies heavily on `waveguide_2d` and mostly just transforms its parameters into 2D equivalents and expands the results back into 3D. """ -from typing import Dict, List, Tuple, Optional, Sequence, Union -import numpy -import scipy.sparse as sparse +from typing import Dict, Optional, Sequence, Union, Any +import numpy # type: ignore -from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, fdfield_t -from . import operators, waveguide_2d, functional +from ..fdmath import vec, unvec, dx_lists_t, fdfield_t +from . import operators, waveguide_2d def solve_mode(mode_number: int, @@ -53,10 +52,10 @@ def solve_mode(mode_number: int, # Find dx in propagation direction dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes]) - dx_prop = 0.5 * sum(dxab_forward)[0] + dx_prop = 0.5 * dxab_forward.sum() # Reduce to 2D and solve the 2D problem - args_2d = { + args_2d: Dict[str, Any] = { 'omega': omega, 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), @@ -68,15 +67,15 @@ def solve_mode(mode_number: int, Apply corrections and expand to 3D ''' # Correct wavenumber to account for numerical dispersion. - wavenumber = 2/dx_prop * numpy.arcsin(wavenumber_2d * dx_prop/2) + wavenumber = 2 / dx_prop * numpy.arcsin(wavenumber_2d * dx_prop / 2) shape = [d.size for d in args_2d['dxes'][0]] - ve, vh = waveguide_2d.normalized_fields_e(e_xy, wavenumber=wavenumber_2d, **args_2d, prop_phase=dx_prop * wavenumber) + ve, vh = waveguide_2d.normalized_fields_e(e_xy, wavenumber=wavenumber_2d, prop_phase=dx_prop * wavenumber, **args_2d) e = unvec(ve, shape) h = unvec(vh, shape) # Adjust for propagation direction - h *= polarity + h *= polarity # type: ignore # mypy issue with numpy # Apply phase shift to H-field h[:2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop) diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index cfc09c1..b9213ec 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -8,10 +8,9 @@ As the z-dependence is known, all the functions in this file assume a 2D grid """ # TODO update module docs -from typing import List, Tuple, Dict, Union -import numpy -from numpy.linalg import norm -import scipy.sparse as sparse +from typing import Dict, Union +import numpy # type: ignore +import scipy.sparse as sparse # type: ignore from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t from ..fdmath.operators import deriv_forward, deriv_back @@ -51,9 +50,9 @@ def cylindrical_operator(omega: complex, Dbx, Dby = deriv_back(dxes[1]) rx = r0 + numpy.cumsum(dxes[0][0]) - ry = r0 + dxes[0][0]/2.0 + numpy.cumsum(dxes[1][0]) - tx = rx/r0 - ty = ry/r0 + ry = r0 + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0]) + tx = rx / r0 + ty = ry / r0 Tx = sparse.diags(vec(tx[:, None].repeat(dxes[0][1].size, axis=1))) Ty = sparse.diags(vec(ty[:, None].repeat(dxes[1][1].size, axis=1))) @@ -108,7 +107,7 @@ def solve_mode(mode_number: int, A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0) eigvals, eigvecs = signed_eigensolve(A_r, mode_number + 3) - e_xy = eigvecs[:, -(mode_number+1)] + e_xy = eigvecs[:, -(mode_number + 1)] ''' Now solve for the eigenvector of the full operator, using the real operator's @@ -128,8 +127,8 @@ def solve_mode(mode_number: int, fields = { 'wavenumber': wavenumber, 'E': unvec(e_xy, shape), -# 'E': unvec(e, shape), -# 'H': unvec(h, shape), + # 'E': unvec(e, shape), + # 'H': unvec(h, shape), } return fields diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py index 379c310..d8e0758 100644 --- a/meanas/fdmath/functional.py +++ b/meanas/fdmath/functional.py @@ -3,8 +3,8 @@ Math functions for finite difference simulations Basic discrete calculus etc. """ -from typing import Sequence, Tuple, Dict, Optional -import numpy +from typing import Sequence, Tuple, Optional +import numpy # type: ignore from .types import fdfield_t, fdfield_updater_t diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py index 30a6708..72fbf5d 100644 --- a/meanas/fdmath/operators.py +++ b/meanas/fdmath/operators.py @@ -3,14 +3,14 @@ Matrix operators for finite difference simulations Basic discrete calculus etc. """ -from typing import Sequence, List, Callable, Tuple, Dict -import numpy -import scipy.sparse as sparse +from typing import Sequence, List +import numpy # type: ignore +import scipy.sparse as sparse # type: ignore -from .types import fdfield_t, vfdfield_t +from .types import vfdfield_t -def rotation(axis: int, shape: Sequence[int], shift_distance: int=1) -> sparse.spmatrix: +def rotation(axis: int, shape: Sequence[int], shift_distance: int = 1) -> sparse.spmatrix: """ Utility operator for performing a circular shift along a specified axis by a specified number of elements. @@ -46,7 +46,7 @@ def rotation(axis: int, shape: Sequence[int], shift_distance: int=1) -> sparse.s return d -def shift_with_mirror(axis: int, shape: Sequence[int], shift_distance: int=1) -> sparse.spmatrix: +def shift_with_mirror(axis: int, shape: Sequence[int], shift_distance: int = 1) -> sparse.spmatrix: """ Utility operator for performing an n-element shift along a specified axis, with mirror boundary conditions applied to the cells beyond the receding edge. diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py index 8215fed..2676bc5 100644 --- a/meanas/fdmath/types.py +++ b/meanas/fdmath/types.py @@ -1,8 +1,8 @@ """ Types shared across multiple submodules """ -import numpy from typing import Sequence, Callable, MutableSequence +import numpy # type: ignore # Field types diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py index 807fc5d..23e3d9c 100644 --- a/meanas/fdmath/vectorization.py +++ b/meanas/fdmath/vectorization.py @@ -4,11 +4,12 @@ and a 1D array representation of that field `[f_x0, f_x1, f_x2,... f_y0,... f_z0 Vectorized versions of the field use row-major (ie., C-style) ordering. """ -from typing import Optional, TypeVar, overload, Union, List -import numpy +from typing import Optional, overload, Union, List +import numpy # type: ignore from .types import fdfield_t, vfdfield_t + @overload def vec(f: None) -> None: pass @@ -60,5 +61,5 @@ def unvec(v: Optional[vfdfield_t], shape: numpy.ndarray) -> Optional[fdfield_t]: """ if numpy.any(numpy.equal(v, None)): return None - return v.reshape((3, *shape), order='C') + return v.reshape((3, *shape), order='C') # type: ignore # already check v is not None diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py index 64656b7..1a7e1bd 100644 --- a/meanas/fdtd/__init__.py +++ b/meanas/fdtd/__init__.py @@ -162,5 +162,5 @@ Boundary conditions from .base import maxwell_e, maxwell_h from .pml import cpml from .energy import (poynting, poynting_divergence, energy_hstep, energy_estep, - delta_energy_h2e, delta_energy_h2e, delta_energy_j) + delta_energy_h2e, delta_energy_j) from .boundaries import conducting_boundary diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py index a632ca2..753eb71 100644 --- a/meanas/fdtd/base.py +++ b/meanas/fdtd/base.py @@ -3,8 +3,7 @@ Basic FDTD field updates """ -from typing import List, Callable, Dict, Union -import numpy +from typing import Union from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t from ..fdmath.functional import curl_forward, curl_back @@ -59,7 +58,7 @@ def maxwell_e(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t: Returns: E-field at time t=1 """ - e += dt * curl_h_fun(h) / epsilon + e += dt * curl_h_fun(h) / epsilon # type: ignore # mypy gets confused around ndarray ops return e return me_fun @@ -113,9 +112,9 @@ def maxwell_h(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t: H-field at time t=1.5 """ if mu is not None: - h -= dt * curl_e_fun(e) / mu + h -= dt * curl_e_fun(e) / mu # type: ignore # mypy gets confused around ndarray ops else: - h -= dt * curl_e_fun(e) + h -= dt * curl_e_fun(e) # type: ignore # mypy gets confused around ndarray ops return h diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py index dbc1d93..8cf0a25 100644 --- a/meanas/fdtd/boundaries.py +++ b/meanas/fdtd/boundaries.py @@ -4,10 +4,9 @@ Boundary conditions #TODO conducting boundary documentation """ -from typing import Callable, Tuple, Dict, Any, List -import numpy +from typing import Tuple, Any, List -from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t +from ..fdmath import fdfield_t, fdfield_updater_t def conducting_boundary(direction: int, diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index 443176b..b8aa8dc 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -1,9 +1,8 @@ -# pylint: disable=unsupported-assignment-operation -from typing import Callable, Tuple, Dict, Optional, Union -import numpy +from typing import Optional, Union +import numpy # type: ignore -from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t -from ..fdmath.functional import deriv_back, deriv_forward +from ..fdmath import dx_lists_t, fdfield_t +from ..fdmath.functional import deriv_back def poynting(e: fdfield_t, @@ -115,10 +114,10 @@ def delta_energy_j(j0: fdfield_t, if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) - du = ((j0 * e1).sum(axis=0) * - dxes[0][0][:, None, None] * - dxes[0][1][None, :, None] * - dxes[0][2][None, None, :]) + du = ((j0 * e1).sum(axis=0) + * dxes[0][0][:, None, None] + * dxes[0][1][None, :, None] + * dxes[0][2][None, None, :]) return du @@ -135,12 +134,12 @@ def dxmul(ee: fdfield_t, if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) - result = ((ee * epsilon).sum(axis=0) * - dxes[0][0][:, None, None] * - dxes[0][1][None, :, None] * - dxes[0][2][None, None, :] + - (hh * mu).sum(axis=0) * - dxes[1][0][:, None, None] * - dxes[1][1][None, :, None] * - dxes[1][2][None, None, :]) + result = ((ee * epsilon).sum(axis=0) + * dxes[0][0][:, None, None] + * dxes[0][1][None, :, None] + * dxes[0][2][None, None, :] + + (hh * mu).sum(axis=0) + * dxes[1][0][:, None, None] + * dxes[1][1][None, :, None] + * dxes[1][2][None, None, :]) return result diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py index 0edd01a..91e7f12 100644 --- a/meanas/fdtd/pml.py +++ b/meanas/fdtd/pml.py @@ -8,9 +8,9 @@ PML implementations # TODO retest pmls! from typing import List, Callable, Tuple, Dict, Any -import numpy +import numpy # type: ignore -from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t +from ..fdmath import fdfield_t __author__ = 'Jan Petykiewicz' @@ -48,8 +48,8 @@ def cpml(direction: int, transverse = numpy.delete(range(3), direction) u, v = transverse - xe = numpy.arange(1, thickness+1, dtype=float) - xh = numpy.arange(1, thickness+1, dtype=float) + xe = numpy.arange(1, thickness + 1, dtype=float) + xh = numpy.arange(1, thickness + 1, dtype=float) if polarity > 0: xe -= 0.5 elif polarity < 0: @@ -76,14 +76,14 @@ def cpml(direction: int, p0e, p1e, p2e = par(xe) p0h, p1h, p2h = par(xh) - region = [slice(None)] * 3 + region_list = [slice(None)] * 3 if polarity < 0: - region[direction] = slice(None, thickness) + region_list[direction] = slice(None, thickness) elif polarity > 0: - region[direction] = slice(-thickness, None) + region_list[direction] = slice(-thickness, None) else: raise Exception('Bad polarity!') - region = tuple(region) + region = tuple(region_list) se = 1 if direction == 1 else -1 diff --git a/meanas/test/conftest.py b/meanas/test/conftest.py index 2522e0c..3514087 100644 --- a/meanas/test/conftest.py +++ b/meanas/test/conftest.py @@ -1,18 +1,18 @@ -from typing import List, Tuple -import numpy -import pytest +""" + +Test fixtures + +""" +import numpy # type: ignore +import pytest # type: ignore from .utils import PRNG -##################################### -# Test fixtures -##################################### - @pytest.fixture(scope='module', params=[(5, 5, 1), (5, 1, 5), (5, 5, 5), - #(7, 7, 7), + # (7, 7, 7), ]) def shape(request): yield (3, *request.param) @@ -41,7 +41,7 @@ def epsilon(request, shape, epsilon_bg, epsilon_fg): epsilon = numpy.full(shape, epsilon_bg, dtype=float) if request.param == 'center': - epsilon[:, shape[1]//2, shape[2]//2, shape[3]//2] = epsilon_fg + epsilon[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = epsilon_fg elif request.param == '000': epsilon[:, 0, 0, 0] = epsilon_fg elif request.param == 'random': @@ -52,7 +52,7 @@ def epsilon(request, shape, epsilon_bg, epsilon_fg): yield epsilon -@pytest.fixture(scope='module', params=[1.0])#, 1.5]) +@pytest.fixture(scope='module', params=[1.0]) # 1.5 def j_mag(request): yield request.param @@ -70,7 +70,7 @@ def dxes(request, shape, dx): dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] for eh in (0, 1): for ax in (0, 1, 2): - dxes[eh][ax][dxes[eh][ax].size // 2] *= 1.1 + dxes[eh][ax][dxes[eh][ax].size // 2] *= 1.1 elif request.param == 'random': dxe = [PRNG.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]] dxh = [(d + numpy.roll(d, -1)) / 2 for d in dxe] diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 2cf0c44..c6b3c02 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -1,13 +1,12 @@ -# pylint: disable=redefined-outer-name from typing import List, Tuple import dataclasses -import pytest -import numpy +import pytest # type: ignore +import numpy # type: ignore #from numpy.testing import assert_allclose, assert_array_equal from .. import fdfd from ..fdmath import vec, unvec -from .utils import assert_close, assert_fields_close +from .utils import assert_close # , assert_fields_close def test_residual(sim): @@ -53,7 +52,7 @@ def test_poynting_planes(sim): ##################################### # Also see conftest.py -@pytest.fixture(params=[1/1500]) +@pytest.fixture(params=[1 / 1500]) def omega(request): yield request.param @@ -74,11 +73,11 @@ def pmc(request): # yield (3, *request.param) -@pytest.fixture(params=['diag']) #'center' +@pytest.fixture(params=['diag']) # 'center' def j_distribution(request, shape, j_mag): j = numpy.zeros(shape, dtype=complex) center_mask = numpy.zeros(shape, dtype=bool) - center_mask[:, shape[1]//2, shape[2]//2, shape[3]//2] = True + center_mask[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = True if request.param == 'center': j[center_mask] = j_mag @@ -102,6 +101,9 @@ class FDResult: @pytest.fixture() def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): + """ + Build simulation from parts + """ # is3d = (numpy.array(shape) == 1).sum() == 0 # if is3d: # pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)') diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py index d13d753..436aa39 100644 --- a/meanas/test/test_fdfd_pml.py +++ b/meanas/test/test_fdfd_pml.py @@ -1,20 +1,15 @@ ##################################### -# pylint: disable=redefined-outer-name -from typing import List, Tuple -import dataclasses -import pytest -import numpy -from numpy.testing import assert_allclose, assert_array_equal +import pytest # type: ignore +import numpy # type: ignore +from numpy.testing import assert_allclose # type: ignore from .. import fdfd from ..fdmath import vec, unvec -from .utils import assert_close, assert_fields_close +#from .utils import assert_close, assert_fields_close from .test_fdfd import FDResult def test_pml(sim, src_polarity): - dim = numpy.where(numpy.array(sim.shape[1:]) > 1)[0][0] # Propagation axis - e_sqr = numpy.squeeze((sim.e.conj() * sim.e).sum(axis=0)) # from matplotlib import pyplot @@ -43,10 +38,10 @@ def test_pml(sim, src_polarity): # Test fixtures -##################################### +# #################################### # Also see conftest.py -@pytest.fixture(params=[1/1500]) +@pytest.fixture(params=[1 / 1500]) def omega(request): yield request.param @@ -61,7 +56,6 @@ def pmc(request): yield request.param - @pytest.fixture(params=[(30, 1, 1), (1, 30, 1), (1, 1, 30)]) @@ -82,16 +76,15 @@ def j_distribution(request, shape, epsilon, dxes, omega, src_polarity): other_dims = [0, 1, 2] other_dims.remove(dim) - dx_prop = (dxes[0][dim][shape[dim + 1] // 2] + - dxes[1][dim][shape[dim + 1] // 2]) / 2 #TODO is this right for nonuniform dxes? + dx_prop = (dxes[0][dim][shape[dim + 1] // 2] + + dxes[1][dim][shape[dim + 1] // 2]) / 2 # TODO is this right for nonuniform dxes? # Mask only contains components orthogonal to propagation direction center_mask = numpy.zeros(shape, dtype=bool) - center_mask[other_dims, shape[1]//2, shape[2]//2, shape[3]//2] = True + center_mask[other_dims, shape[1] // 2, shape[2] // 2, shape[3] // 2] = True if (epsilon[center_mask] != epsilon[center_mask][0]).any(): center_mask[other_dims[1]] = False # If epsilon is not isotropic, pick only one dimension - wavenumber = omega * numpy.sqrt(epsilon[center_mask].mean()) wavenumber_corrected = 2 / dx_prop * numpy.arcsin(wavenumber * dx_prop / 2) diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 95d6817..efeb3e9 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -1,9 +1,8 @@ -# pylint: disable=redefined-outer-name, no-member from typing import List, Tuple import dataclasses -import pytest -import numpy -from numpy.testing import assert_allclose, assert_array_equal +import pytest # type: ignore +import numpy # type: ignore +#from numpy.testing import assert_allclose, assert_array_equal # type: ignore from .. import fdtd from .utils import assert_close, assert_fields_close, PRNG @@ -29,7 +28,7 @@ def test_initial_energy(sim): e0 = sim.es[0] h0 = sim.hs[0] h1 = sim.hs[1] - mask = (j0 != 0) + dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0) u0 = (j0 * j0.conj() / sim.epsilon * dV).sum(axis=0) args = {'dxes': sim.dxes, @@ -53,10 +52,10 @@ def test_energy_conservation(sim): 'epsilon': sim.epsilon} for ii in range(1, 8): - u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) # pylint: disable=bad-whitespace - u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) # pylint: disable=bad-whitespace - delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii-1], dxes=sim.dxes) - delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) # pylint: disable=bad-whitespace + u_hstep = fdtd.energy_hstep(e0=sim.es[ii - 1], h1=sim.hs[ii], e2=sim.es[ii], **args) + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii - 1], dxes=sim.dxes) + delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) u += delta_j_A.sum() assert_close(u_hstep.sum(), u) @@ -70,8 +69,8 @@ def test_poynting_divergence(sim): u_eprev = None for ii in range(1, 8): - u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) # pylint: disable=bad-whitespace - u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) # pylint: disable=bad-whitespace + u_hstep = fdtd.energy_hstep(e0=sim.es[ii - 1], h1=sim.hs[ii], e2=sim.es[ii], **args) + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) du_half_h2e = u_estep - u_hstep - delta_j_B @@ -83,10 +82,10 @@ def test_poynting_divergence(sim): continue # previous half-step - delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii-1], dxes=sim.dxes) + delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii - 1], dxes=sim.dxes) du_half_e2h = u_hstep - u_eprev - delta_j_A - div_s_e2h = sim.dt * fdtd.poynting_divergence(e=sim.es[ii-1], h=sim.hs[ii], dxes=sim.dxes) + div_s_e2h = sim.dt * fdtd.poynting_divergence(e=sim.es[ii - 1], h=sim.hs[ii], dxes=sim.dxes) assert_fields_close(du_half_e2h, -div_s_e2h) u_eprev = u_estep @@ -105,8 +104,8 @@ def test_poynting_planes(sim): u_eprev = None for ii in range(1, 8): - u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) # pylint: disable=bad-whitespace - u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) # pylint: disable=bad-whitespace + u_hstep = fdtd.energy_hstep(e0=sim.es[ii - 1], h1=sim.hs[ii], e2=sim.es[ii], **args) + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) du_half_h2e = u_estep - u_hstep - delta_j_B @@ -121,7 +120,7 @@ def test_poynting_planes(sim): u_eprev = u_estep continue - delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii-1], dxes=sim.dxes) + delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii - 1], dxes=sim.dxes) du_half_e2h = u_hstep - u_eprev - delta_j_A s_e2h = -fdtd.poynting(e=sim.es[ii - 1], h=sim.hs[ii], dxes=sim.dxes) * sim.dt @@ -158,7 +157,7 @@ class TDResult: js: List[numpy.ndarray] = dataclasses.field(default_factory=list) -@pytest.fixture(params=[(0, 4, 8),]) #(0,)]) +@pytest.fixture(params=[(0, 4, 8)]) # (0,) def j_steps(request): yield request.param @@ -167,7 +166,7 @@ def j_steps(request): def j_distribution(request, shape, j_mag): j = numpy.zeros(shape) if request.param == 'center': - j[:, shape[1]//2, shape[2]//2, shape[3]//2] = j_mag + j[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = j_mag elif request.param == '000': j[:, 0, 0, 0] = j_mag elif request.param == 'random': diff --git a/meanas/test/utils.py b/meanas/test/utils.py index ac657a1..a49bc04 100644 --- a/meanas/test/utils.py +++ b/meanas/test/utils.py @@ -1,11 +1,12 @@ -import numpy +import numpy # type: ignore PRNG = numpy.random.RandomState(12345) def assert_fields_close(x, y, *args, **kwargs): - numpy.testing.assert_allclose(x, y, verbose=False, - err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(x, -1), - numpy.rollaxis(y, -1)), *args, **kwargs) + numpy.testing.assert_allclose( + x, y, verbose=False, + err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(x, -1), + numpy.rollaxis(y, -1)), *args, **kwargs) def assert_close(x, y, *args, **kwargs): numpy.testing.assert_allclose(x, y, *args, **kwargs) From bc428f5e8ea3da6cc42353ee91a6d7bd014c0b3e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 16 Oct 2020 21:46:04 -0700 Subject: [PATCH 238/437] add more type hints --- meanas/eigensolvers.py | 3 ++- meanas/fdfd/bloch.py | 28 ++++++++++----------- meanas/fdfd/functional.py | 36 +++++++++++++------------- meanas/fdfd/operators.py | 13 +++++----- meanas/fdfd/scpml.py | 16 ++++++------ meanas/fdfd/solvers.py | 6 ++--- meanas/fdfd/waveguide_2d.py | 6 ++--- meanas/fdmath/operators.py | 6 ++--- meanas/fdtd/__init__.py | 2 +- meanas/fdtd/boundaries.py | 8 +++--- meanas/fdtd/energy.py | 3 +++ meanas/fdtd/pml.py | 2 +- meanas/test/conftest.py | 22 ++++++++++------ meanas/test/test_fdfd.py | 29 ++++++++++++++------- meanas/test/test_fdfd_pml.py | 49 ++++++++++++++++++++++++++---------- meanas/test/test_fdtd.py | 34 ++++++++++++++++--------- meanas/test/utils.py | 13 ++++++++-- 17 files changed, 170 insertions(+), 106 deletions(-) diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py index a15a06d..8c1739e 100644 --- a/meanas/eigensolvers.py +++ b/meanas/eigensolvers.py @@ -73,8 +73,9 @@ def rayleigh_quotient_iteration(operator: Union[sparse.spmatrix, spalg.LinearOpe dtype=operator.dtype, matvec=lambda v: eigval * v) if solver is None: - def solver(A, b): + def solver(A: spalg.LinearOperator, b: numpy.ndarray) -> numpy.ndarray: return spalg.bicgstab(A, b)[0] + assert(solver is not None) v = numpy.squeeze(guess_vector) v /= norm(v) diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index 33c5c13..ed6ca39 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -80,7 +80,7 @@ This module contains functions for generating and solving the ''' -from typing import Tuple, Callable +from typing import Tuple, Callable, Any, List, Optional, cast import logging import numpy # type: ignore from numpy import pi, real, trace # type: ignore @@ -109,10 +109,10 @@ try: 'planner_effort': 'FFTW_EXHAUSTIVE', } - def fftn(*args, **kwargs): + def fftn(*args: Any, **kwargs: Any) -> numpy.ndarray: return pyfftw.interfaces.numpy_fft.fftn(*args, **kwargs, **fftw_args) - def ifftn(*args, **kwargs): + def ifftn(*args: Any, **kwargs: Any) -> numpy.ndarray: return pyfftw.interfaces.numpy_fft.ifftn(*args, **kwargs, **fftw_args) except ImportError: @@ -199,7 +199,7 @@ def maxwell_operator(k0: numpy.ndarray, if mu is not None: mu = numpy.stack(mu, 3) - def operator(h: numpy.ndarray): + def operator(h: numpy.ndarray) -> numpy.ndarray: """ Maxwell operator for Bloch eigenmode simulation. @@ -309,11 +309,11 @@ def hmn_2_hxyz(k0: numpy.ndarray, shape = epsilon[0].shape + (1,) _k_mag, m, n = generate_kmn(k0, G_matrix, shape) - def operator(h: numpy.ndarray): + def operator(h: numpy.ndarray) -> fdfield_t: hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] h_xyz = (m * hin_m + n * hin_n) - return [ifftn(hi) for hi in numpy.rollaxis(h_xyz, 3)] + return numpy.array([ifftn(hi) for hi in numpy.rollaxis(h_xyz, 3)]) return operator @@ -351,7 +351,7 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, if mu is not None: mu = numpy.stack(mu, 3) - def operator(h: numpy.ndarray): + def operator(h: numpy.ndarray) -> numpy.ndarray: """ Approximate inverse Maxwell operator for Bloch eigenmode simulation. @@ -429,7 +429,7 @@ def find_k(frequency: float, direction = numpy.array(direction) / norm(direction) - def get_f(k0_mag: float, band: int = 0): + def get_f(k0_mag: float, band: int = 0) -> numpy.ndarray: k0 = direction * k0_mag n, v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) f = numpy.sqrt(numpy.abs(numpy.real(n[band]))) @@ -552,12 +552,12 @@ def eigsolve(num_modes: int, symZtD = _symmetrize(Z.conj().T @ D) symZtAD = _symmetrize(Z.conj().T @ AD) - Qi_memo = [None, None] + Qi_memo: List[Optional[float]] = [None, None] - def Qi_func(theta): + def Qi_func(theta: float) -> float: nonlocal Qi_memo if Qi_memo[0] == theta: - return Qi_memo[1] + return cast(float, Qi_memo[1]) c = numpy.cos(theta) s = numpy.sin(theta) @@ -579,7 +579,7 @@ def eigsolve(num_modes: int, Qi_memo[1] = Qi return Qi - def trace_func(theta): + def trace_func(theta: float) -> float: c = numpy.cos(theta) s = numpy.sin(theta) Qi = Qi_func(theta) @@ -685,9 +685,9 @@ def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_to return x, fx, dfx ''' -def _rtrace_AtB(A, B): +def _rtrace_AtB(A: numpy.ndarray, B: numpy.ndarray) -> numpy.ndarray: return real(numpy.sum(A.conj() * B)) -def _symmetrize(A): +def _symmetrize(A: numpy.ndarray) -> numpy.ndarray: return (A + A.conj().T) * 0.5 diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index 488d58e..92ec8e9 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -36,13 +36,13 @@ def e_full(omega: complex, ch = curl_back(dxes[1]) ce = curl_forward(dxes[0]) - def op_1(e): + def op_1(e: fdfield_t) -> fdfield_t: curls = ch(ce(e)) - return curls - omega ** 2 * epsilon * e + return curls - omega ** 2 * epsilon * e # type: ignore # issues with numpy/mypy - def op_mu(e): + def op_mu(e: fdfield_t) -> fdfield_t: curls = ch(mu * ce(e)) - return curls - omega ** 2 * epsilon * e + return curls - omega ** 2 * epsilon * e # type: ignore # issues with numpy/mypy if numpy.any(numpy.equal(mu, None)): return op_1 @@ -72,13 +72,13 @@ def eh_full(omega: complex, ch = curl_back(dxes[1]) ce = curl_forward(dxes[0]) - def op_1(e, h): + def op_1(e: fdfield_t, h: fdfield_t) -> Tuple[fdfield_t, fdfield_t]: return (ch(h) - 1j * omega * epsilon * e, - ce(e) + 1j * omega * h) + ce(e) + 1j * omega * h) # type: ignore # issues with numpy/mypy - def op_mu(e, h): + def op_mu(e: fdfield_t, h: fdfield_t) -> Tuple[fdfield_t, fdfield_t]: return (ch(h) - 1j * omega * epsilon * e, - ce(e) + 1j * omega * mu * h) + ce(e) + 1j * omega * mu * h) # type: ignore # issues with numpy/mypy if numpy.any(numpy.equal(mu, None)): return op_1 @@ -105,11 +105,11 @@ def e2h(omega: complex, """ ce = curl_forward(dxes[0]) - def e2h_1_1(e): - return ce(e) / (-1j * omega) + def e2h_1_1(e: fdfield_t) -> fdfield_t: + return ce(e) / (-1j * omega) # type: ignore # issues with numpy/mypy - def e2h_mu(e): - return ce(e) / (-1j * omega * mu) + def e2h_mu(e: fdfield_t) -> fdfield_t: + return ce(e) / (-1j * omega * mu) # type: ignore # issues with numpy/mypy if numpy.any(numpy.equal(mu, None)): return e2h_1_1 @@ -137,13 +137,13 @@ def m2j(omega: complex, """ ch = curl_back(dxes[1]) - def m2j_mu(m): + def m2j_mu(m: fdfield_t) -> fdfield_t: J = ch(m / mu) / (-1j * omega) - return J + return J # type: ignore # issues with numpy/mypy - def m2j_1(m): + def m2j_1(m: fdfield_t) -> fdfield_t: J = ch(m) / (-1j * omega) - return J + return J # type: ignore # issues with numpy/mypy if numpy.any(numpy.equal(mu, None)): return m2j_1 @@ -177,7 +177,7 @@ def e_tfsf_source(TF_region: fdfield_t, # TODO documentation A = e_full(omega, dxes, epsilon, mu) - def op(e): + def op(e: fdfield_t) -> fdfield_t: neg_iwj = A(TF_region * e) - TF_region * A(e) return neg_iwj / (-1j * omega) return op @@ -205,7 +205,7 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[fdfield_t, fdfield_t], fdf Returns: Function `f` that returns E x H as required for the poynting vector. """ - def exh(e: fdfield_t, h: fdfield_t): + def exh(e: fdfield_t, h: fdfield_t) -> fdfield_t: s = numpy.empty_like(e) ex = e[0] * dxes[0][0][:, None, None] ey = e[1] * dxes[0][1][None, :, None] diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index ef2fd57..370b7a2 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -416,12 +416,13 @@ def e_boundary_source(mask: vfdfield_t, shape = [len(dxe) for dxe in dxes[0]] jmask = numpy.zeros_like(mask, dtype=bool) - if periodic_mask_edges: - def shift(axis, polarity): - return rotation(axis=axis, shape=shape, shift_distance=polarity) - else: - def shift(axis, polarity): - return shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity) + def shift_rot(axis: int, polarity: int) -> sparse.spmatrix: + return rotation(axis=axis, shape=shape, shift_distance=polarity) + + def shift_mir(axis: int, polarity: int) -> sparse.spmatrix: + return shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity) + + shift = shift_rot if periodic_mask_edges else shift_mir for axis in (0, 1, 2): if shape[axis] == 1: diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py index 00587f7..67d58ca 100644 --- a/meanas/fdfd/scpml.py +++ b/meanas/fdfd/scpml.py @@ -2,11 +2,9 @@ Functions for creating stretched coordinate perfectly matched layer (PML) absorbers. """ -from typing import Sequence, Union, Callable, Optional +from typing import Sequence, Union, Callable, Optional, List import numpy # type: ignore -from ..fdmath import dx_lists_t, dx_lists_mut - __author__ = 'Jan Petykiewicz' @@ -42,7 +40,7 @@ def uniform_grid_scpml(shape: Union[numpy.ndarray, Sequence[int]], omega: float, epsilon_effective: float = 1.0, s_function: Optional[s_function_t] = None, - ) -> dx_lists_mut: + ) -> List[List[numpy.ndarray]]: """ Create dx arrays for a uniform grid with a cell width of 1 and a pml. @@ -69,7 +67,7 @@ def uniform_grid_scpml(shape: Union[numpy.ndarray, Sequence[int]], s_function = prepare_s_function() # Normalized distance to nearest boundary - def ll(u, n, t): + def ll(u: numpy.ndarray, n: numpy.ndarray, t: numpy.ndarray) -> numpy.ndarray: return ((t - u).clip(0) + (u - (n - t)).clip(0)) / t dx_a = [numpy.array(numpy.inf)] * 3 @@ -90,14 +88,14 @@ def uniform_grid_scpml(shape: Union[numpy.ndarray, Sequence[int]], return [dx_a, dx_b] -def stretch_with_scpml(dxes: dx_lists_mut, +def stretch_with_scpml(dxes: List[List[numpy.ndarray]], axis: int, polarity: int, omega: float, epsilon_effective: float = 1.0, thickness: int = 10, s_function: Optional[s_function_t] = None, - ) -> dx_lists_t: + ) -> List[List[numpy.ndarray]]: """ Stretch dxes to contain a stretched-coordinate PML (SCPML) in one direction along one axis. @@ -134,7 +132,7 @@ def stretch_with_scpml(dxes: dx_lists_mut, bound = pos[thickness] d = bound - pos[0] - def l_d(x): + def l_d(x: numpy.ndarray) -> numpy.ndarray: return (bound - x) / (bound - pos[0]) slc = slice(thickness) @@ -144,7 +142,7 @@ def stretch_with_scpml(dxes: dx_lists_mut, bound = pos[-thickness - 1] d = pos[-1] - bound - def l_d(x): + def l_d(x: numpy.ndarray) -> numpy.ndarray: return (x - bound) / (pos[-1] - bound) if thickness == 0: diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py index 73548ca..a8a423a 100644 --- a/meanas/fdfd/solvers.py +++ b/meanas/fdfd/solvers.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) def _scipy_qmr(A: scipy.sparse.csr_matrix, b: numpy.ndarray, - **kwargs + **kwargs: Any, ) -> numpy.ndarray: """ Wrapper for scipy.sparse.linalg.qmr @@ -37,14 +37,14 @@ def _scipy_qmr(A: scipy.sparse.csr_matrix, ''' ii = 0 - def log_residual(xk): + def log_residual(xk: numpy.ndarray) -> None: nonlocal ii ii += 1 if ii % 100 == 0: logger.info('Solver residual at iteration {} : {}'.format(ii, norm(A @ xk - b))) if 'callback' in kwargs: - def augmented_callback(xk): + def augmented_callback(xk: numpy.ndarray) -> None: log_residual(xk) kwargs['callback'](xk) diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index e5c3775..c7d8148 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -146,7 +146,7 @@ to account for numerical dispersion if the result is introduced into a space wit """ # TODO update module docs -from typing import List, Tuple, Optional +from typing import List, Tuple, Optional, Any import numpy # type: ignore from numpy.linalg import norm # type: ignore import scipy.sparse as sparse # type: ignore @@ -721,8 +721,8 @@ def solve_modes(mode_numbers: List[int], def solve_mode(mode_number: int, - *args, - **kwargs + *args: Any, + **kwargs: Any, ) -> Tuple[vfdfield_t, complex]: """ Wrapper around `solve_modes()` that solves for a single mode. diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py index 72fbf5d..2ac4e7a 100644 --- a/meanas/fdmath/operators.py +++ b/meanas/fdmath/operators.py @@ -67,7 +67,7 @@ def shift_with_mirror(axis: int, shape: Sequence[int], shift_distance: int = 1) raise Exception('Shift ({}) is too large for axis {} of size {}'.format( shift_distance, axis, shape[axis])) - def mirrored_range(n, s): + def mirrored_range(n: int, s: int) -> numpy.ndarray: v = numpy.arange(n) + s v = numpy.where(v >= n, 2 * n - v - 1, v) v = numpy.where(v < 0, - 1 - v, v) @@ -103,7 +103,7 @@ def deriv_forward(dx_e: Sequence[numpy.ndarray]) -> List[sparse.spmatrix]: dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij') - def deriv(axis): + def deriv(axis: int) -> sparse.spmatrix: return rotation(axis, shape, 1) - sparse.eye(n) Ds = [sparse.diags(+1 / dx.ravel(order='C')) @ deriv(a) @@ -128,7 +128,7 @@ def deriv_back(dx_h: Sequence[numpy.ndarray]) -> List[sparse.spmatrix]: dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij') - def deriv(axis): + def deriv(axis: int) -> sparse.spmatrix: return rotation(axis, shape, -1) - sparse.eye(n) Ds = [sparse.diags(-1 / dx.ravel(order='C')) @ deriv(a) diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py index 1a7e1bd..92e215f 100644 --- a/meanas/fdtd/__init__.py +++ b/meanas/fdtd/__init__.py @@ -130,7 +130,7 @@ $$ \\end{aligned} $$ -This result is exact an should practically hold to within numerical precision. No time- +This result is exact and should practically hold to within numerical precision. No time- or spatial-averaging is necessary. Note that each value of $J$ contributes to the energy twice (i.e. once per field update) diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py index 8cf0a25..d03a976 100644 --- a/meanas/fdtd/boundaries.py +++ b/meanas/fdtd/boundaries.py @@ -24,13 +24,13 @@ def conducting_boundary(direction: int, boundary_slice[direction] = 0 shifted1_slice[direction] = 1 - def en(e: fdfield_t): + def en(e: fdfield_t) -> fdfield_t: e[direction][boundary_slice] = 0 e[u][boundary_slice] = e[u][shifted1_slice] e[v][boundary_slice] = e[v][shifted1_slice] return e - def hn(h: fdfield_t): + def hn(h: fdfield_t) -> fdfield_t: h[direction][boundary_slice] = h[direction][shifted1_slice] h[u][boundary_slice] = 0 h[v][boundary_slice] = 0 @@ -46,14 +46,14 @@ def conducting_boundary(direction: int, shifted1_slice[direction] = -2 shifted2_slice[direction] = -3 - def ep(e: fdfield_t): + def ep(e: fdfield_t) -> fdfield_t: e[direction][boundary_slice] = -e[direction][shifted2_slice] e[direction][shifted1_slice] = 0 e[u][boundary_slice] = e[u][shifted1_slice] e[v][boundary_slice] = e[v][shifted1_slice] return e - def hp(h: fdfield_t): + def hp(h: fdfield_t) -> fdfield_t: h[direction][boundary_slice] = h[direction][shifted1_slice] h[u][boundary_slice] = -h[u][shifted2_slice] h[u][shifted1_slice] = 0 diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index b8aa8dc..121c4f6 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -5,6 +5,9 @@ from ..fdmath import dx_lists_t, fdfield_t from ..fdmath.functional import deriv_back +# TODO documentation + + def poynting(e: fdfield_t, h: fdfield_t, dxes: Optional[dx_lists_t] = None, diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py index 91e7f12..e1c9668 100644 --- a/meanas/fdtd/pml.py +++ b/meanas/fdtd/pml.py @@ -63,7 +63,7 @@ def cpml(direction: int, expand_slice_l[direction] = slice(None) expand_slice = tuple(expand_slice_l) - def par(x): + def par(x: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]: scaling = (x / thickness) ** m sigma = scaling * sigma_max kappa = 1 + scaling * (kappa_max - 1) diff --git a/meanas/test/conftest.py b/meanas/test/conftest.py index 3514087..932a62c 100644 --- a/meanas/test/conftest.py +++ b/meanas/test/conftest.py @@ -3,6 +3,7 @@ Test fixtures """ +from typing import Tuple, Iterable, List import numpy # type: ignore import pytest # type: ignore @@ -14,22 +15,26 @@ from .utils import PRNG (5, 5, 5), # (7, 7, 7), ]) -def shape(request): +def shape(request: pytest.FixtureRequest) -> Iterable[Tuple[int, ...]]: yield (3, *request.param) @pytest.fixture(scope='module', params=[1.0, 1.5]) -def epsilon_bg(request): +def epsilon_bg(request: pytest.FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(scope='module', params=[1.0, 2.5]) -def epsilon_fg(request): +def epsilon_fg(request: pytest.FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(scope='module', params=['center', '000', 'random']) -def epsilon(request, shape, epsilon_bg, epsilon_fg): +def epsilon(request: pytest.FixtureRequest, + shape: Tuple[int, ...], + epsilon_bg: float, + epsilon_fg: float, + ) -> Iterable[numpy.ndarray]: is3d = (numpy.array(shape) == 1).sum() == 0 if is3d: if request.param == '000': @@ -53,17 +58,20 @@ def epsilon(request, shape, epsilon_bg, epsilon_fg): @pytest.fixture(scope='module', params=[1.0]) # 1.5 -def j_mag(request): +def j_mag(request: pytest.FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(scope='module', params=[1.0, 1.5]) -def dx(request): +def dx(request: pytest.FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(scope='module', params=['uniform', 'centerbig']) -def dxes(request, shape, dx): +def dxes(request: pytest.FixtureRequest, + shape: Tuple[int, ...], + dx: float, + ) -> Iterable[List[List[numpy.ndarray]]]: if request.param == 'uniform': dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] elif request.param == 'centerbig': diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index c6b3c02..ac84213 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Tuple, Iterable, Optional import dataclasses import pytest # type: ignore import numpy # type: ignore @@ -9,14 +9,14 @@ from ..fdmath import vec, unvec from .utils import assert_close # , assert_fields_close -def test_residual(sim): +def test_residual(sim: 'FDResult') -> None: A = fdfd.operators.e_full(sim.omega, sim.dxes, vec(sim.epsilon)).tocsr() b = -1j * sim.omega * vec(sim.j) residual = A @ vec(sim.e) - b assert numpy.linalg.norm(residual) < 1e-10 -def test_poynting_planes(sim): +def test_poynting_planes(sim: 'FDResult') -> None: mask = (sim.j != 0).any(axis=0) if mask.sum() != 2: pytest.skip(f'test_poynting_planes will only test 2-point sources, got {mask.sum()}') @@ -53,17 +53,17 @@ def test_poynting_planes(sim): # Also see conftest.py @pytest.fixture(params=[1 / 1500]) -def omega(request): +def omega(request: pytest.FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(params=[None]) -def pec(request): +def pec(request: pytest.FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: yield request.param @pytest.fixture(params=[None]) -def pmc(request): +def pmc(request: pytest.FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: yield request.param @@ -74,7 +74,10 @@ def pmc(request): @pytest.fixture(params=['diag']) # 'center' -def j_distribution(request, shape, j_mag): +def j_distribution(request: pytest.FixtureRequest, + shape: Tuple[int, ...], + j_mag: float, + ) -> Iterable[numpy.ndarray]: j = numpy.zeros(shape, dtype=complex) center_mask = numpy.zeros(shape, dtype=bool) center_mask[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = True @@ -89,7 +92,7 @@ def j_distribution(request, shape, j_mag): @dataclasses.dataclass() class FDResult: - shape: Tuple[int] + shape: Tuple[int, ...] dxes: List[List[numpy.ndarray]] epsilon: numpy.ndarray omega: complex @@ -100,7 +103,15 @@ class FDResult: @pytest.fixture() -def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): +def sim(request: pytest.FixtureRequest, + shape: Tuple[int, ...], + epsilon: numpy.ndarray, + dxes: List[List[numpy.ndarray]], + j_distribution: numpy.ndarray, + omega: float, + pec: Optional[numpy.ndarray], + pmc: Optional[numpy.ndarray], + ) -> FDResult: """ Build simulation from parts """ diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py index 436aa39..ac57750 100644 --- a/meanas/test/test_fdfd_pml.py +++ b/meanas/test/test_fdfd_pml.py @@ -1,15 +1,15 @@ -##################################### +from typing import Optional, Tuple, Iterable, List import pytest # type: ignore import numpy # type: ignore from numpy.testing import assert_allclose # type: ignore from .. import fdfd -from ..fdmath import vec, unvec +from ..fdmath import vec, unvec, dx_lists_mut #from .utils import assert_close, assert_fields_close from .test_fdfd import FDResult -def test_pml(sim, src_polarity): +def test_pml(sim: FDResult, src_polarity: int) -> None: e_sqr = numpy.squeeze((sim.e.conj() * sim.e).sum(axis=0)) # from matplotlib import pyplot @@ -42,34 +42,40 @@ def test_pml(sim, src_polarity): # Also see conftest.py @pytest.fixture(params=[1 / 1500]) -def omega(request): +def omega(request: pytest.FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(params=[None]) -def pec(request): +def pec(request: pytest.FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: yield request.param @pytest.fixture(params=[None]) -def pmc(request): +def pmc(request: pytest.FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: yield request.param @pytest.fixture(params=[(30, 1, 1), (1, 30, 1), (1, 1, 30)]) -def shape(request): +def shape(request: pytest.FixtureRequest) -> Iterable[Tuple[int, ...]]: yield (3, *request.param) @pytest.fixture(params=[+1, -1]) -def src_polarity(request): +def src_polarity(request: pytest.FixtureRequest) -> Iterable[int]: yield request.param @pytest.fixture() -def j_distribution(request, shape, epsilon, dxes, omega, src_polarity): +def j_distribution(request: pytest.FixtureRequest, + shape: Tuple[int, ...], + epsilon: numpy.ndarray, + dxes: dx_lists_mut, + omega: float, + src_polarity: int, + ) -> Iterable[numpy.ndarray]: j = numpy.zeros(shape, dtype=complex) dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis @@ -101,13 +107,22 @@ def j_distribution(request, shape, epsilon, dxes, omega, src_polarity): @pytest.fixture() -def epsilon(request, shape, epsilon_bg, epsilon_fg): +def epsilon(request: pytest.FixtureRequest, + shape: Tuple[int, ...], + epsilon_bg: float, + epsilon_fg: float, + ) -> Iterable[numpy.ndarray]: epsilon = numpy.full(shape, epsilon_fg, dtype=float) yield epsilon @pytest.fixture(params=['uniform']) -def dxes(request, shape, dx, omega, epsilon_fg): +def dxes(request: pytest.FixtureRequest, + shape: Tuple[int, ...], + dx: float, + omega: float, + epsilon_fg: float, + ) -> Iterable[List[List[numpy.ndarray]]]: if request.param == 'uniform': dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis @@ -120,7 +135,15 @@ def dxes(request, shape, dx, omega, epsilon_fg): @pytest.fixture() -def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): +def sim(request: pytest.FixtureRequest, + shape: Tuple[int, ...], + epsilon: numpy.ndarray, + dxes: dx_lists_mut, + j_distribution: numpy.ndarray, + omega: float, + pec: Optional[numpy.ndarray], + pmc: Optional[numpy.ndarray], + ) -> FDResult: j_vec = vec(j_distribution) eps_vec = vec(epsilon) e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec, @@ -129,7 +152,7 @@ def sim(request, shape, epsilon, dxes, j_distribution, omega, pec, pmc): sim = FDResult( shape=shape, - dxes=dxes, + dxes=[list(d) for d in dxes], epsilon=epsilon, j=j_distribution, e=e, diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index efeb3e9..56fa553 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Tuple, Iterable import dataclasses import pytest # type: ignore import numpy # type: ignore @@ -8,7 +8,7 @@ from .. import fdtd from .utils import assert_close, assert_fields_close, PRNG -def test_initial_fields(sim): +def test_initial_fields(sim: 'TDResult') -> None: # Make sure initial fields didn't change e0 = sim.es[0] h0 = sim.hs[0] @@ -20,7 +20,7 @@ def test_initial_fields(sim): assert not h0.any() -def test_initial_energy(sim): +def test_initial_energy(sim: 'TDResult') -> None: """ Assumes fields start at 0 before J0 is added """ @@ -41,7 +41,7 @@ def test_initial_energy(sim): assert_fields_close(e0_dot_j0, u0) -def test_energy_conservation(sim): +def test_energy_conservation(sim: 'TDResult') -> None: """ Assumes fields start at 0 before J0 is added """ @@ -63,7 +63,7 @@ def test_energy_conservation(sim): assert_close(u_estep.sum(), u) -def test_poynting_divergence(sim): +def test_poynting_divergence(sim: 'TDResult') -> None: args = {'dxes': sim.dxes, 'epsilon': sim.epsilon} @@ -90,7 +90,7 @@ def test_poynting_divergence(sim): u_eprev = u_estep -def test_poynting_planes(sim): +def test_poynting_planes(sim: 'TDResult') -> None: mask = (sim.js[0] != 0).any(axis=0) if mask.sum() > 1: pytest.skip('test_poynting_planes can only test single point sources, got {}'.format(mask.sum())) @@ -140,30 +140,33 @@ def test_poynting_planes(sim): @pytest.fixture(params=[0.3]) -def dt(request): +def dt(request: pytest.FixtureRequest) -> Iterable[float]: yield request.param @dataclasses.dataclass() class TDResult: - shape: Tuple[int] + shape: Tuple[int, ...] dt: float dxes: List[List[numpy.ndarray]] epsilon: numpy.ndarray j_distribution: numpy.ndarray - j_steps: Tuple[int] + j_steps: Tuple[int, ...] es: List[numpy.ndarray] = dataclasses.field(default_factory=list) hs: List[numpy.ndarray] = dataclasses.field(default_factory=list) js: List[numpy.ndarray] = dataclasses.field(default_factory=list) @pytest.fixture(params=[(0, 4, 8)]) # (0,) -def j_steps(request): +def j_steps(request: pytest.fixtureRequest) -> Iterable[Tuple[int, ...]]: yield request.param @pytest.fixture(params=['center', 'random']) -def j_distribution(request, shape, j_mag): +def j_distribution(request: pytest.FixtureRequest, + shape: Tuple[int, ...], + j_mag: float, + ) -> Iterable[numpy.ndarray]: j = numpy.zeros(shape) if request.param == 'center': j[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = j_mag @@ -175,7 +178,14 @@ def j_distribution(request, shape, j_mag): @pytest.fixture() -def sim(request, shape, epsilon, dxes, dt, j_distribution, j_steps): +def sim(request: pytest.FixtureRequest, + shape: Tuple[int, ...], + epsilon: numpy.ndarray, + dxes: List[List[numpy.ndarray]], + dt: float, + j_distribution: numpy.ndarray, + j_steps: Tuple[int, ...], + ) -> TDResult: is3d = (numpy.array(shape) == 1).sum() == 0 if is3d: if dt != 0.3: diff --git a/meanas/test/utils.py b/meanas/test/utils.py index a49bc04..7c8c372 100644 --- a/meanas/test/utils.py +++ b/meanas/test/utils.py @@ -1,13 +1,22 @@ +from typing import Any import numpy # type: ignore PRNG = numpy.random.RandomState(12345) -def assert_fields_close(x, y, *args, **kwargs): +def assert_fields_close(x: numpy.ndarray, + y: numpy.ndarray, + *args: Any, + **kwargs: Any, + ) -> None: numpy.testing.assert_allclose( x, y, verbose=False, err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(x, -1), numpy.rollaxis(y, -1)), *args, **kwargs) -def assert_close(x, y, *args, **kwargs): +def assert_close(x: numpy.ndarray, + y: numpy.ndarray, + *args: Any, + **kwargs: Any, + ) -> None: numpy.testing.assert_allclose(x, y, *args, **kwargs) From db23cc1d6f438ed3902349af0e420585d43bcf51 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 17 Oct 2020 17:48:58 -0700 Subject: [PATCH 239/437] style and typing fixes in tests --- meanas/test/conftest.py | 20 ++++++++++++-------- meanas/test/test_fdfd.py | 11 ++++++----- meanas/test/test_fdfd_pml.py | 19 ++++++++++--------- meanas/test/test_fdtd.py | 9 +++++---- meanas/test/utils.py | 2 ++ 5 files changed, 35 insertions(+), 26 deletions(-) diff --git a/meanas/test/conftest.py b/meanas/test/conftest.py index 932a62c..311aae3 100644 --- a/meanas/test/conftest.py +++ b/meanas/test/conftest.py @@ -3,34 +3,38 @@ Test fixtures """ -from typing import Tuple, Iterable, List +from typing import Tuple, Iterable, List, Any import numpy # type: ignore import pytest # type: ignore from .utils import PRNG + +FixtureRequest = Any + + @pytest.fixture(scope='module', params=[(5, 5, 1), (5, 1, 5), (5, 5, 5), # (7, 7, 7), ]) -def shape(request: pytest.FixtureRequest) -> Iterable[Tuple[int, ...]]: +def shape(request: FixtureRequest) -> Iterable[Tuple[int, ...]]: yield (3, *request.param) @pytest.fixture(scope='module', params=[1.0, 1.5]) -def epsilon_bg(request: pytest.FixtureRequest) -> Iterable[float]: +def epsilon_bg(request: FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(scope='module', params=[1.0, 2.5]) -def epsilon_fg(request: pytest.FixtureRequest) -> Iterable[float]: +def epsilon_fg(request: FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(scope='module', params=['center', '000', 'random']) -def epsilon(request: pytest.FixtureRequest, +def epsilon(request: FixtureRequest, shape: Tuple[int, ...], epsilon_bg: float, epsilon_fg: float, @@ -58,17 +62,17 @@ def epsilon(request: pytest.FixtureRequest, @pytest.fixture(scope='module', params=[1.0]) # 1.5 -def j_mag(request: pytest.FixtureRequest) -> Iterable[float]: +def j_mag(request: FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(scope='module', params=[1.0, 1.5]) -def dx(request: pytest.FixtureRequest) -> Iterable[float]: +def dx(request: FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(scope='module', params=['uniform', 'centerbig']) -def dxes(request: pytest.FixtureRequest, +def dxes(request: FixtureRequest, shape: Tuple[int, ...], dx: float, ) -> Iterable[List[List[numpy.ndarray]]]: diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index ac84213..076cb52 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -7,6 +7,7 @@ import numpy # type: ignore from .. import fdfd from ..fdmath import vec, unvec from .utils import assert_close # , assert_fields_close +from .conftest import FixtureRequest def test_residual(sim: 'FDResult') -> None: @@ -53,17 +54,17 @@ def test_poynting_planes(sim: 'FDResult') -> None: # Also see conftest.py @pytest.fixture(params=[1 / 1500]) -def omega(request: pytest.FixtureRequest) -> Iterable[float]: +def omega(request: FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(params=[None]) -def pec(request: pytest.FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: +def pec(request: FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: yield request.param @pytest.fixture(params=[None]) -def pmc(request: pytest.FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: +def pmc(request: FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: yield request.param @@ -74,7 +75,7 @@ def pmc(request: pytest.FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: @pytest.fixture(params=['diag']) # 'center' -def j_distribution(request: pytest.FixtureRequest, +def j_distribution(request: FixtureRequest, shape: Tuple[int, ...], j_mag: float, ) -> Iterable[numpy.ndarray]: @@ -103,7 +104,7 @@ class FDResult: @pytest.fixture() -def sim(request: pytest.FixtureRequest, +def sim(request: FixtureRequest, shape: Tuple[int, ...], epsilon: numpy.ndarray, dxes: List[List[numpy.ndarray]], diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py index ac57750..cf9c05a 100644 --- a/meanas/test/test_fdfd_pml.py +++ b/meanas/test/test_fdfd_pml.py @@ -7,6 +7,7 @@ from .. import fdfd from ..fdmath import vec, unvec, dx_lists_mut #from .utils import assert_close, assert_fields_close from .test_fdfd import FDResult +from .conftest import FixtureRequest def test_pml(sim: FDResult, src_polarity: int) -> None: @@ -42,34 +43,34 @@ def test_pml(sim: FDResult, src_polarity: int) -> None: # Also see conftest.py @pytest.fixture(params=[1 / 1500]) -def omega(request: pytest.FixtureRequest) -> Iterable[float]: +def omega(request: FixtureRequest) -> Iterable[float]: yield request.param @pytest.fixture(params=[None]) -def pec(request: pytest.FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: +def pec(request: FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: yield request.param @pytest.fixture(params=[None]) -def pmc(request: pytest.FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: +def pmc(request: FixtureRequest) -> Iterable[Optional[numpy.ndarray]]: yield request.param @pytest.fixture(params=[(30, 1, 1), (1, 30, 1), (1, 1, 30)]) -def shape(request: pytest.FixtureRequest) -> Iterable[Tuple[int, ...]]: +def shape(request: FixtureRequest) -> Iterable[Tuple[int, ...]]: yield (3, *request.param) @pytest.fixture(params=[+1, -1]) -def src_polarity(request: pytest.FixtureRequest) -> Iterable[int]: +def src_polarity(request: FixtureRequest) -> Iterable[int]: yield request.param @pytest.fixture() -def j_distribution(request: pytest.FixtureRequest, +def j_distribution(request: FixtureRequest, shape: Tuple[int, ...], epsilon: numpy.ndarray, dxes: dx_lists_mut, @@ -107,7 +108,7 @@ def j_distribution(request: pytest.FixtureRequest, @pytest.fixture() -def epsilon(request: pytest.FixtureRequest, +def epsilon(request: FixtureRequest, shape: Tuple[int, ...], epsilon_bg: float, epsilon_fg: float, @@ -117,7 +118,7 @@ def epsilon(request: pytest.FixtureRequest, @pytest.fixture(params=['uniform']) -def dxes(request: pytest.FixtureRequest, +def dxes(request: FixtureRequest, shape: Tuple[int, ...], dx: float, omega: float, @@ -135,7 +136,7 @@ def dxes(request: pytest.FixtureRequest, @pytest.fixture() -def sim(request: pytest.FixtureRequest, +def sim(request: FixtureRequest, shape: Tuple[int, ...], epsilon: numpy.ndarray, dxes: dx_lists_mut, diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 56fa553..8f5e013 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -6,6 +6,7 @@ import numpy # type: ignore from .. import fdtd from .utils import assert_close, assert_fields_close, PRNG +from .conftest import FixtureRequest def test_initial_fields(sim: 'TDResult') -> None: @@ -140,7 +141,7 @@ def test_poynting_planes(sim: 'TDResult') -> None: @pytest.fixture(params=[0.3]) -def dt(request: pytest.FixtureRequest) -> Iterable[float]: +def dt(request: FixtureRequest) -> Iterable[float]: yield request.param @@ -158,12 +159,12 @@ class TDResult: @pytest.fixture(params=[(0, 4, 8)]) # (0,) -def j_steps(request: pytest.fixtureRequest) -> Iterable[Tuple[int, ...]]: +def j_steps(request: FixtureRequest) -> Iterable[Tuple[int, ...]]: yield request.param @pytest.fixture(params=['center', 'random']) -def j_distribution(request: pytest.FixtureRequest, +def j_distribution(request: FixtureRequest, shape: Tuple[int, ...], j_mag: float, ) -> Iterable[numpy.ndarray]: @@ -178,7 +179,7 @@ def j_distribution(request: pytest.FixtureRequest, @pytest.fixture() -def sim(request: pytest.FixtureRequest, +def sim(request: FixtureRequest, shape: Tuple[int, ...], epsilon: numpy.ndarray, dxes: List[List[numpy.ndarray]], diff --git a/meanas/test/utils.py b/meanas/test/utils.py index 7c8c372..f76b910 100644 --- a/meanas/test/utils.py +++ b/meanas/test/utils.py @@ -1,8 +1,10 @@ from typing import Any import numpy # type: ignore + PRNG = numpy.random.RandomState(12345) + def assert_fields_close(x: numpy.ndarray, y: numpy.ndarray, *args: Any, From f4afb062105e432994187e69d20859c3e27aa6fa Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 3 Nov 2020 01:12:27 -0800 Subject: [PATCH 240/437] avoid dependencies on package_data __doc__ is handled poorly here; consider an alternate approach --- meanas/VERSION | 1 - meanas/VERSION.py | 4 ++++ meanas/__init__.py | 11 +++++++---- setup.py | 7 ++++--- 4 files changed, 15 insertions(+), 8 deletions(-) delete mode 100644 meanas/VERSION create mode 100644 meanas/VERSION.py diff --git a/meanas/VERSION b/meanas/VERSION deleted file mode 100644 index 5a2a580..0000000 --- a/meanas/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.6 diff --git a/meanas/VERSION.py b/meanas/VERSION.py new file mode 100644 index 0000000..ba887e5 --- /dev/null +++ b/meanas/VERSION.py @@ -0,0 +1,4 @@ +""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ +__version__ = ''' +0.6 +''' diff --git a/meanas/__init__.py b/meanas/__init__.py index 770bdf4..8b9b300 100644 --- a/meanas/__init__.py +++ b/meanas/__init__.py @@ -6,11 +6,14 @@ See the readme or `import meanas; help(meanas)` for more info. import pathlib +from .VERSION import __version__ + __author__ = 'Jan Petykiewicz' -with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f: - __version__ = f.read().strip() -with open(pathlib.Path(__file__).parent.parent / 'README.md', 'r') as f: - __doc__ = f.read() +try: + with open(pathlib.Path(__file__).parent.parent / 'README.md', 'r') as f: + __doc__ = f.read() +except Exception: + pass diff --git a/setup.py b/setup.py index 8438f8f..e316f82 100644 --- a/setup.py +++ b/setup.py @@ -2,11 +2,12 @@ from setuptools import setup, find_packages + with open('README.md', 'r') as f: long_description = f.read() -with open('meanas/VERSION', 'r') as f: - version = f.read().strip() +with open('meanas/VERSION.py', 'rt') as f: + version = f.readlines()[2].strip() setup(name='meanas', version=version, @@ -18,7 +19,7 @@ setup(name='meanas', url='https://mpxd.net/code/jan/meanas', packages=find_packages(), package_data={ - 'meanas': ['VERSION'] + 'meanas': [] }, install_requires=[ 'numpy', From 54bc14f92324b465a6b047a7b909e27a7d7a0691 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 3 Nov 2020 01:13:23 -0800 Subject: [PATCH 241/437] bump version to v0.7 --- meanas/VERSION.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/VERSION.py b/meanas/VERSION.py index ba887e5..9de6fb9 100644 --- a/meanas/VERSION.py +++ b/meanas/VERSION.py @@ -1,4 +1,4 @@ """ VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ __version__ = ''' -0.6 +0.7 ''' From fcd6fc706581a8701926b2bd6e6aea99cc049e17 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 3 Nov 2020 01:15:58 -0800 Subject: [PATCH 242/437] add py.typed to enable downstream typechecking --- meanas/py.typed | 0 setup.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 meanas/py.typed diff --git a/meanas/py.typed b/meanas/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index e316f82..056b1a9 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup(name='meanas', url='https://mpxd.net/code/jan/meanas', packages=find_packages(), package_data={ - 'meanas': [] + 'meanas': ['py.typed'] }, install_requires=[ 'numpy', From 1a754dcbc980c28a93a1d5f733da5e182cdd3e1d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 9 Jan 2021 01:07:12 -0800 Subject: [PATCH 243/437] clarify that `rotation` is a circular shift (rename to `shift_circ`) --- meanas/fdfd/operators.py | 8 ++++---- meanas/fdmath/operators.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 370b7a2..0751f25 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -32,7 +32,7 @@ import numpy # type: ignore import scipy.sparse as sparse # type: ignore from ..fdmath import vec, dx_lists_t, vfdfield_t -from ..fdmath.operators import shift_with_mirror, rotation, curl_forward, curl_back +from ..fdmath.operators import shift_with_mirror, shift_circ, curl_forward, curl_back __author__ = 'Jan Petykiewicz' @@ -316,7 +316,7 @@ def poynting_e_cross(e: vfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ shape = [len(dx) for dx in dxes[0]] - fx, fy, fz = [rotation(i, shape, 1) for i in range(3)] + fx, fy, fz = [shift_circ(i, shape, 1) for i in range(3)] dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] Ex, Ey, Ez = [ei * da for ei, da in zip(numpy.split(e, 3), dxag)] @@ -343,7 +343,7 @@ def poynting_h_cross(h: vfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ shape = [len(dx) for dx in dxes[0]] - fx, fy, fz = [rotation(i, shape, 1) for i in range(3)] + fx, fy, fz = [shift_circ(i, shape, 1) for i in range(3)] dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] @@ -417,7 +417,7 @@ def e_boundary_source(mask: vfdfield_t, jmask = numpy.zeros_like(mask, dtype=bool) def shift_rot(axis: int, polarity: int) -> sparse.spmatrix: - return rotation(axis=axis, shape=shape, shift_distance=polarity) + return shift_circ(axis=axis, shape=shape, shift_distance=polarity) def shift_mir(axis: int, polarity: int) -> sparse.spmatrix: return shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity) diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py index 2ac4e7a..45e0cea 100644 --- a/meanas/fdmath/operators.py +++ b/meanas/fdmath/operators.py @@ -10,7 +10,7 @@ import scipy.sparse as sparse # type: ignore from .types import vfdfield_t -def rotation(axis: int, shape: Sequence[int], shift_distance: int = 1) -> sparse.spmatrix: +def shift_circ(axis: int, shape: Sequence[int], shift_distance: int = 1) -> sparse.spmatrix: """ Utility operator for performing a circular shift along a specified axis by a specified number of elements. @@ -104,7 +104,7 @@ def deriv_forward(dx_e: Sequence[numpy.ndarray]) -> List[sparse.spmatrix]: dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij') def deriv(axis: int) -> sparse.spmatrix: - return rotation(axis, shape, 1) - sparse.eye(n) + return shift_circ(axis, shape, 1) - sparse.eye(n) Ds = [sparse.diags(+1 / dx.ravel(order='C')) @ deriv(a) for a, dx in enumerate(dx_e_expanded)] @@ -129,7 +129,7 @@ def deriv_back(dx_h: Sequence[numpy.ndarray]) -> List[sparse.spmatrix]: dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij') def deriv(axis: int) -> sparse.spmatrix: - return rotation(axis, shape, -1) - sparse.eye(n) + return shift_circ(axis, shape, -1) - sparse.eye(n) Ds = [sparse.diags(-1 / dx.ravel(order='C')) @ deriv(a) for a, dx in enumerate(dx_h_expanded)] @@ -186,7 +186,7 @@ def avg_forward(axis: int, shape: Sequence[int]) -> sparse.spmatrix: raise Exception('Invalid shape: {}'.format(shape)) n = numpy.prod(shape) - return 0.5 * (sparse.eye(n) + rotation(axis, shape)) + return 0.5 * (sparse.eye(n) + shift_circ(axis, shape)) def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix: From 61c85fc654b4aa28b98023e04204c47438c6320f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 9 Jan 2021 01:07:31 -0800 Subject: [PATCH 244/437] remove old VERSION file from manifest --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 41ad357..c28ab72 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,2 @@ include README.md include LICENSE.md -include meanas/VERSION From 8faefff98d53c3dcfdb7b374b725264bf759ef62 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 5 Jul 2021 14:58:14 -0700 Subject: [PATCH 245/437] make vectorization docs point to the right functions --- meanas/fdfd/operators.py | 2 +- meanas/fdfd/solvers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 0751f25..7f0a7ba 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -3,7 +3,7 @@ Sparse matrix operators for use with electromagnetic wave equations. These functions return sparse-matrix (`scipy.sparse.spmatrix`) representations of a variety of operators, intended for use with E and H fields vectorized using the - `meanas.vec()` and `meanas.unvec()` functions. + `meanas.fdmath.vectorization.vec()` and `meanas.fdmath.vectorization.unvec()` functions. E- and H-field values are defined on a Yee cell; `epsilon` values should be calculated for cells centered at each E component (`mu` at each H component). diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py index a8a423a..c9f1ac5 100644 --- a/meanas/fdfd/solvers.py +++ b/meanas/fdfd/solvers.py @@ -74,7 +74,7 @@ def generic(omega: complex, """ Conjugate gradient FDFD solver using CSR sparse matrices. - All ndarray arguments should be 1D arrays, as returned by `meanas.vec()`. + All ndarray arguments should be 1D arrays, as returned by `meanas.fdmath.vectorization.vec()`. Args: omega: Complex frequency to solve at. From 23490694ff4980838727db0f44c1079235336570 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 5 Jul 2021 14:58:39 -0700 Subject: [PATCH 246/437] improve doc generation and fix code links --- make_docs.sh | 10 +++++++--- pdoc_templates/config.mako | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/make_docs.sh b/make_docs.sh index a29558c..c594313 100755 --- a/make_docs.sh +++ b/make_docs.sh @@ -1,4 +1,7 @@ #!/bin/bash + +set -Eeuo pipefail + cd ~/projects/meanas # Approach 1: pdf to html? @@ -6,9 +9,10 @@ cd ~/projects/meanas # pandoc --metadata=title:"meanas" --toc --toc-depth=4 --from=markdown+abbreviations --to=html --output=doc.html --gladtex -s - # Approach 2: pdf to html with gladtex -pdoc3 --pdf --force --template-dir pdoc_templates -o doc . > doc.md -pandoc --metadata=title:"meanas" --from=markdown+abbreviations --to=html --output=doc.html --gladtex -s --css pdoc_templates/pdoc.css doc.md -gladtex -a -n -d _doc_mathimg -c white doc.html +rm -r _doc_mathimg +pdoc --pdf --force --template-dir pdoc_templates -o doc . > doc.md +pandoc --metadata=title:"meanas" --from=markdown+abbreviations --to=html --output=doc.htex --gladtex -s --css pdoc_templates/pdoc.css doc.md +gladtex -a -n -d _doc_mathimg -c white doc.htex # Approach 3: html with gladtex #pdoc3 --html --force --template-dir pdoc_templates -o doc . diff --git a/pdoc_templates/config.mako b/pdoc_templates/config.mako index 3010348..93cf716 100644 --- a/pdoc_templates/config.mako +++ b/pdoc_templates/config.mako @@ -20,7 +20,7 @@ #git_link_template = 'https://bitbucket.org/USER/PROJECT/src/{commit}/{path}#lines-{start_line}:{end_line}' #git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start_line}' #git_link_template = None - git_link_template = 'https://mpxd.net/code/jan/fdfd_tools/src/commit/{commit}/{path}#L{start_line}-L{end_line}' + git_link_template = 'https://mpxd.net/code/jan/meanas/src/commit/{commit}/{path}#L{start_line}-L{end_line}' # A prefix to use for every HTML hyperlink in the generated documentation. # No prefix results in all links being relative. From fcca9e3ae5127499f22ef3d09c39253eef773781 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 5 Jul 2021 15:01:08 -0700 Subject: [PATCH 247/437] Fix waveguide eigenvalue derivation Thanks to Rafael Diaz Fuentes and Paolo Pintus for catching and correcting these! --- meanas/fdfd/waveguide_2d.py | 59 ++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index c7d8148..7943b1f 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -69,8 +69,8 @@ $$ - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (-\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z) \\\\ &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x ( \\imath \\omega \\epsilon_{xx} E_x) - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (-\\imath \\omega \\epsilon_{yy} E_y) \\\\ -\\gamma \\tilde{\\partial}_x E_z &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) - \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\ +\\gamma \\tilde{\\partial}_x E_z &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + + \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\ \\end{aligned} $$ @@ -78,23 +78,32 @@ With a similar approach (but using $\\gamma \\tilde{\\partial}_y$ instead), we c $$ \\begin{aligned} -\\gamma \\tilde{\\partial}_y E_z &= \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) - \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\ +\\gamma \\tilde{\\partial}_y E_z &= \\tilde{\\partial}_y \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + + \\tilde{\\partial}_y \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\ \\end{aligned} $$ We can combine this equation for $\\gamma \\tilde{\\partial}_y E_z$ with -the unused $\\imath \\omega \\mu_{xx} H_z$ and $\\imath \\omega \\mu_{yy} H_y$ equations to get +the unused $\\imath \\omega \\mu_{xx} H_x$ and $\\imath \\omega \\mu_{yy} H_y$ equations to get $$ \\begin{aligned} +-\\imath \\omega \\mu_{xx} \\gamma H_x &= \\gamma^2 E_y + \\gamma \\tilde{\\partial}_y E_z \\\\ -\\imath \\omega \\mu_{xx} \\gamma H_x &= \\gamma^2 E_y + \\tilde{\\partial}_y ( - \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) - + \\tilde{\\partial}_x \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) - ) \\\\ + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y) + )\\\\ +\\end{aligned} +$$ + +and + +$$ +\\begin{aligned} +-\\imath \\omega \\mu_{yy} \\gamma H_y &= -\\gamma^2 E_x - \\gamma \\tilde{\\partial}_x E_z \\\\ -\\imath \\omega \\mu_{yy} \\gamma H_y &= -\\gamma^2 E_x - \\tilde{\\partial}_x ( - \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_x (\\epsilon_{xx} E_x) - + \\tilde{\\partial}_y \\frac{1}{\\epsilon_zz} \\hat{\\partial}_y (\\epsilon_{yy} E_y) + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y) )\\\\ \\end{aligned} $$ @@ -106,10 +115,10 @@ $$ \\begin{aligned} -\\imath \\omega \\mu_{xx} (\\gamma H_x) &= -\\imath \\omega \\mu_{xx} (-\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z) \\\\ &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y - -\\imath \\omega \\mu_{xx} \\hat{\\partial}_x ( + +\\imath \\omega \\mu_{xx} \\hat{\\partial}_x ( \\frac{1}{-\\imath \\omega \\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x)) \\\\ &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y - +\\mu_{xx} \\hat{\\partial}_x \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\ + -\\mu_{xx} \\hat{\\partial}_x \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\ \\end{aligned} $$ @@ -117,12 +126,30 @@ and, similarly, $$ \\begin{aligned} --\\imath \\omega \\mu_{yy} (\\gamma H_y) &= -\\omega^2 \\mu_{yy} \\epsilon_{xx} E_x - +\\mu_{yy} \\hat{\\partial}_y \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\ +-\\imath \\omega \\mu_{yy} (\\gamma H_y) &= \\omega^2 \\mu_{yy} \\epsilon_{xx} E_x + +\\mu_{yy} \\hat{\\partial}_y \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\ +\\end{aligned} +$$ + +By combining both pairs of expressions, we get + +$$ +\\begin{aligned} +-\\gamma^2 E_x - \\tilde{\\partial}_x ( + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y) + ) &= \\omega^2 \\mu_{yy} \\epsilon_{xx} E_x + +\\mu_{yy} \\hat{\\partial}_y \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\ +\\gamma^2 E_y + \\tilde{\\partial}_y ( + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x) + + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y) + ) &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y + -\\mu_{xx} \\hat{\\partial}_x \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\ \\end{aligned} $$ Using these, we can construct the eigenvalue problem + $$ \\beta^2 \\begin{bmatrix} E_x \\\\ E_y \\end{bmatrix} = (\\omega^2 \\begin{bmatrix} \\mu_{yy} \\epsilon_{xx} & 0 \\\\ @@ -137,6 +164,10 @@ $$ \\beta^2 \\begin{bmatrix} E_x \\\\ E_y \\end{bmatrix} $$ +where $\\gamma = \\imath\\beta$. In the literature, $\\beta$ is usually used to denote +the lossless/real part of the propagation constant, but in `meanas` it is allowed to +be complex. + An equivalent eigenvalue problem can be formed using the $H_x$ and $H_y$ fields, if those are more convenient. Note that $E_z$ was never discretized, so $\\gamma$ and $\\beta$ will need adjustment From 39f9de2e55c7777c54a1f1ac678a1493c1d805d8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 11 Jul 2021 17:25:26 -0700 Subject: [PATCH 248/437] strip whitespace from version string --- meanas/VERSION.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/VERSION.py b/meanas/VERSION.py index 9de6fb9..7261d61 100644 --- a/meanas/VERSION.py +++ b/meanas/VERSION.py @@ -1,4 +1,4 @@ """ VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ __version__ = ''' 0.7 -''' +'''.strip() From a6ae3da4b1b1bb7a45b2e7c9dcac9da3dafce6c2 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 11 Jul 2021 17:25:31 -0700 Subject: [PATCH 249/437] update email --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 056b1a9..fc7cbe7 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup(name='meanas', long_description=long_description, long_description_content_type='text/markdown', author='Jan Petykiewicz', - author_email='anewusername@gmail.com', + author_email='jan@mpxd.net', url='https://mpxd.net/code/jan/meanas', packages=find_packages(), package_data={ From 204afb80242b17cc857a42202f00db076f3cc519 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 11 Jul 2021 17:26:03 -0700 Subject: [PATCH 250/437] Return: -> Returns: --- meanas/fdfd/functional.py | 4 ++-- meanas/fdfd/waveguide_2d.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index 92ec8e9..f16151f 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -29,7 +29,7 @@ def e_full(omega: complex, epsilon: Dielectric constant mu: Magnetic permeability (default 1 everywhere) - Return: + Returns: Function `f` implementing the wave operator `f(E)` -> `-i * omega * J` """ @@ -99,7 +99,7 @@ def e2h(omega: complex, dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` mu: Magnetic permeability (default 1 everywhere) - Return: + Returns: Function `f` for converting `E` to `H`, `f(E)` -> `H` """ diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 7943b1f..015c299 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -603,7 +603,7 @@ def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) - Return: + Returns: Sparse matrix representation of the operator. """ n = 1 @@ -623,7 +623,7 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) - Return: + Returns: Sparse matrix representation of the operator. """ n = 1 From 8c8f9f6e69bcaadde2a9e6d0503f7b89751d7111 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 11 Jul 2021 17:26:33 -0700 Subject: [PATCH 251/437] factor out omega**2 terms --- meanas/fdfd/waveguide_cyl.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index b9213ec..da75f0c 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -70,9 +70,11 @@ def cylindrical_operator(omega: complex, b1 = Dby @ Ty @ Dfx diag = sparse.block_diag - op = (omega**2 * diag((Tx, Ty)) + pa) @ diag((a0, a1)) + \ - - (sparse.bmat(((None, Ty), (Tx, None))) + omega**-2 * pb) @ diag((b0, b1)) + omega2 = omega * omega + + op = (omega2 * diag((Tx, Ty)) + pa) @ diag((a0, a1)) + \ + - (sparse.bmat(((None, Ty), (Tx, None))) + pb / omega2) @ diag((b0, b1)) return op From 8ac0d52cd17b3101ea5cff0395e1f77aa94e713a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 11 Jul 2021 17:27:02 -0700 Subject: [PATCH 252/437] Add some docs for energy calculations --- meanas/fdtd/energy.py | 144 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 2 deletions(-) diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index 121c4f6..93eedf0 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -13,7 +13,62 @@ def poynting(e: fdfield_t, dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: """ - Calculate the poynting vector + Calculate the poynting vector `S` ($S$). + + This is the energy transfer rate (amount of energy `U` per `dt` transferred + between adjacent cells) in each direction that happens during the half-step + bounded by the two provided fields. + + The returned vector field `S` is the energy flow across +x, +y, and +z + boundaries of the corresponding `U` cell. For example, + + ``` + mx = numpy.roll(mask, -1, axis=0) + my = numpy.roll(mask, -1, axis=1) + mz = numpy.roll(mask, -1, axis=2) + + u_hstep = fdtd.energy_hstep(e0=es[ii - 1], h1=hs[ii], e2=es[ii], **args) + u_estep = fdtd.energy_estep(h0=hs[ii], e1=es[ii], h2=hs[ii + 1], **args) + delta_j_B = fdtd.delta_energy_j(j0=js[ii], e1=es[ii], dxes=dxes) + du_half_h2e = u_estep - u_hstep - delta_j_B + + s_h2e = -fdtd.poynting(e=es[ii], h=hs[ii], dxes=dxes) * dt + planes = [s_h2e[0, mask].sum(), -s_h2e[0, mx].sum(), + s_h2e[1, mask].sum(), -s_h2e[1, my].sum(), + s_h2e[2, mask].sum(), -s_h2e[2, mz].sum()] + + assert_close(sum(planes), du_half_h2e[mask]) + ``` + + (see `meanas.tests.test_fdtd.test_poynting_planes`) + + The full relationship is + $$ + \\begin{aligned} + (U_{l+\\frac{1}{2}} - U_l) / \\Delta_t + &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l + \\frac{1}{2}} \\ \\ + - \\hat{H}_{l+\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\ + - \\tilde{E}_l \\cdot \\tilde{J}_{l+\\frac{1}{2}} \\\\ + (U_l - U_{l-\\frac{1}{2}}) / \\Delta_t + &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l - \\frac{1}{2}} \\ \\ + - \\hat{H}_{l-\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\ + - \\tilde{E}_l \\cdot \\tilde{J}_{l-\\frac{1}{2}} \\\\ + \\end{aligned} + $$ + + These equalities are exact and should practically hold to within numerical precision. + No time- or spatial-averaging is necessary. (See `meanas.fdtd` docs for derivation.) + + Args: + e: E-field + h: H-field (one half-timestep before or after `e`) + dxes: Grid description; see `meanas.fdmath`. + + Returns: + s: Vector field. Components indicate the energy transfer rate from the + corresponding energy cell into its +x, +y, and +z neighbors during + the half-step from the time of the earlier input field until the + time of later input field. """ if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) @@ -39,7 +94,24 @@ def poynting_divergence(s: Optional[fdfield_t] = None, dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: """ - Calculate the divergence of the poynting vector + Calculate the divergence of the poynting vector. + + This is the net energy flow for each cell, i.e. the change in energy `U` + per `dt` caused by transfer of energy to nearby cells (rather than + absorption/emission by currents `J` or `M`). + + See `poynting` and `meanas.fdtd` for more details. + Args: + s: Poynting vector, as calculated with `poynting`. Optional; caller + can provide `e` and `h` instead. + e: E-field (optional; need either `s` or both `e` and `h`) + h: H-field (optional; need either `s` or both `e` and `h`) + dxes: Grid description; see `meanas.fdmath`. + + Returns: + ds: Divergence of the poynting vector. + Entries indicate the net energy flow out of the corresponding + energy cell. """ if s is None: assert(e is not None) @@ -59,6 +131,22 @@ def energy_hstep(e0: fdfield_t, mu: Optional[fdfield_t] = None, dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: + """ + Calculate energy `U` at the time of the provided H-field `h1`. + + TODO: Figure out what this means spatially. + + Args: + e0: E-field one half-timestep before the energy. + h1: H-field (at the same timestep as the energy). + e2: E-field one half-timestep after the energy. + epsilon: Dielectric constant distribution. + mu: Magnetic permeability distribution. + dxes: Grid description; see `meanas.fdmath`. + + Returns: + Energy, at the time of the H-field `h1`. + """ u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) return u @@ -70,6 +158,22 @@ def energy_estep(h0: fdfield_t, mu: Optional[fdfield_t] = None, dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: + """ + Calculate energy `U` at the time of the provided E-field `e1`. + + TODO: Figure out what this means spatially. + + Args: + h0: H-field one half-timestep before the energy. + e1: E-field (at the same timestep as the energy). + h2: H-field one half-timestep after the energy. + epsilon: Dielectric constant distribution. + mu: Magnetic permeability distribution. + dxes: Grid description; see `meanas.fdmath`. + + Returns: + Energy, at the time of the E-field `e1`. + """ u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) return u @@ -84,7 +188,21 @@ def delta_energy_h2e(dt: float, dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: """ + Change in energy during the half-step from `h1` to `e2`. + This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) + + Args: + e0: E-field one half-timestep before the start of the energy delta. + h1: H-field at the start of the energy delta. + e2: E-field at the end of the energy delta (one half-timestep after `h1`). + h3: H-field one half-timestep after the end of the energy delta. + epsilon: Dielectric constant distribution. + mu: Magnetic permeability distribution. + dxes: Grid description; see `meanas.fdmath`. + + Returns: + Change in energy from the time of `h1` to the time of `e2`. """ de = e2 * (e2 - e0) / dt dh = h1 * (h3 - h1) / dt @@ -102,7 +220,21 @@ def delta_energy_e2h(dt: float, dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: """ + Change in energy during the half-step from `e1` to `h2`. + This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) + + Args: + h0: E-field one half-timestep before the start of the energy delta. + e1: H-field at the start of the energy delta. + h2: E-field at the end of the energy delta (one half-timestep after `e1`). + e3: H-field one half-timestep after the end of the energy delta. + epsilon: Dielectric constant distribution. + mu: Magnetic permeability distribution. + dxes: Grid description; see `meanas.fdmath`. + + Returns: + Change in energy from the time of `e1` to the time of `h2`. """ de = e1 * (e3 - e1) / dt dh = h2 * (h2 - h0) / dt @@ -114,6 +246,14 @@ def delta_energy_j(j0: fdfield_t, e1: fdfield_t, dxes: Optional[dx_lists_t] = None, ) -> fdfield_t: + """ + Calculate + + Note that each value of $J$ contributes to the energy twice (i.e. once per field update) + despite only causing the value of $E$ to change once (same for $M$ and $H$). + + + """ if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) From 81cd3d7d85aaa784bee2935f8320854090dbefe2 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 11 Jul 2021 17:27:42 -0700 Subject: [PATCH 253/437] add html_helpers.py to templates --- pdoc_templates/html_helpers.py | 540 +++++++++++++++++++++++++++++++++ 1 file changed, 540 insertions(+) create mode 100644 pdoc_templates/html_helpers.py diff --git a/pdoc_templates/html_helpers.py b/pdoc_templates/html_helpers.py new file mode 100644 index 0000000..a0be764 --- /dev/null +++ b/pdoc_templates/html_helpers.py @@ -0,0 +1,540 @@ +""" +Helper functions for HTML output. +""" +import inspect +import os +import re +import subprocess +import traceback +from functools import partial, lru_cache +from typing import Callable, Match +from warnings import warn + +import markdown +from markdown.inlinepatterns import InlineProcessor +from markdown.util import AtomicString, etree + +import pdoc + + +@lru_cache() +def minify_css(css: str, + _whitespace=partial(re.compile(r'\s*([,{:;}])\s*').sub, r'\1'), + _comments=partial(re.compile(r'/\*.*?\*/', flags=re.DOTALL).sub, ''), + _trailing_semicolon=partial(re.compile(r';\s*}').sub, '}')): + """ + Minify CSS by removing extraneous whitespace, comments, and trailing semicolons. + """ + return _trailing_semicolon(_whitespace(_comments(css))).strip() + + +def minify_html(html: str, + _minify=partial( + re.compile(r'(.*?)()|(.*)', re.IGNORECASE | re.DOTALL).sub, + lambda m, _norm_space=partial(re.compile(r'\s\s+').sub, '\n'): ( + _norm_space(m.group(1) or '') + + (m.group(2) or '') + + _norm_space(m.group(3) or '')))): + """ + Minify HTML by replacing all consecutive whitespace with a single space + (or newline) character, except inside `
` tags.
+    """
+    return _minify(html)
+
+
+def glimpse(text: str, max_length=153, *, paragraph=True,
+            _split_paragraph=partial(re.compile(r'\s*\n\s*\n\s*').split, maxsplit=1),
+            _trim_last_word=partial(re.compile(r'\S+$').sub, ''),
+            _remove_titles=partial(re.compile(r'^(#+|-{4,}|={4,})', re.MULTILINE).sub, ' ')):
+    """
+    Returns a short excerpt (e.g. first paragraph) of text.
+    If `paragraph` is True, the first paragraph will be returned,
+    but never longer than `max_length` characters.
+    """
+    text = text.lstrip()
+    if paragraph:
+        text, *rest = _split_paragraph(text)
+        if rest:
+            text = text.rstrip('.')
+            text += ' …'
+        text = _remove_titles(text).strip()
+
+    if len(text) > max_length:
+        text = _trim_last_word(text[:max_length - 2])
+        if not text.endswith('.') or not paragraph:
+            text = text.rstrip('. ') + ' …'
+    return text
+
+
+_md = markdown.Markdown(
+    output_format='html5',
+    extensions=[
+        "markdown.extensions.abbr",
+        "markdown.extensions.attr_list",
+        "markdown.extensions.def_list",
+        "markdown.extensions.fenced_code",
+        "markdown.extensions.footnotes",
+        "markdown.extensions.tables",
+        "markdown.extensions.admonition",
+        "markdown.extensions.smarty",
+        "markdown.extensions.toc",
+    ],
+    extension_configs={
+        "markdown.extensions.smarty": dict(
+            smart_dashes=True,
+            smart_ellipses=True,
+            smart_quotes=False,
+            smart_angled_quotes=False,
+        ),
+    },
+)
+
+
+class _ToMarkdown:
+    """
+    This class serves as a namespace for methods converting common
+    documentation formats into markdown our Python-Markdown with
+    addons can ingest.
+
+    If debugging regexs (I can't imagine why that would be necessary
+    — they are all perfect!) an insta-preview tool such as RegEx101.com
+    will come in handy.
+    """
+    @staticmethod
+    def _deflist(name, type, desc,
+                 # Wraps any identifiers and string literals in parameter type spec
+                 # in backticks while skipping common "stopwords" such as 'or', 'of',
+                 # 'optional' ... See §4 Parameters:
+                 # https://numpydoc.readthedocs.io/en/latest/format.html#sections
+                 _type_parts=partial(
+                     re.compile(r'[\w.\'"]+').sub,
+                     lambda m: ('{}' if m.group(0) in ('of', 'or', 'default', 'optional') else
+                                '`{}`').format(m.group(0)))):
+        """
+        Returns `name`, `type`, and `desc` formatted as a
+        Python-Markdown definition list entry. See also:
+        https://python-markdown.github.io/extensions/definition_lists/
+        """
+        type = _type_parts(type or '')
+        desc = desc or ' '
+        assert _ToMarkdown._is_indented_4_spaces(desc)
+        assert name or type
+        ret = ""
+        if name:
+            ret += '**`{}`**'.format(name)
+        if type:
+            ret += ' : {}'.format(type) if ret else type
+        ret += '\n:   {}\n\n'.format(desc)
+        return ret
+
+    @staticmethod
+    def _numpy_params(match,
+                      _name_parts=partial(re.compile(', ').sub, '`**, **`')):
+        """ Converts NumpyDoc parameter (etc.) sections into Markdown. """
+        name, type, desc = match.group("name", "type", "desc")
+        type = type or match.groupdict().get('just_type', None)
+        desc = desc.strip()
+        name = name and _name_parts(name)
+        return _ToMarkdown._deflist(name, type, desc)
+
+    @staticmethod
+    def _numpy_seealso(match):
+        """
+        Converts NumpyDoc "See Also" section either into referenced code,
+        optionally within a definition list.
+        """
+        spec_with_desc, simple_list = match.groups()
+        if spec_with_desc:
+            return '\n\n'.join('`{}`\n:   {}'.format(*map(str.strip, line.split(':', 1)))
+                               for line in filter(None, spec_with_desc.split('\n')))
+        return ', '.join('`{}`'.format(i) for i in simple_list.split(', '))
+
+    @staticmethod
+    def _numpy_sections(match):
+        """
+        Convert sections with parameter, return, and see also lists to Markdown
+        lists.
+        """
+        section, body = match.groups()
+        if section.title() == 'See Also':
+            body = re.sub(r'^((?:\n?[\w.]* ?: .*)+)|(.*\w.*)',
+                          _ToMarkdown._numpy_seealso, body)
+        elif section.title() in ('Returns', 'Yields', 'Raises', 'Warns'):
+            body = re.sub(r'^(?:(?P\*{0,2}\w+(?:, \*{0,2}\w+)*)'
+                          r'(?: ?: (?P.*))|'
+                          r'(?P\w[^\n`*]*))(?(?:\n(?: {4}.*|$))*)',
+                          _ToMarkdown._numpy_params, body, flags=re.MULTILINE)
+        else:
+            body = re.sub(r'^(?P\*{0,2}\w+(?:, \*{0,2}\w+)*)'
+                          r'(?: ?: (?P.*))?(?(?:\n(?: {4}.*|$))*)',
+                          _ToMarkdown._numpy_params, body, flags=re.MULTILINE)
+        return section + '\n-----\n' + body
+
+    @staticmethod
+    def numpy(text):
+        """
+        Convert `text` in numpydoc docstring format to Markdown
+        to be further converted later.
+        """
+        return re.sub(r'^(\w[\w ]+)\n-{3,}\n'
+                      r'((?:(?!.+\n-+).*$\n?)*)',
+                      _ToMarkdown._numpy_sections, text, flags=re.MULTILINE)
+
+    @staticmethod
+    def _is_indented_4_spaces(txt, _3_spaces_or_less=re.compile(r'\n\s{0,3}\S').search):
+        return '\n' not in txt or not _3_spaces_or_less(txt)
+
+    @staticmethod
+    def _fix_indent(name, type, desc):
+        """Maybe fix indent from 2 to 4 spaces."""
+        if not _ToMarkdown._is_indented_4_spaces(desc):
+            desc = desc.replace('\n', '\n  ')
+        return name, type, desc
+
+    @staticmethod
+    def indent(indent, text, *, clean_first=False):
+        if clean_first:
+            text = inspect.cleandoc(text)
+        return re.sub(r'\n', '\n' + indent, indent + text.rstrip())
+
+    @staticmethod
+    def google(text,
+               _googledoc_sections=partial(
+                   re.compile(r'^([A-Z]\w+):$\n((?:\n?(?: {2,}.*|$))+)', re.MULTILINE).sub,
+                   lambda m, _params=partial(
+                           re.compile(r'^([\w*]+)(?: \(([\w.,=\[\] ]+)\))?: '
+                                      r'((?:.*)(?:\n(?: {2,}.*|$))*)', re.MULTILINE).sub,
+                           lambda m: _ToMarkdown._deflist(*_ToMarkdown._fix_indent(*m.groups()))): (
+                       m.group() if not m.group(2) else '\n{}\n-----\n{}'.format(
+                           m.group(1), _params(inspect.cleandoc('\n' + m.group(2))))))):
+        """
+        Convert `text` in Google-style docstring format to Markdown
+        to be further converted later.
+        """
+        return _googledoc_sections(text)
+
+    @staticmethod
+    def _admonition(match, module=None, limit_types=None):
+        indent, type, value, text = match.groups()
+
+        if limit_types and type not in limit_types:
+            return match.group(0)
+
+        if type == 'include' and module:
+            try:
+                return _ToMarkdown._include_file(indent, value,
+                                                 _ToMarkdown._directive_opts(text), module)
+            except Exception as e:
+                raise RuntimeError('`.. include:: {}` error in module {!r}: {}'
+                                   .format(value, module.name, e))
+        if type in ('image', 'figure'):
+            return '{}![{}]({})\n'.format(
+                indent, text.translate(str.maketrans({'\n': ' ',
+                                                      '[': '\\[',
+                                                      ']': '\\]'})).strip(), value)
+        if type == 'math':
+            return _ToMarkdown.indent(indent,
+                                      '\\[ ' + text.strip() + ' \\]',
+                                      clean_first=True)
+
+        if type == 'versionchanged':
+            title = 'Changed in version: ' + value
+        elif type == 'versionadded':
+            title = 'Added in version: ' + value
+        elif type == 'deprecated' and value:
+            title = 'Deprecated since version: ' + value
+        elif type == 'admonition':
+            title = value
+        elif type.lower() == 'todo':
+            title = 'TODO'
+            text = value + ' ' + text
+        else:
+            title = type.capitalize()
+            if value:
+                title += ': ' + value
+
+        text = _ToMarkdown.indent(indent + '    ', text, clean_first=True)
+        return '{}!!! {} "{}"\n{}\n'.format(indent, type, title, text)
+
+    @staticmethod
+    def admonitions(text, module, limit_types=None):
+        """
+        Process reStructuredText's block directives such as
+        `.. warning::`, `.. deprecated::`, `.. versionadded::`, etc.
+        and turn them into Python-M>arkdown admonitions.
+
+        `limit_types` is optionally a set of directives to limit processing to.
+
+        See: https://python-markdown.github.io/extensions/admonition/
+        """
+        substitute = partial(re.compile(r'^(?P *)\.\. ?(\w+)::(?: *(.*))?'
+                                        r'((?:\n(?:(?P=indent) +.*| *$))*)', re.MULTILINE).sub,
+                             partial(_ToMarkdown._admonition, module=module,
+                                     limit_types=limit_types))
+        # Apply twice for nested (e.g. image inside warning)
+        return substitute(substitute(text))
+
+    @staticmethod
+    def _include_file(indent: str, path: str, options: dict, module: pdoc.Module) -> str:
+        start_line = int(options.get('start-line', 0))
+        end_line = int(options.get('end-line', 0)) or None
+        start_after = options.get('start-after')
+        end_before = options.get('end-before')
+
+        with open(os.path.join(os.path.dirname(module.obj.__file__), path),
+                  encoding='utf-8') as f:
+            text = ''.join(list(f)[start_line:end_line])
+
+        if start_after:
+            text = text[text.index(start_after) + len(start_after):]
+        if end_before:
+            text = text[:text.index(end_before)]
+
+        return _ToMarkdown.indent(indent, text)
+
+    @staticmethod
+    def _directive_opts(text: str) -> dict:
+        return dict(re.findall(r'^ *:([^:]+): *(.*)', text, re.MULTILINE))
+
+    @staticmethod
+    def doctests(text,
+                 _indent_doctests=partial(
+                     re.compile(r'(?:^(?P```|~~~).*\n)?'
+                                r'(?:^>>>.*'
+                                r'(?:\n(?:(?:>>>|\.\.\.).*))*'
+                                r'(?:\n.*)?\n\n?)+'
+                                r'(?P=fence)?', re.MULTILINE).sub,
+                     lambda m: (m.group(0) if m.group('fence') else
+                                ('\n    ' + '\n    '.join(m.group(0).split('\n')) + '\n\n')))):
+        """
+        Indent non-fenced (`~~~`) top-level (0-indented)
+        doctest blocks so they render as code.
+        """
+        if not text.endswith('\n'):  # Needed for the r'(?:\n.*)?\n\n?)+' line (GH-72)
+            text += '\n'
+        return _indent_doctests(text)
+
+    @staticmethod
+    def raw_urls(text):
+        """Wrap URLs in Python-Markdown-compatible ."""
+        return re.sub(r'(?)\s]+)(\s*)', r'\1<\2>\3', text)
+
+import subprocess
+
+class _MathPattern(InlineProcessor):
+    NAME = 'pdoc-math'
+    PATTERN = r'(?'):  # CUT was put into its own paragraph
+        toc = toc[:-3].rstrip()
+    return toc
+
+
+def format_git_link(template: str, dobj: pdoc.Doc):
+    """
+    Interpolate `template` as a formatted string literal using values extracted
+    from `dobj` and the working environment.
+    """
+    if not template:
+        return None
+    try:
+        if 'commit' in _str_template_fields(template):
+            commit = _git_head_commit()
+        abs_path = inspect.getfile(inspect.unwrap(dobj.obj))
+        path = _project_relative_path(abs_path)
+        lines, start_line = inspect.getsourcelines(dobj.obj)
+        end_line = start_line + len(lines) - 1
+        url = template.format(**locals())
+        return url
+    except Exception:
+        warn('format_git_link for {} failed:\n{}'.format(dobj.obj, traceback.format_exc()))
+        return None
+
+
+@lru_cache()
+def _git_head_commit():
+    """
+    If the working directory is part of a git repository, return the
+    head git commit hash. Otherwise, raise a CalledProcessError.
+    """
+    process_args = ['git', 'rev-parse', 'HEAD']
+    try:
+        commit = subprocess.check_output(process_args, universal_newlines=True).strip()
+        return commit
+    except OSError as error:
+        warn("git executable not found on system:\n{}".format(error))
+    except subprocess.CalledProcessError as error:
+        warn(
+            "Ensure pdoc is run within a git repository.\n"
+            "`{}` failed with output:\n{}"
+            .format(' '.join(process_args), error.output)
+        )
+    return None
+
+
+@lru_cache()
+def _git_project_root():
+    """
+    Return the path to project root directory or None if indeterminate.
+    """
+    path = None
+    for cmd in (['git', 'rev-parse', '--show-superproject-working-tree'],
+                ['git', 'rev-parse', '--show-toplevel']):
+        try:
+            path = subprocess.check_output(cmd, universal_newlines=True).rstrip('\r\n')
+            if path:
+                break
+        except (subprocess.CalledProcessError, OSError):
+            pass
+    return path
+
+
+@lru_cache()
+def _project_relative_path(absolute_path):
+    """
+    Convert an absolute path of a python source file to a project-relative path.
+    Assumes the project's path is either the current working directory or
+    Python library installation.
+    """
+    from distutils.sysconfig import get_python_lib
+    for prefix_path in (_git_project_root() or os.getcwd(),
+                        get_python_lib()):
+        common_path = os.path.commonpath([prefix_path, absolute_path])
+        if common_path == prefix_path:
+            # absolute_path is a descendant of prefix_path
+            return os.path.relpath(absolute_path, prefix_path)
+    raise RuntimeError(
+        "absolute path {!r} is not a descendant of the current working directory "
+        "or of the system's python library."
+        .format(absolute_path)
+    )
+
+
+@lru_cache()
+def _str_template_fields(template):
+    """
+    Return a list of `str.format` field names in a template string.
+    """
+    from string import Formatter
+    return [
+        field_name
+        for _, field_name, _, _ in Formatter().parse(template)
+        if field_name is not None
+    ]

From 47e2cb2efcdde96cece405e5e5aff9cce4809645 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 11 Jul 2021 17:33:15 -0700
Subject: [PATCH 254/437] vec/unvec live in fdmath

---
 examples/tcyl.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/tcyl.py b/examples/tcyl.py
index b0b57a2..d106589 100644
--- a/examples/tcyl.py
+++ b/examples/tcyl.py
@@ -2,7 +2,7 @@ import importlib
 import numpy
 from numpy.linalg import norm
 
-from meanas import vec, unvec
+from meanas.fdmath import vec, unvec
 from meanas.fdfd import waveguide_mode, functional, scpml
 from meanas.fdfd.solvers import generic as generic_solver
 

From cb0062f6f1bb4a0b8b469b5b88cdaf3671222383 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 5 Sep 2021 17:49:08 -0700
Subject: [PATCH 255/437] reduce matplotlib log spam

---
 examples/fdfd.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/examples/fdfd.py b/examples/fdfd.py
index 3fe895f..47b8a30 100644
--- a/examples/fdfd.py
+++ b/examples/fdfd.py
@@ -15,6 +15,7 @@ from matplotlib import pyplot
 import logging
 
 logging.basicConfig(level=logging.DEBUG)
+logging.getLogger('matplotlib').setLevel(logging.WARNING)
 
 __author__ = 'Jan Petykiewicz'
 

From c06dd03a901571cc13c56f6c50f7b0c98a82de5e Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 5 Sep 2021 17:49:49 -0700
Subject: [PATCH 256/437] force remove in case the dir doesn't exist

---
 make_docs.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/make_docs.sh b/make_docs.sh
index c594313..7ce9e39 100755
--- a/make_docs.sh
+++ b/make_docs.sh
@@ -9,7 +9,7 @@ cd ~/projects/meanas
 #    pandoc --metadata=title:"meanas" --toc --toc-depth=4 --from=markdown+abbreviations --to=html --output=doc.html --gladtex -s -
 
 # Approach 2: pdf to html with gladtex
-rm -r _doc_mathimg
+rm -rf _doc_mathimg
 pdoc --pdf --force --template-dir pdoc_templates -o doc . > doc.md
 pandoc --metadata=title:"meanas" --from=markdown+abbreviations --to=html --output=doc.htex --gladtex -s --css pdoc_templates/pdoc.css doc.md
 gladtex -a -n -d _doc_mathimg -c white doc.htex

From d3db3ca91e66e79c5dd68e4ee3279a271f07d289 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 5 Sep 2021 17:50:34 -0700
Subject: [PATCH 257/437] fix formula not rendering

---
 meanas/fdfd/waveguide_2d.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 015c299..39463bf 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -150,8 +150,9 @@ $$
 
 Using these, we can construct the eigenvalue problem
 
-$$ \\beta^2 \\begin{bmatrix} E_x \\\\
-                             E_y \\end{bmatrix} =
+$$
+\\beta^2 \\begin{bmatrix} E_x \\\\
+                          E_y \\end{bmatrix} =
     (\\omega^2 \\begin{bmatrix} \\mu_{yy} \\epsilon_{xx} & 0 \\\\
                                                        0 & \\mu_{xx} \\epsilon_{yy} \\end{bmatrix} +
                  \\begin{bmatrix} -\\mu_{yy} \\hat{\\partial}_y \\\\

From 01b49713884a2c619015cb53b0a7666e4a70f622 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 5 Sep 2021 17:50:51 -0700
Subject: [PATCH 258/437] Overlap is computed with the conjugate

---
 meanas/fdfd/waveguide_3d.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py
index 8f466de..4a65453 100644
--- a/meanas/fdfd/waveguide_3d.py
+++ b/meanas/fdfd/waveguide_3d.py
@@ -167,7 +167,7 @@ def compute_overlap_e(E: fdfield_t,
         mu: Magnetic permeability (default 1 everywhere)
 
     Returns:
-        overlap_e such that `numpy.sum(overlap_e * other_e)` computes the overlap integral
+        overlap_e such that `numpy.sum(overlap_e * other_e.conj())` computes the overlap integral
     """
     slices = tuple(slices)
 

From c0bbc1f46dfc0616c86c7edbe87d4a4ef4e52b54 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 5 Sep 2021 17:52:09 -0700
Subject: [PATCH 259/437] fix fdtd pmls

integrate them into the update operations
---
 meanas/fdmath/functional.py |  23 +++-
 meanas/fdtd/__init__.py     |   2 +-
 meanas/fdtd/pml.py          | 211 ++++++++++++++++++++++++++----------
 3 files changed, 174 insertions(+), 62 deletions(-)

diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py
index d8e0758..a62655d 100644
--- a/meanas/fdmath/functional.py
+++ b/meanas/fdmath/functional.py
@@ -3,7 +3,8 @@ Math functions for finite difference simulations
 
 Basic discrete calculus etc.
 """
-from typing import Sequence, Tuple, Optional
+from typing import Sequence, Tuple, Optional, Callable
+
 import numpy            # type: ignore
 
 from .types import fdfield_t, fdfield_updater_t
@@ -109,3 +110,23 @@ def curl_back(dx_h: Optional[Sequence[numpy.ndarray]] = None) -> fdfield_updater
     return ch_fun
 
 
+def curl_forward_parts(dx_e: Optional[Sequence[numpy.ndarray]] = None) -> Callable:
+    Dx, Dy, Dz = deriv_forward(dx_e)
+
+    def mkparts_fwd(e: fdfield_t) -> Tuple[Tuple[fdfield_t, ...]]:
+        return ((-Dz(e[1]),  Dy(e[2])),
+                ( Dz(e[0]), -Dx(e[2])),
+                (-Dy(e[0]),  Dx(e[1])))
+
+    return mkparts_fwd
+
+
+def curl_back_parts(dx_h: Optional[Sequence[numpy.ndarray]] = None) -> Callable:
+    Dx, Dy, Dz = deriv_back(dx_e)
+
+    def mkparts_back(h: fdfield_t) -> Tuple[Tuple[fdfield_t, ...]]:
+        return ((-Dz(h[1]),  Dy(h[2])),
+                ( Dz(h[0]), -Dx(h[2])),
+                (-Dy(h[0]),  Dx(h[1])))
+
+    return mkparts_back
diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py
index 92e215f..c1e7106 100644
--- a/meanas/fdtd/__init__.py
+++ b/meanas/fdtd/__init__.py
@@ -160,7 +160,7 @@ Boundary conditions
 """
 
 from .base import maxwell_e, maxwell_h
-from .pml import cpml
+from .pml import cpml_params, updates_with_cpml
 from .energy import (poynting, poynting_divergence, energy_hstep, energy_estep,
                      delta_energy_h2e, delta_energy_j)
 from .boundaries import conducting_boundary
diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py
index e1c9668..066cca8 100644
--- a/meanas/fdtd/pml.py
+++ b/meanas/fdtd/pml.py
@@ -7,31 +7,31 @@ PML implementations
 """
 # TODO retest pmls!
 
-from typing import List, Callable, Tuple, Dict, Any
+from typing import List, Callable, Tuple, Dict, Sequence, Any, Optional
 import numpy            # type: ignore
 
-from ..fdmath import fdfield_t
+from ..fdmath import fdfield_t, dx_lists_t
+from ..fdmath.functional import deriv_forward, deriv_back
 
 
 __author__ = 'Jan Petykiewicz'
 
 
-def cpml(direction: int,
-         polarity: int,
-         dt: float,
-         epsilon: fdfield_t,
-         thickness: int = 8,
-         ln_R_per_layer: float = -1.6,
-         epsilon_eff: float = 1,
-         mu_eff: float = 1,
-         m: float = 3.5,
-         ma: float = 1,
-         cfs_alpha: float = 0,
-         dtype: numpy.dtype = numpy.float32,
-         ) -> Tuple[Callable, Callable, Dict[str, fdfield_t]]:
+def cpml_params(
+        axis: int,
+        polarity: int,
+        dt: float,
+        thickness: int = 8,
+        ln_R_per_layer: float = -1.6,
+        epsilon_eff: float = 1,
+        mu_eff: float = 1,
+        m: float = 3.5,
+        ma: float = 1,
+        cfs_alpha: float = 0,
+        ) -> Dict[str, Any]:
 
-    if direction not in range(3):
-        raise Exception('Invalid direction: {}'.format(direction))
+    if axis not in range(3):
+        raise Exception('Invalid axis: {}'.format(axis))
 
     if polarity not in (-1, 1):
         raise Exception('Invalid polarity: {}'.format(polarity))
@@ -45,10 +45,8 @@ def cpml(direction: int,
     sigma_max = -ln_R_per_layer / 2 * (m + 1)
     kappa_max = numpy.sqrt(epsilon_eff * mu_eff)
     alpha_max = cfs_alpha
-    transverse = numpy.delete(range(3), direction)
-    u, v = transverse
 
-    xe = numpy.arange(1, thickness + 1, dtype=float)
+    xe = numpy.arange(1, thickness + 1, dtype=float)        # TODO: pass in dtype?
     xh = numpy.arange(1, thickness + 1, dtype=float)
     if polarity > 0:
         xe -= 0.5
@@ -59,8 +57,8 @@ def cpml(direction: int,
     else:
         raise Exception('Bad polarity!')
 
-    expand_slice_l: List[Any] = [None] * 3
-    expand_slice_l[direction] = slice(None)
+    expand_slice_l: List[Any] = [None, None, None]
+    expand_slice_l[axis] = slice(None)
     expand_slice = tuple(expand_slice_l)
 
     def par(x: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]:
@@ -76,52 +74,145 @@ def cpml(direction: int,
     p0e, p1e, p2e = par(xe)
     p0h, p1h, p2h = par(xh)
 
-    region_list = [slice(None)] * 3
+    region_list = [slice(None), slice(None), slice(None)]
     if polarity < 0:
-        region_list[direction] = slice(None, thickness)
+        region_list[axis] = slice(None, thickness)
     elif polarity > 0:
-        region_list[direction] = slice(-thickness, None)
+        region_list[axis] = slice(-thickness, None)
     else:
         raise Exception('Bad polarity!')
     region = tuple(region_list)
 
-    se = 1 if direction == 1 else -1
+    return {
+        'param_e': (p0e, p1e, p2e),
+        'param_h': (p0h, p1h, p2h),
+        'region': region,
+        }
 
-    # TODO check if epsilon is uniform in pml region?
-    shape = list(epsilon[0].shape)
-    shape[direction] = thickness
-    psi_e = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)]
-    psi_h = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)]
 
-    fields = {
-        'psi_e_u': psi_e[0],
-        'psi_e_v': psi_e[1],
-        'psi_h_u': psi_h[0],
-        'psi_h_v': psi_h[1],
-    }
+def updates_with_cpml(
+         cpml_params: Sequence[Sequence[Optional[Dict[str, Any]]]],
+         dt: float,
+         dxes: dx_lists_t,
+         epsilon: fdfield_t,
+         *,
+         dtype: numpy.dtype = numpy.float32,
+         ) -> Tuple[Callable[[fdfield_t, fdfield_t], None],
+                    Callable[[fdfield_t, fdfield_t], None]]:
 
-    # Note that this is kinda slow -- would be faster to reuse dHv*p2h for the original
-    #  H update, but then you have multiple arrays and a monolithic (field + pml) update operation
-    def pml_e(e: fdfield_t, h: fdfield_t, epsilon: fdfield_t) -> Tuple[fdfield_t, fdfield_t]:
-        dHv = h[v][region] - numpy.roll(h[v], 1, axis=direction)[region]
-        dHu = h[u][region] - numpy.roll(h[u], 1, axis=direction)[region]
-        psi_e[0] *= p0e
-        psi_e[0] += p1e * dHv * p2e
-        psi_e[1] *= p0e
-        psi_e[1] += p1e * dHu * p2e
-        e[u][region] += se * dt / epsilon[u][region] * (psi_e[0] + (p2e - 1) * dHv)
-        e[v][region] -= se * dt / epsilon[v][region] * (psi_e[1] + (p2e - 1) * dHu)
-        return e, h
+    Dfx, Dfy, Dfz = deriv_forward(dxes[1])
+    Dbx, Dby, Dbz = deriv_back(dxes[1])
 
-    def pml_h(e: fdfield_t, h: fdfield_t) -> Tuple[fdfield_t, fdfield_t]:
-        dEv = (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region])
-        dEu = (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region])
-        psi_h[0] *= p0h
-        psi_h[0] += p1h * dEv * p2h
-        psi_h[1] *= p0h
-        psi_h[1] += p1h * dEu * p2h
-        h[u][region] -= se * dt * (psi_h[0] + (p2h - 1) * dEv)
-        h[v][region] += se * dt * (psi_h[1] + (p2h - 1) * dEu)
-        return e, h
+    psi_E = [[None, None], [None, None], [None, None]]
+    psi_H = [[None, None], [None, None], [None, None]]
+    params_E = [[None, None], [None, None], [None, None]]
+    params_H = [[None, None], [None, None], [None, None]]
 
-    return pml_e, pml_h, fields
+    for axis in range(3):
+        for pp, polarity in enumerate((-1, 1)):
+            if cpml_params[axis][pp] is None:
+                psi_E[axis][pp] = (None, None)
+                psi_H[axis][pp] = (None, None)
+                continue
+
+            cpml_param = cpml_params[axis][pp]
+
+            region = cpml_param['region']
+            region_shape = epsilon[0][region].shape
+
+            psi_E[axis][pp] = (numpy.zeros(region_shape, dtype=dtype),
+                                    numpy.zeros(region_shape, dtype=dtype))
+            psi_H[axis][pp] = (numpy.zeros(region_shape, dtype=dtype),
+                                    numpy.zeros(region_shape, dtype=dtype))
+            params_E[axis][pp] = cpml_param['param_e'] + (region,)
+            params_H[axis][pp] = cpml_param['param_h'] + (region,)
+
+
+    pE = numpy.empty_like(epsilon, dtype=dtype)
+    pH = numpy.empty_like(epsilon, dtype=dtype)
+
+    def update_E(e: fdfield_t, h: fdfield_t, epsilon: fdfield_t) -> None:
+        dyHx = Dby(h[0])
+        dzHx = Dbz(h[0])
+        dxHy = Dbx(h[1])
+        dzHy = Dbz(h[1])
+        dxHz = Dbx(h[2])
+        dyHz = Dby(h[2])
+
+        dH = ((dxHy, dxHz),
+              (dyHx, dyHz),
+              (dzHx, dzHy))
+
+        pE.fill(0)
+
+        for axis in range(3):
+            se = (-1, 1, -1)[axis]
+            transverse = numpy.delete(range(3), axis)
+            u, v = transverse
+            dHu, dHv = dH[axis]
+
+            for pp in range(2):
+                psi_Eu, psi_Ev = psi_E[axis][pp]
+
+                if psi_Eu is None:
+                    # No pml in this direction
+                    continue
+
+                p0e, p1e, p2e, region = params_E[axis][pp]
+
+                dHu[region] *= p2e
+                dHv[region] *= p2e
+                psi_Eu *= p0e
+                psi_Ev *= p0e
+                psi_Eu += p1e * dHv[region]    # note reversed u,v mapping
+                psi_Ev += p1e * dHu[region]
+                pE[u][region] += +se * psi_Eu
+                pE[v][region] += -se * psi_Ev
+
+        e[0] += dt / epsilon[0] * (dyHz - dzHy + pE[0])
+        e[1] += dt / epsilon[1] * (dzHx - dxHz + pE[1])
+        e[2] += dt / epsilon[2] * (dxHy - dyHx + pE[2])
+
+
+    def update_H(e: fdfield_t, h: fdfield_t, mu: fdfield_t = (1, 1, 1)) -> None:
+        dyEx = Dfy(e[0])
+        dzEx = Dfz(e[0])
+        dxEy = Dfx(e[1])
+        dzEy = Dfz(e[1])
+        dxEz = Dfx(e[2])
+        dyEz = Dfy(e[2])
+
+        dE = ((dxEy, dxEz),
+              (dyEx, dyEz),
+              (dzEx, dzEy))
+
+        pH.fill(0)
+
+        for axis in range(3):
+            se = (-1, 1, -1)[axis]
+            transverse = numpy.delete(range(3), axis)
+            u, v = transverse
+            dEu, dEv = dE[axis]
+
+            for pp in range(2):
+                psi_Hu, psi_Hv = psi_H[axis][pp]
+
+                if psi_Hu is None:
+                    # No pml here
+                    continue
+
+                p0h, p1h, p2h, region = params_H[axis][pp]
+
+                dEu[region] *= p2h      # modifies d_E_
+                dEv[region] *= p2h
+                psi_Hu *= p0h
+                psi_Hv *= p0h
+                psi_Hu += p1h * dEv[region]    # note reversed u,v mapping
+                psi_Hv += p1h * dEu[region]
+                pH[u][region] += +se * psi_Hu
+                pH[v][region] += -se * psi_Hv
+
+        h[0] -= dt / mu[0] * (dyEz - dzEy + pH[0])
+        h[1] -= dt / mu[1] * (dzEx - dxEz + pH[1])
+        h[2] -= dt / mu[2] * (dxEy - dyEx + pH[2])
+    return update_E, update_H

From e2ef6d1c8dbb10c0ae16987a3b3c806eeb4c2f27 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 30 Aug 2022 23:49:39 -0700
Subject: [PATCH 260/437] move to hatch-based build

---
 MANIFEST.in        |  2 --
 meanas/LICENSE.md  |  1 +
 meanas/README.md   |  1 +
 meanas/VERSION.py  |  4 ----
 meanas/__init__.py |  5 ++---
 pyproject.toml     | 53 ++++++++++++++++++++++++++++++++++++++++++++++
 setup.py           | 45 ---------------------------------------
 7 files changed, 57 insertions(+), 54 deletions(-)
 delete mode 100644 MANIFEST.in
 create mode 120000 meanas/LICENSE.md
 create mode 120000 meanas/README.md
 delete mode 100644 meanas/VERSION.py
 create mode 100644 pyproject.toml
 delete mode 100644 setup.py

diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index c28ab72..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,2 +0,0 @@
-include README.md
-include LICENSE.md
diff --git a/meanas/LICENSE.md b/meanas/LICENSE.md
new file mode 120000
index 0000000..7eabdb1
--- /dev/null
+++ b/meanas/LICENSE.md
@@ -0,0 +1 @@
+../LICENSE.md
\ No newline at end of file
diff --git a/meanas/README.md b/meanas/README.md
new file mode 120000
index 0000000..32d46ee
--- /dev/null
+++ b/meanas/README.md
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/meanas/VERSION.py b/meanas/VERSION.py
deleted file mode 100644
index 7261d61..0000000
--- a/meanas/VERSION.py
+++ /dev/null
@@ -1,4 +0,0 @@
-""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
-__version__ = '''
-0.7
-'''.strip()
diff --git a/meanas/__init__.py b/meanas/__init__.py
index 8b9b300..3b426e3 100644
--- a/meanas/__init__.py
+++ b/meanas/__init__.py
@@ -6,13 +6,12 @@ See the readme or `import meanas; help(meanas)` for more info.
 
 import pathlib
 
-from .VERSION import __version__
-
+__version__ = '0.7'
 __author__ = 'Jan Petykiewicz'
 
 
 try:
-    with open(pathlib.Path(__file__).parent.parent / 'README.md', 'r') as f:
+    with open(pathlib.Path(__file__).parent / 'README.md', 'r') as f:
         __doc__ = f.read()
 except Exception:
     pass
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..fc66831
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,53 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "meanas"
+description = "Electromagnetic simulation tools"
+readme = "README.md"
+license = { file = "LICENSE.md" }
+authors = [
+    { name="Jan Petykiewicz", email="jan@mpxd.net" },
+    ]
+homepage = "https://mpxd.net/code/jan/meanas"
+repository = "https://mpxd.net/code/jan/meanas"
+keywords = [
+    "electromagnetic",
+    "photonics",
+    "simulation",
+    "FDTD",
+    "FDFD",
+    "finite",
+    "difference",
+    "Bloch",
+    "EME",
+    "mode",
+    "solver",
+    ]
+classifiers = [
+    "Programming Language :: Python :: 3",
+    "Development Status :: 4 - Beta",
+    "Intended Audience :: Developers",
+    "Intended Audience :: Science/Research",
+    "License :: OSI Approved :: GNU Affero General Public License v3",
+    "Topic :: Scientific/Engineering :: Physics",
+    ]
+requires-python = ">=3.8"
+include = [
+    "LICENSE.md"
+    ]
+dynamic = ["version"]
+dependencies = [
+    "numpy~=1.21",
+    "scipy",
+    ]
+
+
+[tool.hatch.version]
+path = "meanas/__init__.py"
+
+[project.optional-dependencies]
+dev = ["pytest", "pdoc", "gridlock"]
+examples = ["gridlock"]
+test = ["pytest"]
diff --git a/setup.py b/setup.py
deleted file mode 100644
index fc7cbe7..0000000
--- a/setup.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env python3
-
-from setuptools import setup, find_packages
-
-
-with open('README.md', 'r') as f:
-    long_description = f.read()
-
-with open('meanas/VERSION.py', 'rt') as f:
-    version = f.readlines()[2].strip()
-
-setup(name='meanas',
-      version=version,
-      description='Electromagnetic simulation tools',
-      long_description=long_description,
-      long_description_content_type='text/markdown',
-      author='Jan Petykiewicz',
-      author_email='jan@mpxd.net',
-      url='https://mpxd.net/code/jan/meanas',
-      packages=find_packages(),
-      package_data={
-          'meanas': ['py.typed']
-      },
-      install_requires=[
-            'numpy',
-            'scipy',
-      ],
-      extras_require={
-            'test': [
-                'pytest',
-                'dataclasses',
-                ],
-            'examples': [
-                'gridlock',
-                ],
-      },
-      classifiers=[
-            'Programming Language :: Python :: 3',
-            'Development Status :: 4 - Beta',
-            'Intended Audience :: Developers',
-            'Intended Audience :: Science/Research',
-            'License :: OSI Approved :: GNU Affero General Public License v3',
-            'Topic :: Scientific/Engineering :: Physics',
-      ],
-      )

From 6b0182c10279749ccb6920c7ee6734037b8398d3 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 30 Aug 2022 23:50:29 -0700
Subject: [PATCH 261/437] update README

---
 README.md | 33 ++++++++++++---------------------
 1 file changed, 12 insertions(+), 21 deletions(-)

diff --git a/README.md b/README.md
index 91bf355..709e13a 100644
--- a/README.md
+++ b/README.md
@@ -48,45 +48,36 @@ linear systems, ideally with double precision.
 
 **Requirements:**
 
-* python 3 (tests require 3.7)
+* python >=3.8
 * numpy
 * scipy
 
 
 Install from PyPI with pip:
 ```bash
-pip3 install 'meanas[test,examples]'
+pip3 install 'meanas[dev]'
 ```
 
 ### Development install
-Install python3.7, virtualenv, and git:
+Install python3 and git:
 ```bash
 # This is for Debian/Ubuntu/other-apt-based systems; you may need an alternative command
-sudo apt install python3.7 virtualenv build-essential python3.7-dev git
-```
-
-If python 3.7 is not your default python3 version, create a virtualenv:
-```bash
-# Check python3 version:
-python3 --version
-# output on my system: Python 3.7.5rc1
-# If this indicates a version >= 3.7, you can skip all
-#  the steps involving virtualenv or referencing the venv/ directory
-
-# Create a virtual environment using python3.7 and place it in the directory `venv/`
-virtualenv -p python3.7 venv
+sudo apt install python3 build-essential python3-dev git
 ```
 
 In-place development install:
 ```bash
 # Download using git
-#git clone https://mpxd.net/code/jan/meanas.git
+git clone https://mpxd.net/code/jan/meanas.git
+
+# If you'd like to create a virtualenv, do so:
+python3 -m venv my_venv
 
 # If you are using a virtualenv, activate it
-source venv/bin/activate
+source my_venv/bin/activate
 
-# Install in-place (-e, editable) from ./meanas, including testing and example dependencies ([test, examples])
-pip3 install --user -e './meanas[test,examples]'
+# Install in-place (-e, editable) from ./meanas, including development dependencies ([dev])
+pip3 install --user -e './meanas[dev]'
 
 # Run tests
 cd meanas
@@ -95,7 +86,7 @@ python3 -m pytest -rsxX | tee test_results.txt
 
 #### See also:
 - [git book](https://git-scm.com/book/en/v2)
-- [virtualenv documentation](https://virtualenv.pypa.io/en/stable/userguide/)
+- [venv documentation](https://docs.python.org/3/tutorial/venv.html)
 - [python language reference](https://docs.python.org/3/reference/index.html)
 - [python standard library](https://docs.python.org/3/library/index.html)
 

From 831c39246a822cdb7ca4483a59f5d5684f4a8946 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 12:27:10 -0700
Subject: [PATCH 262/437] drop pathlib prefix

---
 examples/bloch.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/examples/bloch.py b/examples/bloch.py
index e77c80f..29bd82e 100644
--- a/examples/bloch.py
+++ b/examples/bloch.py
@@ -4,14 +4,15 @@ from numpy.linalg import norm
 import logging
 from pathlib import Path
 
+
 logging.basicConfig(level=logging.DEBUG)
 logger = logging.getLogger(__name__)
 
-WISDOM_FILEPATH = pathlib.Path.home() / '.local' / 'share' / 'pyfftw' / 'wisdom.pickle'
+WISDOM_FILEPATH = Path.home() / '.local/share/pyfftw/wisdom.pickle'
 
 
 def pyfftw_save_wisdom(path):
-    path = pathlib.Path(path)
+    path = Path(path)
     try:
         import pyfftw
         import pickle
@@ -24,7 +25,7 @@ def pyfftw_save_wisdom(path):
 
 
 def pyfftw_load_wisdom(path):
-    path = pathlib.Path(path)
+    path = Path(path)
     try:
         import pyfftw
         import pickle

From eedcc7b9197c6babae129ba36c7863e3da50c804 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 12:29:11 -0700
Subject: [PATCH 263/437] update for new Gridlock

---
 examples/bloch.py | 12 ++++++------
 examples/fdfd.py  | 42 +++++++++++++++++++++++-------------------
 examples/fdtd.py  | 11 ++++++-----
 examples/tcyl.py  |  7 ++++---
 4 files changed, 39 insertions(+), 33 deletions(-)

diff --git a/examples/bloch.py b/examples/bloch.py
index 29bd82e..75fc833 100644
--- a/examples/bloch.py
+++ b/examples/bloch.py
@@ -47,10 +47,10 @@ g = gridlock.Grid([numpy.arange(-x_period/2, x_period/2, dx),
                    numpy.arange(-1000, 1000, dx),
                    numpy.arange(-1000, 1000, dx)],
                   shifts=numpy.array([[0,0,0]]),
-                  initial=1.445**2,
                   periodic=True)
+gdata = g.allocate(1.445**2)
 
-g.draw_cuboid([0,0,0], [200e8, 220, 220], eps=3.47**2)
+g.draw_cuboid(gdata, [0,0,0], [200e8, 220, 220], foreground=3.47**2)
 
 #x_period = y_period = z_period = 13000
 #g = gridlock.Grid([numpy.arange(3), ]*3,
@@ -60,9 +60,9 @@ g.draw_cuboid([0,0,0], [200e8, 220, 220], eps=3.47**2)
 
 g2 = g.copy()
 g2.shifts = numpy.zeros((6,3))
-g2.grids = [numpy.zeros(g.shape) for _ in range(6)]
+g2data = g2.allocate(0)
 
-epsilon = [g.grids[0],] * 3
+epsilon = [gdata[0],] * 3
 reciprocal_lattice = numpy.diag(1000/numpy.array([x_period, y_period, z_period])) #cols are vectors
 
 pyfftw_load_wisdom(WISDOM_FILEPATH)
@@ -93,8 +93,8 @@ for k0x in [.25]:
     z = 0
     e = v2e(v[0])
     for i in range(3):
-        g2.grids[i] += numpy.real(e[i])
-        g2.grids[i+3] += numpy.imag(e[i])
+        g2data[i] += numpy.real(e[i])
+        g2data[i+3] += numpy.imag(e[i])
 
     f = numpy.sqrt(numpy.real(numpy.abs(n))) # TODO
     print('k0x = {:3g}\n eigval = {}\n f = {}\n'.format(k0x, n, f))
diff --git a/examples/fdfd.py b/examples/fdfd.py
index 47b8a30..42079f8 100644
--- a/examples/fdfd.py
+++ b/examples/fdfd.py
@@ -44,14 +44,17 @@ def test0(solver=generic_solver):
     edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
 
     # #### Create the grid, mask, and draw the device ####
-    grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3)
-    grid.draw_cylinder(surface_normal=gridlock.Direction.z,
+    grid = gridlock.Grid(edge_coords)
+    epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
+    grid.draw_cylinder(epsilon,
+                       surface_normal=2,
                        center=center,
                        radius=max(radii),
                        thickness=th,
                        eps=n_ring**2,
                        num_points=24)
-    grid.draw_cylinder(surface_normal=gridlock.Direction.z,
+    grid.draw_cylinder(epsilon,
+                       surface_normal=2,
                        center=center,
                        radius=min(radii),
                        thickness=th*1.1,
@@ -64,7 +67,7 @@ def test0(solver=generic_solver):
             dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega,
                                                         thickness=pml_thickness)
 
-    J = [numpy.zeros_like(grid.grids[0], dtype=complex) for _ in range(3)]
+    J = [numpy.zeros_like(epsilon[0], dtype=complex) for _ in range(3)]
     J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1
 
 
@@ -74,11 +77,11 @@ def test0(solver=generic_solver):
     sim_args = {
         'omega': omega,
         'dxes': dxes,
-        'epsilon': vec(grid.grids),
+        'epsilon': vec(epsilon),
     }
     x = solver(J=vec(J), **sim_args)
 
-    A = operators.e_full(omega, dxes, vec(grid.grids)).tocsr()
+    A = operators.e_full(omega, dxes, vec(epsilon)).tocsr()
     b = -1j * omega * vec(J)
     print('Norm of the residual is ', norm(A @ x - b))
 
@@ -117,8 +120,9 @@ def test1(solver=generic_solver):
     edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
 
     # #### Create the grid and draw the device ####
-    grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3)
-    grid.draw_cuboid(center=center, dimensions=[8e3, w, th], eps=n_wg**2)
+    grid = gridlock.Grid(edge_coords)
+    epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
+    grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], eps=n_wg**2)
 
     dxes = [grid.dxyz, grid.autoshifted_dxyz()]
     for a in (0, 1, 2):
@@ -139,18 +143,18 @@ def test1(solver=generic_solver):
         'polarity': +1,
     }
 
-    wg_results = waveguide_3d.solve_mode(mode_number=0, omega=omega, epsilon=grid.grids, **wg_args)
+    wg_results = waveguide_3d.solve_mode(mode_number=0, omega=omega, epsilon=epsilon, **wg_args)
     J = waveguide_3d.compute_source(E=wg_results['E'], wavenumber=wg_results['wavenumber'],
-                                    omega=omega, epsilon=grid.grids, **wg_args)
+                                    omega=omega, epsilon=epsilon, **wg_args)
     e_overlap = waveguide_3d.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], **wg_args)
 
-    pecg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3)
-    # pecg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1)
-    # pecg.visualize_isosurface()
+    pecg = numpy.zeros_like(epsilon)
+    # pecg.draw_cuboid(pecg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1)
+    # pecg.visualize_isosurface(pecg)
 
-    pmcg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3)
-    # pmcg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1)
-    # pmcg.visualize_isosurface()
+    pmcg = numpy.zeros_like(epsilon)
+    # grid.draw_cuboid(pmcg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1)
+    # grid.visualize_isosurface(pmcg)
 
     def pcolor(v):
         vmax = numpy.max(numpy.abs(v))
@@ -171,9 +175,9 @@ def test1(solver=generic_solver):
     sim_args = {
         'omega': omega,
         'dxes': dxes,
-        'epsilon': vec(grid.grids),
-        'pec': vec(pecg.grids),
-        'pmc': vec(pmcg.grids),
+        'epsilon': vec(epsilon),
+        'pec': vec(pecg),
+        'pmc': vec(pmcg),
     }
 
     x = solver(J=vec(J), **sim_args)
diff --git a/examples/fdtd.py b/examples/fdtd.py
index 3ffa077..026e16a 100644
--- a/examples/fdtd.py
+++ b/examples/fdtd.py
@@ -105,22 +105,23 @@ def main():
     edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
 
     # #### Create the grid, mask, and draw the device ####
-    grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3)
-    grid.draw_slab(surface_normal=gridlock.Direction.z,
+    grid = gridlock.Grid(edge_coords)
+    epsilon = grid.allocate(n_air**2, dtype=dtype)
+    grid.draw_slab(epsilon,
+                   surface_normal=2,
                    center=[0, 0, 0],
                    thickness=th,
                    eps=n_slab**2)
     mask = perturbed_l3(a, r)
 
-    grid.draw_polygons(surface_normal=gridlock.Direction.z,
+    grid.draw_polygons(epsilon,
+                       surface_normal=2,
                        center=[0, 0, 0],
                        thickness=2 * th,
                        eps=n_air**2,
                        polygons=mask.as_polygons())
 
     print(grid.shape)
-    # #### Create the simulation grid ####
-    epsilon = [eps.astype(dtype) for eps in grid.grids]
 
     dt = .99/numpy.sqrt(3)
     e = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)]
diff --git a/examples/tcyl.py b/examples/tcyl.py
index d106589..590a260 100644
--- a/examples/tcyl.py
+++ b/examples/tcyl.py
@@ -42,8 +42,9 @@ def test1(solver=generic_solver):
     edge_coords[0] = numpy.array([-dx, dx])
 
     # #### Create the grid and draw the device ####
-    grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3)
-    grid.draw_cuboid(center=center, dimensions=[8e3, w, th], eps=n_wg**2)
+    grid = gridlock.Grid(edge_coords)
+    epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
+    grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], eps=n_wg**2)
 
     dxes = [grid.dxyz, grid.autoshifted_dxyz()]
     for a in (1, 2):
@@ -54,7 +55,7 @@ def test1(solver=generic_solver):
     wg_args = {
         'omega': omega,
         'dxes': [(d[1], d[2]) for d in dxes],
-        'epsilon': vec(g.transpose([1, 2, 0]) for g in grid.grids),
+        'epsilon': vec(g.transpose([1, 2, 0]) for g in epsilon),
         'r0': r0,
     }
 

From d42a625e5f0fa5e5119a20c8a6a6149d30b21acf Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 12:43:26 -0700
Subject: [PATCH 264/437] typing updates

---
 examples/fdfd.py     |   2 +-
 meanas/fdfd/bloch.py | 134 +++++++++++++++++++++++--------------------
 2 files changed, 73 insertions(+), 63 deletions(-)

diff --git a/examples/fdfd.py b/examples/fdfd.py
index 42079f8..4612ba0 100644
--- a/examples/fdfd.py
+++ b/examples/fdfd.py
@@ -156,7 +156,7 @@ def test1(solver=generic_solver):
     # grid.draw_cuboid(pmcg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1)
     # grid.visualize_isosurface(pmcg)
 
-    def pcolor(v):
+    def pcolor(v) -> None:
         vmax = numpy.max(numpy.abs(v))
         pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax)
         pyplot.axis('equal')
diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index ed6ca39..040c867 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -82,9 +82,10 @@ This module contains functions for generating and solving the
 
 from typing import Tuple, Callable, Any, List, Optional, cast
 import logging
-import numpy                            # type: ignore
-from numpy import pi, real, trace       # type: ignore
-from numpy.fft import fftfreq           # type: ignore
+import numpy
+from numpy import pi, real, trace
+from numpy.fft import fftfreq
+from numpy.typing import NDArray, ArrayLike
 import scipy                            # type: ignore
 import scipy.optimize                   # type: ignore
 from scipy.linalg import norm           # type: ignore
@@ -109,21 +110,22 @@ try:
         'planner_effort': 'FFTW_EXHAUSTIVE',
         }
 
-    def fftn(*args: Any, **kwargs: Any) -> numpy.ndarray:
+    def fftn(*args: Any, **kwargs: Any) -> NDArray[numpy.float64]:
         return pyfftw.interfaces.numpy_fft.fftn(*args, **kwargs, **fftw_args)
 
-    def ifftn(*args: Any, **kwargs: Any) -> numpy.ndarray:
+    def ifftn(*args: Any, **kwargs: Any) -> NDArray[numpy.float64]:
         return pyfftw.interfaces.numpy_fft.ifftn(*args, **kwargs, **fftw_args)
 
 except ImportError:
-    from numpy.fft import fftn, ifftn       # type: ignore
+    from numpy.fft import fftn, ifftn
     logger.info('Using numpy fft')
 
 
-def generate_kmn(k0: numpy.ndarray,
-                 G_matrix: numpy.ndarray,
-                 shape: numpy.ndarray
-                 ) -> Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]:
+def generate_kmn(
+        k0: ArrayLike,
+        G_matrix: ArrayLike,
+        shape: ArrayLike,
+        ) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
     """
     Generate a (k, m, n) orthogonal basis for each k-vector in the simulation grid.
 
@@ -162,11 +164,12 @@ def generate_kmn(k0: numpy.ndarray,
     return k_mag, m, n
 
 
-def maxwell_operator(k0: numpy.ndarray,
-                     G_matrix: numpy.ndarray,
-                     epsilon: fdfield_t,
-                     mu: fdfield_t = None
-                     ) -> Callable[[numpy.ndarray], numpy.ndarray]:
+def maxwell_operator(
+        k0: ArrayLike,
+        G_matrix: ArrayLike,
+        epsilon: fdfield_t,
+        mu: Optional[fdfield_t] = None
+        ) -> Callable[[NDArray[numpy.float64]], NDArray[numpy.float64]]:
     """
     Generate the Maxwell operator
 
@@ -199,7 +202,7 @@ def maxwell_operator(k0: numpy.ndarray,
     if mu is not None:
         mu = numpy.stack(mu, 3)
 
-    def operator(h: numpy.ndarray) -> numpy.ndarray:
+    def operator(h: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
         """
         Maxwell operator for Bloch eigenmode simulation.
 
@@ -244,10 +247,11 @@ def maxwell_operator(k0: numpy.ndarray,
     return operator
 
 
-def hmn_2_exyz(k0: numpy.ndarray,
-               G_matrix: numpy.ndarray,
-               epsilon: fdfield_t,
-               ) -> Callable[[numpy.ndarray], fdfield_t]:
+def hmn_2_exyz(
+        k0: ArrayLike,
+        G_matrix: ArrayLike,
+        epsilon: fdfield_t,
+        ) -> Callable[[NDArray[numpy.float64]], fdfield_t]:
     """
     Generate an operator which converts a vectorized spatial-frequency-space
      `h_mn` into an E-field distribution, i.e.
@@ -272,7 +276,7 @@ def hmn_2_exyz(k0: numpy.ndarray,
 
     k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
-    def operator(h: numpy.ndarray) -> fdfield_t:
+    def operator(h: NDArray[numpy.float64]) -> fdfield_t:
         hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
         d_xyz = (n * hin_m
                - m * hin_n) * k_mag
@@ -283,10 +287,11 @@ def hmn_2_exyz(k0: numpy.ndarray,
     return operator
 
 
-def hmn_2_hxyz(k0: numpy.ndarray,
-               G_matrix: numpy.ndarray,
-               epsilon: fdfield_t
-               ) -> Callable[[numpy.ndarray], fdfield_t]:
+def hmn_2_hxyz(
+        k0: ArrayLike,
+        G_matrix: ArrayLike,
+        epsilon: fdfield_t
+        ) -> Callable[[NDArray[numpy.float64]], fdfield_t]:
     """
     Generate an operator which converts a vectorized spatial-frequency-space
      `h_mn` into an H-field distribution, i.e.
@@ -309,7 +314,7 @@ def hmn_2_hxyz(k0: numpy.ndarray,
     shape = epsilon[0].shape + (1,)
     _k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
-    def operator(h: numpy.ndarray) -> fdfield_t:
+    def operator(h: NDArray[numpy.float64]) -> fdfield_t:
         hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
         h_xyz = (m * hin_m
                + n * hin_n)
@@ -318,11 +323,12 @@ def hmn_2_hxyz(k0: numpy.ndarray,
     return operator
 
 
-def inverse_maxwell_operator_approx(k0: numpy.ndarray,
-                                    G_matrix: numpy.ndarray,
-                                    epsilon: fdfield_t,
-                                    mu: fdfield_t = None
-                                    ) -> Callable[[numpy.ndarray], numpy.ndarray]:
+def inverse_maxwell_operator_approx(
+        k0: ArrayLike,
+        G_matrix: ArrayLike,
+        epsilon: fdfield_t,
+        mu: Optional[fdfield_t] = None,
+        ) -> Callable[[NDArray[numpy.float64]], NDArray[numpy.float64]]:
     """
     Generate an approximate inverse of the Maxwell operator,
 
@@ -351,7 +357,7 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray,
     if mu is not None:
         mu = numpy.stack(mu, 3)
 
-    def operator(h: numpy.ndarray) -> numpy.ndarray:
+    def operator(h: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
         """
         Approximate inverse Maxwell operator for Bloch eigenmode simulation.
 
@@ -397,17 +403,18 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray,
     return operator
 
 
-def find_k(frequency: float,
-           tolerance: float,
-           direction: numpy.ndarray,
-           G_matrix: numpy.ndarray,
-           epsilon: fdfield_t,
-           mu: fdfield_t = None,
-           band: int = 0,
-           k_min: float = 0,
-           k_max: float = 0.5,
-           solve_callback: Callable = None
-           ) -> Tuple[numpy.ndarray, float]:
+def find_k(
+        frequency: float,
+        tolerance: float,
+        direction: ArrayLike,
+        G_matrix: ArrayLike,
+        epsilon: fdfield_t,
+        mu: Optional[fdfield_t] = None,
+        band: int = 0,
+        k_min: float = 0,
+        k_max: float = 0.5,
+        solve_callback: Optional[Callable] = None
+        ) -> Tuple[NDArray[numpy.float64], float]:
     """
     Search for a bloch vector that has a given frequency.
 
@@ -429,7 +436,7 @@ def find_k(frequency: float,
 
     direction = numpy.array(direction) / norm(direction)
 
-    def get_f(k0_mag: float, band: int = 0) -> numpy.ndarray:
+    def get_f(k0_mag: float, band: int = 0) -> float:
         k0 = direction * k0_mag
         n, v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu)
         f = numpy.sqrt(numpy.abs(numpy.real(n[band])))
@@ -437,23 +444,26 @@ def find_k(frequency: float,
             solve_callback(k0_mag, n, v, f)
         return f
 
-    res = scipy.optimize.minimize_scalar(lambda x: abs(get_f(x, band) - frequency),
-                                         (k_min + k_max) / 2,
-                                         method='Bounded',
-                                         bounds=(k_min, k_max),
-                                         options={'xatol': abs(tolerance)})
+    res = scipy.optimize.minimize_scalar(
+        lambda x: abs(get_f(x, band) - frequency),
+        (k_min + k_max) / 2,
+        method='Bounded',
+        bounds=(k_min, k_max),
+        options={'xatol': abs(tolerance)},
+        )
     return res.x * direction, res.fun + frequency
 
 
-def eigsolve(num_modes: int,
-             k0: numpy.ndarray,
-             G_matrix: numpy.ndarray,
-             epsilon: fdfield_t,
-             mu: fdfield_t = None,
-             tolerance: float = 1e-20,
-             max_iters: int = 10000,
-             reset_iters: int = 100,
-             ) -> Tuple[numpy.ndarray, numpy.ndarray]:
+def eigsolve(
+        num_modes: int,
+        k0: ArrayLike,
+        G_matrix: ArrayLike,
+        epsilon: fdfield_t,
+        mu: Optional[fdfield_t] = None,
+        tolerance: float = 1e-20,
+        max_iters: int = 10000,
+        reset_iters: int = 100,
+        ) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
     """
     Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector
      k0 of the specified structure.
@@ -625,7 +635,7 @@ def eigsolve(num_modes: int,
         theta = result.x
 
         improvement = numpy.abs(E - new_E) * 2 / numpy.abs(E + new_E)
-        logger.info('linmin improvement {}'.format(improvement))
+        logger.info(f'linmin improvement {improvement}')
         Z *= numpy.cos(theta)
         Z += D * numpy.sin(theta)
 
@@ -651,7 +661,7 @@ def eigsolve(num_modes: int,
         f = numpy.sqrt(-numpy.real(n))
         df = numpy.sqrt(-numpy.real(n + eigness))
         neff_err = kmag * (1 / df - 1 / f)
-        logger.info('eigness {}: {}\n neff_err: {}'.format(i, eigness, neff_err))
+        logger.info(f'eigness {i}: {eigness}\n neff_err: {neff_err}')
 
     order = numpy.argsort(numpy.abs(eigvals))
     return eigvals[order], eigvecs.T[order]
@@ -685,9 +695,9 @@ def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_to
         return x, fx, dfx
 '''
 
-def _rtrace_AtB(A: numpy.ndarray, B: numpy.ndarray) -> numpy.ndarray:
+def _rtrace_AtB(A: NDArray[numpy.float64], B: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
     return real(numpy.sum(A.conj() * B))
 
-def _symmetrize(A: numpy.ndarray) -> numpy.ndarray:
+def _symmetrize(A: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
     return (A + A.conj().T) * 0.5
 

From faecc79179c63651fcddea41d0b15f1718b4d15f Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 14:32:40 -0700
Subject: [PATCH 265/437] typing and formatting updates

---
 meanas/eigensolvers.py         |  46 +++++----
 meanas/fdfd/bloch.py           |  20 ++--
 meanas/fdfd/farfield.py        |  32 +++---
 meanas/fdfd/functional.py      |  49 +++++----
 meanas/fdfd/operators.py       |  88 ++++++++--------
 meanas/fdfd/scpml.py           |  53 ++++++----
 meanas/fdfd/solvers.py         |  47 +++++----
 meanas/fdfd/waveguide_2d.py    | 182 ++++++++++++++++++---------------
 meanas/fdfd/waveguide_3d.py    |  83 ++++++++-------
 meanas/fdfd/waveguide_cyl.py   |  34 +++---
 meanas/fdmath/functional.py    |  29 ++++--
 meanas/fdmath/operators.py     |  37 +++++--
 meanas/fdmath/types.py         |  39 +++----
 meanas/fdmath/vectorization.py |  13 +--
 meanas/fdtd/boundaries.py      |   7 +-
 meanas/fdtd/energy.py          | 114 +++++++++++----------
 meanas/fdtd/pml.py             |   5 +-
 meanas/test/conftest.py        |  23 +++--
 meanas/test/test_fdfd.py       |  41 ++++----
 meanas/test/test_fdfd_pml.py   |  79 ++++++++------
 meanas/test/test_fdtd.py       |  61 ++++++-----
 meanas/test/utils.py           |  25 +++--
 22 files changed, 621 insertions(+), 486 deletions(-)

diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py
index 8c1739e..aa8b9ba 100644
--- a/meanas/eigensolvers.py
+++ b/meanas/eigensolvers.py
@@ -2,16 +2,18 @@
 Solvers for eigenvalue / eigenvector problems
 """
 from typing import Tuple, Callable, Optional, Union
-import numpy                          # type: ignore
-from numpy.linalg import norm         # type: ignore
+import numpy
+from numpy.typing import NDArray, ArrayLike
+from numpy.linalg import norm
 from scipy import sparse              # type: ignore
 import scipy.sparse.linalg as spalg   # type: ignore
 
 
-def power_iteration(operator: sparse.spmatrix,
-                    guess_vector: Optional[numpy.ndarray] = None,
-                    iterations: int = 20,
-                    ) -> Tuple[complex, numpy.ndarray]:
+def power_iteration(
+        operator: sparse.spmatrix,
+        guess_vector: Optional[NDArray[numpy.float64]] = None,
+        iterations: int = 20,
+        ) -> Tuple[complex, NDArray[numpy.float64]]:
     """
     Use power iteration to estimate the dominant eigenvector of a matrix.
 
@@ -37,12 +39,13 @@ def power_iteration(operator: sparse.spmatrix,
     return lm_eigval, v
 
 
-def rayleigh_quotient_iteration(operator: Union[sparse.spmatrix, spalg.LinearOperator],
-                                guess_vector: numpy.ndarray,
-                                iterations: int = 40,
-                                tolerance: float = 1e-13,
-                                solver: Optional[Callable[..., numpy.ndarray]] = None,
-                                ) -> Tuple[complex, numpy.ndarray]:
+def rayleigh_quotient_iteration(
+        operator: Union[sparse.spmatrix, spalg.LinearOperator],
+        guess_vector: NDArray[numpy.float64],
+        iterations: int = 40,
+        tolerance: float = 1e-13,
+        solver: Optional[Callable[..., NDArray[numpy.float64]]] = None,
+        ) -> Tuple[complex, NDArray[numpy.float64]]:
     """
     Use Rayleigh quotient iteration to refine an eigenvector guess.
 
@@ -69,11 +72,13 @@ def rayleigh_quotient_iteration(operator: Union[sparse.spmatrix, spalg.LinearOpe
             solver = spalg.spsolve
     except TypeError:
         def shift(eigval: float) -> spalg.LinearOperator:
-            return spalg.LinearOperator(shape=operator.shape,
-                                        dtype=operator.dtype,
-                                        matvec=lambda v: eigval * v)
+            return spalg.LinearOperator(
+                    shape=operator.shape,
+                    dtype=operator.dtype,
+                    matvec=lambda v: eigval * v,
+                    )
         if solver is None:
-            def solver(A: spalg.LinearOperator, b: numpy.ndarray) -> numpy.ndarray:
+            def solver(A: spalg.LinearOperator, b: ArrayLike) -> NDArray[numpy.float64]:
                 return spalg.bicgstab(A, b)[0]
     assert(solver is not None)
 
@@ -90,10 +95,11 @@ def rayleigh_quotient_iteration(operator: Union[sparse.spmatrix, spalg.LinearOpe
     return eigval, v
 
 
-def signed_eigensolve(operator: Union[sparse.spmatrix, spalg.LinearOperator],
-                      how_many: int,
-                      negative: bool = False,
-                      ) -> Tuple[numpy.ndarray, numpy.ndarray]:
+def signed_eigensolve(
+        operator: Union[sparse.spmatrix, spalg.LinearOperator],
+        how_many: int,
+        negative: bool = False,
+        ) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
     """
     Find the largest-magnitude positive-only (or negative-only) eigenvalues and
      eigenvectors of the provided matrix.
diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 040c867..872781c 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -80,7 +80,7 @@ This module contains functions for generating and solving the
 
 '''
 
-from typing import Tuple, Callable, Any, List, Optional, cast
+from typing import Tuple, Callable, Any, List, Optional, cast, Union
 import logging
 import numpy
 from numpy import pi, real, trace
@@ -433,11 +433,10 @@ def find_k(
         `(k, actual_frequency)`
         The found k-vector and its frequency.
     """
-
     direction = numpy.array(direction) / norm(direction)
 
     def get_f(k0_mag: float, band: int = 0) -> float:
-        k0 = direction * k0_mag
+        k0 = direction * k0_mag                         # type: ignore
         n, v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu)
         f = numpy.sqrt(numpy.abs(numpy.real(n[band])))
         if solve_callback:
@@ -482,6 +481,8 @@ def eigsolve(
         `(eigenvalues, eigenvectors)` where `eigenvalues[i]` corresponds to the
         vector `eigenvectors[i, :]`
     """
+    k0 = numpy.array(k0, copy=False)
+
     h_size = 2 * epsilon[0].size
 
     kmag = norm(G_matrix @ k0)
@@ -497,9 +498,9 @@ def eigsolve(
 
     y_shape = (h_size, num_modes)
 
-    prev_E = 0
-    d_scale = 1
-    prev_traceGtKG = 0
+    prev_E = 0.0
+    d_scale = 1.0
+    prev_traceGtKG = 0.0
     #prev_theta = 0.5
     D = numpy.zeros(shape=y_shape, dtype=complex)
 
@@ -545,7 +546,7 @@ def eigsolve(
 
         if prev_traceGtKG == 0 or i % reset_iters == 0:
             logger.info('CG reset')
-            gamma = 0
+            gamma = 0.0
         else:
             gamma = traceGtKG / prev_traceGtKG
 
@@ -695,7 +696,10 @@ def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_to
         return x, fx, dfx
 '''
 
-def _rtrace_AtB(A: NDArray[numpy.float64], B: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
+def _rtrace_AtB(
+        A: NDArray[numpy.float64],
+        B: Union[NDArray[numpy.float64], float],
+        ) -> float:
     return real(numpy.sum(A.conj() * B))
 
 def _symmetrize(A: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py
index eb53b24..e1a725d 100644
--- a/meanas/fdfd/farfield.py
+++ b/meanas/fdfd/farfield.py
@@ -2,19 +2,20 @@
 Functions for performing near-to-farfield transformation (and the reverse).
 """
 from typing import Dict, List, Any
-import numpy            # type: ignore
-from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift     # type: ignore
-from numpy import pi    # type: ignore
+import numpy
+from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift
+from numpy import pi
 
 from ..fdmath import fdfield_t
 
 
-def near_to_farfield(E_near: fdfield_t,
-                     H_near: fdfield_t,
-                     dx: float,
-                     dy: float,
-                     padded_size: List[int] = None
-                     ) -> Dict[str, Any]:
+def near_to_farfield(
+        E_near: fdfield_t,
+        H_near: fdfield_t,
+        dx: float,
+        dy: float,
+        padded_size: List[int] = None
+        ) -> Dict[str, Any]:
     """
     Compute the farfield, i.e. the distribution of the fields after propagation
       through several wavelengths of uniform medium.
@@ -120,12 +121,13 @@ def near_to_farfield(E_near: fdfield_t,
     return outputs
 
 
-def far_to_nearfield(E_far: fdfield_t,
-                     H_far: fdfield_t,
-                     dkx: float,
-                     dky: float,
-                     padded_size: List[int] = None
-                     ) -> Dict[str, Any]:
+def far_to_nearfield(
+        E_far: fdfield_t,
+        H_far: fdfield_t,
+        dkx: float,
+        dky: float,
+        padded_size: List[int] = None
+        ) -> Dict[str, Any]:
     """
     Compute the farfield, i.e. the distribution of the fields after propagation
       through several wavelengths of uniform medium.
diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py
index f16151f..9a83910 100644
--- a/meanas/fdfd/functional.py
+++ b/meanas/fdfd/functional.py
@@ -5,8 +5,8 @@ Functional versions of many FDFD operators. These can be useful for performing
 The functions generated here expect `fdfield_t` inputs with shape (3, X, Y, Z),
 e.g. E = [E_x, E_y, E_z] where each component has shape (X, Y, Z)
 """
-from typing import Callable, Tuple
-import numpy        # type: ignore
+from typing import Callable, Tuple, Optional
+import numpy
 
 from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t
 from ..fdmath.functional import curl_forward, curl_back
@@ -15,11 +15,12 @@ from ..fdmath.functional import curl_forward, curl_back
 __author__ = 'Jan Petykiewicz'
 
 
-def e_full(omega: complex,
-           dxes: dx_lists_t,
-           epsilon: fdfield_t,
-           mu: fdfield_t = None
-           ) -> fdfield_updater_t:
+def e_full(
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: fdfield_t,
+        mu: fdfield_t = None
+        ) -> fdfield_updater_t:
     """
     Wave operator for use with E-field. See `operators.e_full` for details.
 
@@ -50,11 +51,12 @@ def e_full(omega: complex,
         return op_mu
 
 
-def eh_full(omega: complex,
-            dxes: dx_lists_t,
-            epsilon: fdfield_t,
-            mu: fdfield_t = None
-            ) -> Callable[[fdfield_t, fdfield_t], Tuple[fdfield_t, fdfield_t]]:
+def eh_full(
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: fdfield_t,
+        mu: fdfield_t = None
+        ) -> Callable[[fdfield_t, fdfield_t], Tuple[fdfield_t, fdfield_t]]:
     """
     Wave operator for full (both E and H) field representation.
     See `operators.eh_full`.
@@ -86,9 +88,10 @@ def eh_full(omega: complex,
         return op_mu
 
 
-def e2h(omega: complex,
+def e2h(
+        omega: complex,
         dxes: dx_lists_t,
-        mu: fdfield_t = None,
+        mu: Optional[fdfield_t] = None,
         ) -> fdfield_updater_t:
     """
     Utility operator for converting the `E` field into the `H` field.
@@ -117,9 +120,10 @@ def e2h(omega: complex,
         return e2h_mu
 
 
-def m2j(omega: complex,
+def m2j(
+        omega: complex,
         dxes: dx_lists_t,
-        mu: fdfield_t = None,
+        mu: Optional[fdfield_t] = None,
         ) -> fdfield_updater_t:
     """
     Utility operator for converting magnetic current `M` distribution
@@ -151,12 +155,13 @@ def m2j(omega: complex,
         return m2j_mu
 
 
-def e_tfsf_source(TF_region: fdfield_t,
-                  omega: complex,
-                  dxes: dx_lists_t,
-                  epsilon: fdfield_t,
-                  mu: fdfield_t = None,
-                  ) -> fdfield_updater_t:
+def e_tfsf_source(
+        TF_region: fdfield_t,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: fdfield_t,
+        mu: Optional[fdfield_t] = None,
+        ) -> fdfield_updater_t:
     """
     Operator that turns an E-field distribution into a total-field/scattered-field
     (TFSF) source.
diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py
index 7f0a7ba..f36080b 100644
--- a/meanas/fdfd/operators.py
+++ b/meanas/fdfd/operators.py
@@ -28,7 +28,7 @@ The following operators are included:
 """
 
 from typing import Tuple, Optional
-import numpy                        # type: ignore
+import numpy
 import scipy.sparse as sparse       # type: ignore
 
 from ..fdmath import vec, dx_lists_t, vfdfield_t
@@ -38,13 +38,14 @@ from ..fdmath.operators import shift_with_mirror, shift_circ, curl_forward, curl
 __author__ = 'Jan Petykiewicz'
 
 
-def e_full(omega: complex,
-           dxes: dx_lists_t,
-           epsilon: vfdfield_t,
-           mu: Optional[vfdfield_t] = None,
-           pec: Optional[vfdfield_t] = None,
-           pmc: Optional[vfdfield_t] = None,
-           ) -> sparse.spmatrix:
+def e_full(
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None,
+        pec: Optional[vfdfield_t] = None,
+        pmc: Optional[vfdfield_t] = None,
+        ) -> sparse.spmatrix:
     """
     Wave operator
      $$ \\nabla \\times (\\frac{1}{\\mu} \\nabla \\times) - \\Omega^2 \\epsilon $$
@@ -96,8 +97,9 @@ def e_full(omega: complex,
     return op
 
 
-def e_full_preconditioners(dxes: dx_lists_t
-                           ) -> Tuple[sparse.spmatrix, sparse.spmatrix]:
+def e_full_preconditioners(
+        dxes: dx_lists_t,
+        ) -> Tuple[sparse.spmatrix, sparse.spmatrix]:
     """
     Left and right preconditioners `(Pl, Pr)` for symmetrizing the `e_full` wave operator.
 
@@ -122,13 +124,14 @@ def e_full_preconditioners(dxes: dx_lists_t
     return P_left, P_right
 
 
-def h_full(omega: complex,
-           dxes: dx_lists_t,
-           epsilon: vfdfield_t,
-           mu: Optional[vfdfield_t] = None,
-           pec: Optional[vfdfield_t] = None,
-           pmc: Optional[vfdfield_t] = None,
-           ) -> sparse.spmatrix:
+def h_full(
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None,
+        pec: Optional[vfdfield_t] = None,
+        pmc: Optional[vfdfield_t] = None,
+        ) -> sparse.spmatrix:
     """
     Wave operator
      $$ \\nabla \\times (\\frac{1}{\\epsilon} \\nabla \\times) - \\omega^2 \\mu $$
@@ -178,13 +181,14 @@ def h_full(omega: complex,
     return A
 
 
-def eh_full(omega: complex,
-            dxes: dx_lists_t,
-            epsilon: vfdfield_t,
-            mu: Optional[vfdfield_t] = None,
-            pec: Optional[vfdfield_t] = None,
-            pmc: Optional[vfdfield_t] = None
-            ) -> sparse.spmatrix:
+def eh_full(
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None,
+        pec: Optional[vfdfield_t] = None,
+        pmc: Optional[vfdfield_t] = None,
+        ) -> sparse.spmatrix:
     """
     Wave operator for `[E, H]` field representation. This operator implements Maxwell's
      equations without cancelling out either E or H. The operator is
@@ -247,7 +251,8 @@ def eh_full(omega: complex,
     return A
 
 
-def e2h(omega: complex,
+def e2h(
+        omega: complex,
         dxes: dx_lists_t,
         mu: Optional[vfdfield_t] = None,
         pmc: Optional[vfdfield_t] = None,
@@ -278,9 +283,10 @@ def e2h(omega: complex,
     return op
 
 
-def m2j(omega: complex,
+def m2j(
+        omega: complex,
         dxes: dx_lists_t,
-        mu: Optional[vfdfield_t] = None
+        mu: Optional[vfdfield_t] = None,
         ) -> sparse.spmatrix:
     """
     Operator for converting a magnetic current M into an electric current J.
@@ -357,12 +363,13 @@ def poynting_h_cross(h: vfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
     return P
 
 
-def e_tfsf_source(TF_region: vfdfield_t,
-                  omega: complex,
-                  dxes: dx_lists_t,
-                  epsilon: vfdfield_t,
-                  mu: Optional[vfdfield_t] = None,
-                  ) -> sparse.spmatrix:
+def e_tfsf_source(
+        TF_region: vfdfield_t,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None,
+        ) -> sparse.spmatrix:
     """
     Operator that turns a desired E-field distribution into a
      total-field/scattered-field (TFSF) source.
@@ -387,13 +394,14 @@ def e_tfsf_source(TF_region: vfdfield_t,
     return (A @ Q - Q @ A) / (-1j * omega)
 
 
-def e_boundary_source(mask: vfdfield_t,
-                      omega: complex,
-                      dxes: dx_lists_t,
-                      epsilon: vfdfield_t,
-                      mu: Optional[vfdfield_t] = None,
-                      periodic_mask_edges: bool = False,
-                      ) -> sparse.spmatrix:
+def e_boundary_source(
+        mask: vfdfield_t,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None,
+        periodic_mask_edges: bool = False,
+        ) -> sparse.spmatrix:
     """
     Operator that turns an E-field distrubtion into a current (J) distribution
       along the edges (external and internal) of the provided mask. This is just an
diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py
index 67d58ca..0f9c92c 100644
--- a/meanas/fdfd/scpml.py
+++ b/meanas/fdfd/scpml.py
@@ -3,7 +3,9 @@ Functions for creating stretched coordinate perfectly matched layer (PML) absorb
 """
 
 from typing import Sequence, Union, Callable, Optional, List
-import numpy            # type: ignore
+
+import numpy
+from numpy.typing import ArrayLike, NDArray
 
 
 __author__ = 'Jan Petykiewicz'
@@ -13,9 +15,10 @@ s_function_t = Callable[[float], float]
 """Typedef for s-functions, see `prepare_s_function()`"""
 
 
-def prepare_s_function(ln_R: float = -16,
-                       m: float = 4
-                       ) -> s_function_t:
+def prepare_s_function(
+        ln_R: float = -16,
+        m: float = 4
+        ) -> s_function_t:
     """
     Create an s_function to pass to the SCPML functions. This is used when you would like to
     customize the PML parameters.
@@ -29,18 +32,19 @@ def prepare_s_function(ln_R: float = -16,
         of the cell width; needs to be divided by `sqrt(epilon_effective) * real(omega))`
         before use.
     """
-    def s_factor(distance: numpy.ndarray) -> numpy.ndarray:
+    def s_factor(distance: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
         s_max = (m + 1) * ln_R / 2  # / 2 because we assume periodic boundaries
         return s_max * (distance ** m)
     return s_factor
 
 
-def uniform_grid_scpml(shape: Union[numpy.ndarray, Sequence[int]],
-                       thicknesses: Union[numpy.ndarray, Sequence[int]],
-                       omega: float,
-                       epsilon_effective: float = 1.0,
-                       s_function: Optional[s_function_t] = None,
-                       ) -> List[List[numpy.ndarray]]:
+def uniform_grid_scpml(
+        shape: ArrayLike,    # ints
+        thicknesses: ArrayLike,  # ints
+        omega: float,
+        epsilon_effective: float = 1.0,
+        s_function: Optional[s_function_t] = None,
+        ) -> List[List[NDArray[numpy.float64]]]:
     """
     Create dx arrays for a uniform grid with a cell width of 1 and a pml.
 
@@ -67,7 +71,11 @@ def uniform_grid_scpml(shape: Union[numpy.ndarray, Sequence[int]],
         s_function = prepare_s_function()
 
     # Normalized distance to nearest boundary
-    def ll(u: numpy.ndarray, n: numpy.ndarray, t: numpy.ndarray) -> numpy.ndarray:
+    def ll(
+            u: NDArray[numpy.float64],
+            n: NDArray[numpy.float64],
+            t: NDArray[numpy.float64],
+            ) -> NDArray[numpy.float64]:
         return ((t - u).clip(0) + (u - (n - t)).clip(0)) / t
 
     dx_a = [numpy.array(numpy.inf)] * 3
@@ -88,14 +96,15 @@ def uniform_grid_scpml(shape: Union[numpy.ndarray, Sequence[int]],
     return [dx_a, dx_b]
 
 
-def stretch_with_scpml(dxes: List[List[numpy.ndarray]],
-                       axis: int,
-                       polarity: int,
-                       omega: float,
-                       epsilon_effective: float = 1.0,
-                       thickness: int = 10,
-                       s_function: Optional[s_function_t] = None,
-                       ) -> List[List[numpy.ndarray]]:
+def stretch_with_scpml(
+        dxes: List[List[NDArray[numpy.float64]]],
+        axis: int,
+        polarity: int,
+        omega: float,
+        epsilon_effective: float = 1.0,
+        thickness: int = 10,
+        s_function: Optional[s_function_t] = None,
+        ) -> List[List[NDArray[numpy.float64]]]:
     """
         Stretch dxes to contain a stretched-coordinate PML (SCPML) in one direction along one axis.
 
@@ -132,7 +141,7 @@ def stretch_with_scpml(dxes: List[List[numpy.ndarray]],
         bound = pos[thickness]
         d = bound - pos[0]
 
-        def l_d(x: numpy.ndarray) -> numpy.ndarray:
+        def l_d(x: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
             return (bound - x) / (bound - pos[0])
 
         slc = slice(thickness)
@@ -142,7 +151,7 @@ def stretch_with_scpml(dxes: List[List[numpy.ndarray]],
         bound = pos[-thickness - 1]
         d = pos[-1] - bound
 
-        def l_d(x: numpy.ndarray) -> numpy.ndarray:
+        def l_d(x: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
             return (x - bound) / (pos[-1] - bound)
 
         if thickness == 0:
diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py
index c9f1ac5..0688966 100644
--- a/meanas/fdfd/solvers.py
+++ b/meanas/fdfd/solvers.py
@@ -2,11 +2,12 @@
 Solvers and solver interface for FDFD problems.
 """
 
-from typing import Callable, Dict, Any
+from typing import Callable, Dict, Any, Optional
 import logging
 
-import numpy                        # type: ignore
-from numpy.linalg import norm       # type: ignore
+import numpy
+from numpy.typing import ArrayLike, NDArray
+from numpy.linalg import norm
 import scipy.sparse.linalg          # type: ignore
 
 from ..fdmath import dx_lists_t, vfdfield_t
@@ -16,10 +17,11 @@ from . import operators
 logger = logging.getLogger(__name__)
 
 
-def _scipy_qmr(A: scipy.sparse.csr_matrix,
-               b: numpy.ndarray,
-               **kwargs: Any,
-               ) -> numpy.ndarray:
+def _scipy_qmr(
+        A: scipy.sparse.csr_matrix,
+        b: ArrayLike,
+        **kwargs: Any,
+        ) -> NDArray[numpy.float64]:
     """
     Wrapper for scipy.sparse.linalg.qmr
 
@@ -37,14 +39,14 @@ def _scipy_qmr(A: scipy.sparse.csr_matrix,
     '''
     ii = 0
 
-    def log_residual(xk: numpy.ndarray) -> None:
+    def log_residual(xk: ArrayLike) -> None:
         nonlocal ii
         ii += 1
         if ii % 100 == 0:
             logger.info('Solver residual at iteration {} : {}'.format(ii, norm(A @ xk - b)))
 
     if 'callback' in kwargs:
-        def augmented_callback(xk: numpy.ndarray) -> None:
+        def augmented_callback(xk: ArrayLike) -> None:
             log_residual(xk)
             kwargs['callback'](xk)
 
@@ -60,17 +62,18 @@ def _scipy_qmr(A: scipy.sparse.csr_matrix,
     return x
 
 
-def generic(omega: complex,
-            dxes: dx_lists_t,
-            J: vfdfield_t,
-            epsilon: vfdfield_t,
-            mu: vfdfield_t = None,
-            pec: vfdfield_t = None,
-            pmc: vfdfield_t = None,
-            adjoint: bool = False,
-            matrix_solver: Callable[..., numpy.ndarray] = _scipy_qmr,
-            matrix_solver_opts: Dict[str, Any] = None,
-            ) -> vfdfield_t:
+def generic(
+        omega: complex,
+        dxes: dx_lists_t,
+        J: vfdfield_t,
+        epsilon: vfdfield_t,
+        mu: vfdfield_t = None,
+        pec: vfdfield_t = None,
+        pmc: vfdfield_t = None,
+        adjoint: bool = False,
+        matrix_solver: Callable[..., ArrayLike] = _scipy_qmr,
+        matrix_solver_opts: Optional[Dict[str, Any]] = None,
+        ) -> vfdfield_t:
     """
     Conjugate gradient FDFD solver using CSR sparse matrices.
 
@@ -90,8 +93,8 @@ def generic(omega: complex,
         adjoint: If true, solves the adjoint problem.
         matrix_solver: Called as `matrix_solver(A, b, **matrix_solver_opts) -> x`,
                 where `A`: `scipy.sparse.csr_matrix`;
-                      `b`: `numpy.ndarray`;
-                      `x`: `numpy.ndarray`;
+                      `b`: `ArrayLike`;
+                      `x`: `ArrayLike`;
                 Default is a wrapped version of `scipy.sparse.linalg.qmr()`
                  which doesn't return convergence info and logs the residual
                  every 100 iterations.
diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 39463bf..177d8d3 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -179,8 +179,9 @@ to account for numerical dispersion if the result is introduced into a space wit
 # TODO update module docs
 
 from typing import List, Tuple, Optional, Any
-import numpy                        # type: ignore
-from numpy.linalg import norm       # type: ignore
+import numpy
+from numpy.typing import NDArray, ArrayLike
+from numpy.linalg import norm
 import scipy.sparse as sparse       # type: ignore
 
 from ..fdmath.operators import deriv_forward, deriv_back, cross
@@ -191,11 +192,12 @@ from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
 __author__ = 'Jan Petykiewicz'
 
 
-def operator_e(omega: complex,
-               dxes: dx_lists_t,
-               epsilon: vfdfield_t,
-               mu: Optional[vfdfield_t] = None,
-               ) -> sparse.spmatrix:
+def operator_e(
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None,
+        ) -> sparse.spmatrix:
     """
     Waveguide operator of the form
 
@@ -257,11 +259,12 @@ def operator_e(omega: complex,
     return op
 
 
-def operator_h(omega: complex,
-               dxes: dx_lists_t,
-               epsilon: vfdfield_t,
-               mu: Optional[vfdfield_t] = None,
-               ) -> sparse.spmatrix:
+def operator_h(
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None,
+        ) -> sparse.spmatrix:
     """
     Waveguide operator of the form
 
@@ -324,14 +327,15 @@ def operator_h(omega: complex,
     return op
 
 
-def normalized_fields_e(e_xy: numpy.ndarray,
-                        wavenumber: complex,
-                        omega: complex,
-                        dxes: dx_lists_t,
-                        epsilon: vfdfield_t,
-                        mu: Optional[vfdfield_t] = None,
-                        prop_phase: float = 0,
-                        ) -> Tuple[vfdfield_t, vfdfield_t]:
+def normalized_fields_e(
+        e_xy: ArrayLike,
+        wavenumber: complex,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None,
+        prop_phase: float = 0,
+        ) -> Tuple[vfdfield_t, vfdfield_t]:
     """
     Given a vector `e_xy` containing the vectorized E_x and E_y fields,
      returns normalized, vectorized E and H fields for the system.
@@ -358,14 +362,15 @@ def normalized_fields_e(e_xy: numpy.ndarray,
     return e_norm, h_norm
 
 
-def normalized_fields_h(h_xy: numpy.ndarray,
-                        wavenumber: complex,
-                        omega: complex,
-                        dxes: dx_lists_t,
-                        epsilon: vfdfield_t,
-                        mu: Optional[vfdfield_t] = None,
-                        prop_phase: float = 0,
-                        ) -> Tuple[vfdfield_t, vfdfield_t]:
+def normalized_fields_h(
+        h_xy: ArrayLike,
+        wavenumber: complex,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None,
+        prop_phase: float = 0,
+        ) -> Tuple[vfdfield_t, vfdfield_t]:
     """
     Given a vector `h_xy` containing the vectorized H_x and H_y fields,
      returns normalized, vectorized E and H fields for the system.
@@ -392,14 +397,15 @@ def normalized_fields_h(h_xy: numpy.ndarray,
     return e_norm, h_norm
 
 
-def _normalized_fields(e: numpy.ndarray,
-                       h: numpy.ndarray,
-                       omega: complex,
-                       dxes: dx_lists_t,
-                       epsilon: vfdfield_t,
-                       mu: Optional[vfdfield_t] = None,
-                       prop_phase: float = 0,
-                       ) -> Tuple[vfdfield_t, vfdfield_t]:
+def _normalized_fields(
+        e: ArrayLike,
+        h: ArrayLike,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None,
+        prop_phase: float = 0,
+        ) -> Tuple[vfdfield_t, vfdfield_t]:
     # TODO documentation
     shape = [s.size for s in dxes[0]]
     dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
@@ -434,12 +440,13 @@ def _normalized_fields(e: numpy.ndarray,
     return e, h
 
 
-def exy2h(wavenumber: complex,
-          omega: complex,
-          dxes: dx_lists_t,
-          epsilon: vfdfield_t,
-          mu: Optional[vfdfield_t] = None
-          ) -> sparse.spmatrix:
+def exy2h(
+        wavenumber: complex,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None
+        ) -> sparse.spmatrix:
     """
     Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
      into a vectorized H containing all three H components
@@ -459,12 +466,13 @@ def exy2h(wavenumber: complex,
     return e2hop @ exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon)
 
 
-def hxy2e(wavenumber: complex,
-          omega: complex,
-          dxes: dx_lists_t,
-          epsilon: vfdfield_t,
-          mu: Optional[vfdfield_t] = None
-          ) -> sparse.spmatrix:
+def hxy2e(
+        wavenumber: complex,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None
+        ) -> sparse.spmatrix:
     """
     Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
      into a vectorized E containing all three E components
@@ -484,10 +492,11 @@ def hxy2e(wavenumber: complex,
     return h2eop @ hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu)
 
 
-def hxy2h(wavenumber: complex,
-          dxes: dx_lists_t,
-          mu: Optional[vfdfield_t] = None
-          ) -> sparse.spmatrix:
+def hxy2h(
+        wavenumber: complex,
+        dxes: dx_lists_t,
+        mu: Optional[vfdfield_t] = None
+        ) -> sparse.spmatrix:
     """
     Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
      into a vectorized H containing all three H components
@@ -517,10 +526,11 @@ def hxy2h(wavenumber: complex,
     return op
 
 
-def exy2e(wavenumber: complex,
-          dxes: dx_lists_t,
-          epsilon: vfdfield_t,
-          ) -> sparse.spmatrix:
+def exy2e(
+        wavenumber: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        ) -> sparse.spmatrix:
     """
     Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
      into a vectorized E containing all three E components
@@ -550,7 +560,8 @@ def exy2e(wavenumber: complex,
     return op
 
 
-def e2h(wavenumber: complex,
+def e2h(
+        wavenumber: complex,
         omega: complex,
         dxes: dx_lists_t,
         mu: Optional[vfdfield_t] = None
@@ -574,7 +585,8 @@ def e2h(wavenumber: complex,
     return op
 
 
-def h2e(wavenumber: complex,
+def h2e(
+        wavenumber: complex,
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t
@@ -636,13 +648,14 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
     return cross([Dbx, Dby, Bz])
 
 
-def h_err(h: vfdfield_t,
-          wavenumber: complex,
-          omega: complex,
-          dxes: dx_lists_t,
-          epsilon: vfdfield_t,
-          mu: vfdfield_t = None
-          ) -> float:
+def h_err(
+        h: vfdfield_t,
+        wavenumber: complex,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: Optional[vfdfield_t] = None
+        ) -> float:
     """
     Calculates the relative error in the H field
 
@@ -670,13 +683,14 @@ def h_err(h: vfdfield_t,
     return norm(op) / norm(h)
 
 
-def e_err(e: vfdfield_t,
-          wavenumber: complex,
-          omega: complex,
-          dxes: dx_lists_t,
-          epsilon: vfdfield_t,
-          mu: vfdfield_t = None
-          ) -> float:
+def e_err(
+        e: vfdfield_t,
+        wavenumber: complex,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: vfdfield_t = Optional[None]
+        ) -> float:
     """
     Calculates the relative error in the E field
 
@@ -703,13 +717,14 @@ def e_err(e: vfdfield_t,
     return norm(op) / norm(e)
 
 
-def solve_modes(mode_numbers: List[int],
-                omega: complex,
-                dxes: dx_lists_t,
-                epsilon: vfdfield_t,
-                mu: vfdfield_t = None,
-                mode_margin: int = 2,
-                ) -> Tuple[numpy.ndarray, List[complex]]:
+def solve_modes(
+        mode_numbers: List[int],
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: vfdfield_t = None,
+        mode_margin: int = 2,
+        ) -> Tuple[NDArray[numpy.float64], List[complex]]:
     """
     Given a 2D region, attempts to solve for the eigenmode with the specified mode numbers.
 
@@ -752,10 +767,11 @@ def solve_modes(mode_numbers: List[int],
     return e_xys, wavenumbers
 
 
-def solve_mode(mode_number: int,
-               *args: Any,
-               **kwargs: Any,
-               ) -> Tuple[vfdfield_t, complex]:
+def solve_mode(
+        mode_number: int,
+        *args: Any,
+        **kwargs: Any,
+        ) -> Tuple[vfdfield_t, complex]:
     """
     Wrapper around `solve_modes()` that solves for a single mode.
 
diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py
index 4a65453..a6a2cba 100644
--- a/meanas/fdfd/waveguide_3d.py
+++ b/meanas/fdfd/waveguide_3d.py
@@ -5,21 +5,23 @@ This module relies heavily on `waveguide_2d` and mostly just transforms
 its parameters into 2D equivalents and expands the results back into 3D.
 """
 from typing import Dict, Optional, Sequence, Union, Any
-import numpy                    # type: ignore
+import numpy
+from numpy.typing import NDArray
 
 from ..fdmath import vec, unvec, dx_lists_t, fdfield_t
 from . import operators, waveguide_2d
 
 
-def solve_mode(mode_number: int,
-               omega: complex,
-               dxes: dx_lists_t,
-               axis: int,
-               polarity: int,
-               slices: Sequence[slice],
-               epsilon: fdfield_t,
-               mu: Optional[fdfield_t] = None,
-               ) -> Dict[str, Union[complex, numpy.ndarray]]:
+def solve_mode(
+        mode_number: int,
+        omega: complex,
+        dxes: dx_lists_t,
+        axis: int,
+        polarity: int,
+        slices: Sequence[slice],
+        epsilon: fdfield_t,
+        mu: Optional[fdfield_t] = None,
+        ) -> Dict[str, Union[complex, NDArray[numpy.float_]]]:
     """
     Given a 3D grid, selects a slice from the grid and attempts to
      solve for an eigenmode propagating through that slice.
@@ -36,7 +38,13 @@ def solve_mode(mode_number: int,
         mu: Magnetic permeability (default 1 everywhere)
 
     Returns:
-        `{'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex}`
+        ```
+        {
+            'E': List[NDArray[numpy.float_]],
+            'H': List[NDArray[numpy.float_]],
+            'wavenumber': complex,
+        }
+        ```
     """
     if mu is None:
         mu = numpy.ones_like(epsilon)
@@ -97,16 +105,17 @@ def solve_mode(mode_number: int,
     return results
 
 
-def compute_source(E: fdfield_t,
-                   wavenumber: complex,
-                   omega: complex,
-                   dxes: dx_lists_t,
-                   axis: int,
-                   polarity: int,
-                   slices: Sequence[slice],
-                   epsilon: fdfield_t,
-                   mu: Optional[fdfield_t] = None,
-                   ) -> fdfield_t:
+def compute_source(
+        E: fdfield_t,
+        wavenumber: complex,
+        omega: complex,
+        dxes: dx_lists_t,
+        axis: int,
+        polarity: int,
+        slices: Sequence[slice],
+        epsilon: fdfield_t,
+        mu: Optional[fdfield_t] = None,
+        ) -> fdfield_t:
     """
     Given an eigenmode obtained by `solve_mode`, returns the current source distribution
     necessary to position a unidirectional source at the slice location.
@@ -142,18 +151,21 @@ def compute_source(E: fdfield_t,
     return J
 
 
-def compute_overlap_e(E: fdfield_t,
-                      wavenumber: complex,
-                      dxes: dx_lists_t,
-                      axis: int,
-                      polarity: int,
-                      slices: Sequence[slice],
-                      ) -> fdfield_t:                 # TODO DOCS
+def compute_overlap_e(
+        E: fdfield_t,
+        wavenumber: complex,
+        dxes: dx_lists_t,
+        axis: int,
+        polarity: int,
+        slices: Sequence[slice],
+        ) -> fdfield_t:                 # TODO DOCS
     """
     Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the
     mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn)
     [assumes reflection symmetry].
 
+    TODO: add reference
+
     Args:
         E: E-field of the mode
         H: H-field of the mode (advanced by half of a Yee cell from E)
@@ -187,13 +199,14 @@ def compute_overlap_e(E: fdfield_t,
     return Etgt
 
 
-def expand_e(E: fdfield_t,
-             wavenumber: complex,
-             dxes: dx_lists_t,
-             axis: int,
-             polarity: int,
-             slices: Sequence[slice],
-             ) -> fdfield_t:
+def expand_e(
+        E: fdfield_t,
+        wavenumber: complex,
+        dxes: dx_lists_t,
+        axis: int,
+        polarity: int,
+        slices: Sequence[slice],
+        ) -> fdfield_t:
     """
     Given an eigenmode obtained by `solve_mode`, expands the E-field from the 2D
     slice where the mode was calculated to the entire domain (along the propagation
diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index da75f0c..a1a0633 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -9,7 +9,7 @@ As the z-dependence is known, all the functions in this file assume a 2D grid
 # TODO update module docs
 
 from typing import Dict, Union
-import numpy                        # type: ignore
+import numpy
 import scipy.sparse as sparse       # type: ignore
 
 from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t
@@ -17,11 +17,12 @@ from ..fdmath.operators import deriv_forward, deriv_back
 from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
 
 
-def cylindrical_operator(omega: complex,
-                         dxes: dx_lists_t,
-                         epsilon: vfdfield_t,
-                         r0: float,
-                         ) -> sparse.spmatrix:
+def cylindrical_operator(
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        r0: float,
+        ) -> sparse.spmatrix:
     """
     Cylindrical coordinate waveguide operator of the form
 
@@ -78,12 +79,13 @@ def cylindrical_operator(omega: complex,
     return op
 
 
-def solve_mode(mode_number: int,
-               omega: complex,
-               dxes: dx_lists_t,
-               epsilon: vfdfield_t,
-               r0: float,
-               ) -> Dict[str, Union[complex, fdfield_t]]:
+def solve_mode(
+        mode_number: int,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        r0: float,
+        ) -> Dict[str, Union[complex, fdfield_t]]:
     """
     TODO: fixup
     Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
@@ -99,7 +101,13 @@ def solve_mode(mode_number: int,
             r within the simulation domain.
 
     Returns:
-        `{'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex}`
+        ```
+        {
+            'E': List[NDArray[numpy.float_]],
+            'H': List[NDArray[numpy.float_]],
+            'wavenumber': complex,
+        }
+        ```
     """
 
     '''
diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py
index a62655d..aad8a33 100644
--- a/meanas/fdmath/functional.py
+++ b/meanas/fdmath/functional.py
@@ -5,13 +5,15 @@ Basic discrete calculus etc.
 """
 from typing import Sequence, Tuple, Optional, Callable
 
-import numpy            # type: ignore
+import numpy
+from numpy.typing import NDArray
 
 from .types import fdfield_t, fdfield_updater_t
 
 
-def deriv_forward(dx_e: Optional[Sequence[numpy.ndarray]] = None
-                  ) -> Tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
+def deriv_forward(
+        dx_e: Optional[Sequence[NDArray[numpy.float_]]] = None,
+        ) -> Tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
     """
     Utility operators for taking discretized derivatives (backward variant).
 
@@ -33,8 +35,9 @@ def deriv_forward(dx_e: Optional[Sequence[numpy.ndarray]] = None
     return derivs
 
 
-def deriv_back(dx_h: Optional[Sequence[numpy.ndarray]] = None
-               ) -> Tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
+def deriv_back(
+        dx_h: Optional[Sequence[NDArray[numpy.float_]]] = None,
+        ) -> Tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
     """
     Utility operators for taking discretized derivatives (forward variant).
 
@@ -56,7 +59,9 @@ def deriv_back(dx_h: Optional[Sequence[numpy.ndarray]] = None
     return derivs
 
 
-def curl_forward(dx_e: Optional[Sequence[numpy.ndarray]] = None) -> fdfield_updater_t:
+def curl_forward(
+        dx_e: Optional[Sequence[NDArray[numpy.float_]]] = None,
+        ) -> fdfield_updater_t:
     """
     Curl operator for use with the E field.
 
@@ -83,7 +88,9 @@ def curl_forward(dx_e: Optional[Sequence[numpy.ndarray]] = None) -> fdfield_upda
     return ce_fun
 
 
-def curl_back(dx_h: Optional[Sequence[numpy.ndarray]] = None) -> fdfield_updater_t:
+def curl_back(
+        dx_h: Optional[Sequence[NDArray[numpy.float_]]] = None,
+        ) -> fdfield_updater_t:
     """
     Create a function which takes the backward curl of a field.
 
@@ -110,7 +117,9 @@ def curl_back(dx_h: Optional[Sequence[numpy.ndarray]] = None) -> fdfield_updater
     return ch_fun
 
 
-def curl_forward_parts(dx_e: Optional[Sequence[numpy.ndarray]] = None) -> Callable:
+def curl_forward_parts(
+        dx_e: Optional[Sequence[NDArray[numpy.float_]]] = None,
+        ) -> Callable:
     Dx, Dy, Dz = deriv_forward(dx_e)
 
     def mkparts_fwd(e: fdfield_t) -> Tuple[Tuple[fdfield_t, ...]]:
@@ -121,7 +130,9 @@ def curl_forward_parts(dx_e: Optional[Sequence[numpy.ndarray]] = None) -> Callab
     return mkparts_fwd
 
 
-def curl_back_parts(dx_h: Optional[Sequence[numpy.ndarray]] = None) -> Callable:
+def curl_back_parts(
+        dx_h: Optional[Sequence[NDArray[numpy.float_]]] = None,
+        ) -> Callable:
     Dx, Dy, Dz = deriv_back(dx_e)
 
     def mkparts_back(h: fdfield_t) -> Tuple[Tuple[fdfield_t, ...]]:
diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py
index 45e0cea..d90261a 100644
--- a/meanas/fdmath/operators.py
+++ b/meanas/fdmath/operators.py
@@ -4,13 +4,18 @@ Matrix operators for finite difference simulations
 Basic discrete calculus etc.
 """
 from typing import Sequence, List
-import numpy                    # type: ignore
+import numpy
+from numpy.typing import NDArray
 import scipy.sparse as sparse   # type: ignore
 
 from .types import vfdfield_t
 
 
-def shift_circ(axis: int, shape: Sequence[int], shift_distance: int = 1) -> sparse.spmatrix:
+def shift_circ(
+        axis: int,
+        shape: Sequence[int],
+        shift_distance: int = 1,
+        ) -> sparse.spmatrix:
     """
     Utility operator for performing a circular shift along a specified axis by a
      specified number of elements.
@@ -46,7 +51,11 @@ def shift_circ(axis: int, shape: Sequence[int], shift_distance: int = 1) -> spar
     return d
 
 
-def shift_with_mirror(axis: int, shape: Sequence[int], shift_distance: int = 1) -> sparse.spmatrix:
+def shift_with_mirror(
+        axis: int,
+        shape: Sequence[int],
+        shift_distance: int = 1,
+        ) -> sparse.spmatrix:
     """
     Utility operator for performing an n-element shift along a specified axis, with mirror
     boundary conditions applied to the cells beyond the receding edge.
@@ -67,7 +76,7 @@ def shift_with_mirror(axis: int, shape: Sequence[int], shift_distance: int = 1)
         raise Exception('Shift ({}) is too large for axis {} of size {}'.format(
                         shift_distance, axis, shape[axis]))
 
-    def mirrored_range(n: int, s: int) -> numpy.ndarray:
+    def mirrored_range(n: int, s: int) -> NDArray[numpy.int_]:
         v = numpy.arange(n) + s
         v = numpy.where(v >= n, 2 * n - v - 1, v)
         v = numpy.where(v < 0, - 1 - v, v)
@@ -87,7 +96,9 @@ def shift_with_mirror(axis: int, shape: Sequence[int], shift_distance: int = 1)
     return d
 
 
-def deriv_forward(dx_e: Sequence[numpy.ndarray]) -> List[sparse.spmatrix]:
+def deriv_forward(
+        dx_e: Sequence[NDArray[numpy.float_]],
+        ) -> List[sparse.spmatrix]:
     """
     Utility operators for taking discretized derivatives (forward variant).
 
@@ -112,7 +123,9 @@ def deriv_forward(dx_e: Sequence[numpy.ndarray]) -> List[sparse.spmatrix]:
     return Ds
 
 
-def deriv_back(dx_h: Sequence[numpy.ndarray]) -> List[sparse.spmatrix]:
+def deriv_back(
+        dx_h: Sequence[NDArray[numpy.float_]],
+        ) -> List[sparse.spmatrix]:
     """
     Utility operators for taking discretized derivatives (backward variant).
 
@@ -137,7 +150,9 @@ def deriv_back(dx_h: Sequence[numpy.ndarray]) -> List[sparse.spmatrix]:
     return Ds
 
 
-def cross(B: Sequence[sparse.spmatrix]) -> sparse.spmatrix:
+def cross(
+        B: Sequence[sparse.spmatrix],
+        ) -> sparse.spmatrix:
     """
     Cross product operator
 
@@ -203,7 +218,9 @@ def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix:
     return avg_forward(axis, shape).T
 
 
-def curl_forward(dx_e: Sequence[numpy.ndarray]) -> sparse.spmatrix:
+def curl_forward(
+        dx_e: Sequence[NDArray[numpy.float_]],
+        ) -> sparse.spmatrix:
     """
     Curl operator for use with the E field.
 
@@ -217,7 +234,9 @@ def curl_forward(dx_e: Sequence[numpy.ndarray]) -> sparse.spmatrix:
     return cross(deriv_forward(dx_e))
 
 
-def curl_back(dx_h: Sequence[numpy.ndarray]) -> sparse.spmatrix:
+def curl_back(
+        dx_h: Sequence[NDArray[numpy.float_]],
+        ) -> sparse.spmatrix:
     """
     Curl operator for use with the H field.
 
diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py
index 2676bc5..2dd0040 100644
--- a/meanas/fdmath/types.py
+++ b/meanas/fdmath/types.py
@@ -2,31 +2,20 @@
 Types shared across multiple submodules
 """
 from typing import Sequence, Callable, MutableSequence
-import numpy            # type: ignore
+import numpy
+from numpy.typing import NDArray
 
 
 # Field types
-# TODO: figure out a better way to set the docstrings without creating actual subclasses?
-#   Probably not a big issue since they're only used for type hinting
-class fdfield_t(numpy.ndarray):
-    """
-    Vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)
+fdfield_t = NDArray[numpy.float_]
+"""Vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)"""
 
-    This is actually is just an unaltered `numpy.ndarray`
-    """
-    pass
-
-class vfdfield_t(numpy.ndarray):
-    """
-    Linearized vector field (single vector of length 3*X*Y*Z)
-
-    This is actually just an unaltered `numpy.ndarray`
-    """
-    pass
+vfdfield_t = NDArray[numpy.float_]
+"""Linearized vector field (single vector of length 3*X*Y*Z)"""
 
 
-dx_lists_t = Sequence[Sequence[numpy.ndarray]]
-'''
+dx_lists_t = Sequence[Sequence[NDArray[numpy.float_]]]
+"""
  'dxes' datastructure which contains grid cell width information in the following format:
 
      [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...], [dz_e[0], ...]],
@@ -34,15 +23,11 @@ dx_lists_t = Sequence[Sequence[numpy.ndarray]]
 
    where `dx_e[0]` is the x-width of the `x=0` cells, as used when calculating dE/dx,
    and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc.
-'''
+"""
 
-dx_lists_mut = MutableSequence[MutableSequence[numpy.ndarray]]
-'''
- Mutable version of `dx_lists_t`
-'''
+dx_lists_mut = MutableSequence[MutableSequence[NDArray[numpy.float_]]]
+"""Mutable version of `dx_lists_t`"""
 
 
 fdfield_updater_t = Callable[..., fdfield_t]
-'''
- Convenience type for functions which take and return an fdfield_t
-'''
+"""Convenience type for functions which take and return an fdfield_t"""
diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py
index 23e3d9c..6b6c49e 100644
--- a/meanas/fdmath/vectorization.py
+++ b/meanas/fdmath/vectorization.py
@@ -5,7 +5,8 @@ Vectorized versions of the field use row-major (ie., C-style) ordering.
 """
 
 from typing import Optional, overload, Union, List
-import numpy                # type: ignore
+import numpy
+from numpy.typing import ArrayLike
 
 from .types import fdfield_t, vfdfield_t
 
@@ -15,10 +16,10 @@ def vec(f: None) -> None:
     pass
 
 @overload
-def vec(f: Union[fdfield_t, List[numpy.ndarray]]) -> vfdfield_t:
+def vec(f: Union[fdfield_t, List[ArrayLike]]) -> vfdfield_t:
     pass
 
-def vec(f: Optional[Union[fdfield_t, List[numpy.ndarray]]]) -> Optional[vfdfield_t]:
+def vec(f: Optional[Union[fdfield_t, List[ArrayLike]]]) -> Optional[vfdfield_t]:
     """
     Create a 1D ndarray from a 3D vector field which spans a 1-3D region.
 
@@ -37,14 +38,14 @@ def vec(f: Optional[Union[fdfield_t, List[numpy.ndarray]]]) -> Optional[vfdfield
 
 
 @overload
-def unvec(v: None, shape: numpy.ndarray) -> None:
+def unvec(v: None, shape: ArrayLike) -> None:
     pass
 
 @overload
-def unvec(v: vfdfield_t, shape: numpy.ndarray) -> fdfield_t:
+def unvec(v: vfdfield_t, shape: ArrayLike) -> fdfield_t:
     pass
 
-def unvec(v: Optional[vfdfield_t], shape: numpy.ndarray) -> Optional[fdfield_t]:
+def unvec(v: Optional[vfdfield_t], shape: ArrayLike) -> Optional[fdfield_t]:
     """
     Perform the inverse of vec(): take a 1D ndarray and output a 3D field
      of form `[f_x, f_y, f_z]` where each of `f_*` is a len(shape)-dimensional
diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py
index d03a976..4171936 100644
--- a/meanas/fdtd/boundaries.py
+++ b/meanas/fdtd/boundaries.py
@@ -9,9 +9,10 @@ from typing import Tuple, Any, List
 from ..fdmath import fdfield_t, fdfield_updater_t
 
 
-def conducting_boundary(direction: int,
-                        polarity: int
-                        ) -> Tuple[fdfield_updater_t, fdfield_updater_t]:
+def conducting_boundary(
+        direction: int,
+        polarity: int
+        ) -> Tuple[fdfield_updater_t, fdfield_updater_t]:
     dirs = [0, 1, 2]
     if direction not in dirs:
         raise Exception('Invalid direction: {}'.format(direction))
diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py
index 93eedf0..ca7d308 100644
--- a/meanas/fdtd/energy.py
+++ b/meanas/fdtd/energy.py
@@ -1,5 +1,5 @@
 from typing import Optional, Union
-import numpy        # type: ignore
+import numpy
 
 from ..fdmath import dx_lists_t, fdfield_t
 from ..fdmath.functional import deriv_back
@@ -8,10 +8,11 @@ from ..fdmath.functional import deriv_back
 # TODO documentation
 
 
-def poynting(e: fdfield_t,
-             h: fdfield_t,
-             dxes: Optional[dx_lists_t] = None,
-             ) -> fdfield_t:
+def poynting(
+        e: fdfield_t,
+        h: fdfield_t,
+        dxes: Optional[dx_lists_t] = None,
+        ) -> fdfield_t:
     """
     Calculate the poynting vector `S` ($S$).
 
@@ -87,12 +88,13 @@ def poynting(e: fdfield_t,
     return s
 
 
-def poynting_divergence(s: Optional[fdfield_t] = None,
-                        *,
-                        e: Optional[fdfield_t] = None,
-                        h: Optional[fdfield_t] = None,
-                        dxes: Optional[dx_lists_t] = None,
-                        ) -> fdfield_t:
+def poynting_divergence(
+        s: Optional[fdfield_t] = None,
+        *,
+        e: Optional[fdfield_t] = None,
+        h: Optional[fdfield_t] = None,
+        dxes: Optional[dx_lists_t] = None,
+        ) -> fdfield_t:
     """
     Calculate the divergence of the poynting vector.
 
@@ -124,13 +126,14 @@ def poynting_divergence(s: Optional[fdfield_t] = None,
     return ds
 
 
-def energy_hstep(e0: fdfield_t,
-                 h1: fdfield_t,
-                 e2: fdfield_t,
-                 epsilon: Optional[fdfield_t] = None,
-                 mu: Optional[fdfield_t] = None,
-                 dxes: Optional[dx_lists_t] = None,
-                 ) -> fdfield_t:
+def energy_hstep(
+        e0: fdfield_t,
+        h1: fdfield_t,
+        e2: fdfield_t,
+        epsilon: Optional[fdfield_t] = None,
+        mu: Optional[fdfield_t] = None,
+        dxes: Optional[dx_lists_t] = None,
+        ) -> fdfield_t:
     """
     Calculate energy `U` at the time of the provided H-field `h1`.
 
@@ -151,13 +154,14 @@ def energy_hstep(e0: fdfield_t,
     return u
 
 
-def energy_estep(h0: fdfield_t,
-                 e1: fdfield_t,
-                 h2: fdfield_t,
-                 epsilon: Optional[fdfield_t] = None,
-                 mu: Optional[fdfield_t] = None,
-                 dxes: Optional[dx_lists_t] = None,
-                 ) -> fdfield_t:
+def energy_estep(
+        h0: fdfield_t,
+        e1: fdfield_t,
+        h2: fdfield_t,
+        epsilon: Optional[fdfield_t] = None,
+        mu: Optional[fdfield_t] = None,
+        dxes: Optional[dx_lists_t] = None,
+        ) -> fdfield_t:
     """
     Calculate energy `U` at the time of the provided E-field `e1`.
 
@@ -178,15 +182,16 @@ def energy_estep(h0: fdfield_t,
     return u
 
 
-def delta_energy_h2e(dt: float,
-                     e0: fdfield_t,
-                     h1: fdfield_t,
-                     e2: fdfield_t,
-                     h3: fdfield_t,
-                     epsilon: Optional[fdfield_t] = None,
-                     mu: Optional[fdfield_t] = None,
-                     dxes: Optional[dx_lists_t] = None,
-                     ) -> fdfield_t:
+def delta_energy_h2e(
+        dt: float,
+        e0: fdfield_t,
+        h1: fdfield_t,
+        e2: fdfield_t,
+        h3: fdfield_t,
+        epsilon: Optional[fdfield_t] = None,
+        mu: Optional[fdfield_t] = None,
+        dxes: Optional[dx_lists_t] = None,
+        ) -> fdfield_t:
     """
     Change in energy during the half-step from `h1` to `e2`.
 
@@ -210,15 +215,16 @@ def delta_energy_h2e(dt: float,
     return du
 
 
-def delta_energy_e2h(dt: float,
-                     h0: fdfield_t,
-                     e1: fdfield_t,
-                     h2: fdfield_t,
-                     e3: fdfield_t,
-                     epsilon: Optional[fdfield_t] = None,
-                     mu: Optional[fdfield_t] = None,
-                     dxes: Optional[dx_lists_t] = None,
-                     ) -> fdfield_t:
+def delta_energy_e2h(
+        dt: float,
+        h0: fdfield_t,
+        e1: fdfield_t,
+        h2: fdfield_t,
+        e3: fdfield_t,
+        epsilon: Optional[fdfield_t] = None,
+        mu: Optional[fdfield_t] = None,
+        dxes: Optional[dx_lists_t] = None,
+        ) -> fdfield_t:
     """
     Change in energy during the half-step from `e1` to `h2`.
 
@@ -242,10 +248,11 @@ def delta_energy_e2h(dt: float,
     return du
 
 
-def delta_energy_j(j0: fdfield_t,
-                   e1: fdfield_t,
-                   dxes: Optional[dx_lists_t] = None,
-                   ) -> fdfield_t:
+def delta_energy_j(
+        j0: fdfield_t,
+        e1: fdfield_t,
+        dxes: Optional[dx_lists_t] = None,
+        ) -> fdfield_t:
     """
     Calculate
 
@@ -264,12 +271,13 @@ def delta_energy_j(j0: fdfield_t,
     return du
 
 
-def dxmul(ee: fdfield_t,
-          hh: fdfield_t,
-          epsilon: Optional[Union[fdfield_t, float]] = None,
-          mu: Optional[Union[fdfield_t, float]] = None,
-          dxes: Optional[dx_lists_t] = None
-          ) -> fdfield_t:
+def dxmul(
+        ee: fdfield_t,
+        hh: fdfield_t,
+        epsilon: Optional[Union[fdfield_t, float]] = None,
+        mu: Optional[Union[fdfield_t, float]] = None,
+        dxes: Optional[dx_lists_t] = None,
+        ) -> fdfield_t:
     if epsilon is None:
         epsilon = 1
     if mu is None:
diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py
index 066cca8..6f7aff7 100644
--- a/meanas/fdtd/pml.py
+++ b/meanas/fdtd/pml.py
@@ -8,7 +8,8 @@ PML implementations
 # TODO retest pmls!
 
 from typing import List, Callable, Tuple, Dict, Sequence, Any, Optional
-import numpy            # type: ignore
+import numpy
+from typing import NDArray
 
 from ..fdmath import fdfield_t, dx_lists_t
 from ..fdmath.functional import deriv_forward, deriv_back
@@ -61,7 +62,7 @@ def cpml_params(
     expand_slice_l[axis] = slice(None)
     expand_slice = tuple(expand_slice_l)
 
-    def par(x: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]:
+    def par(x: NDArray[numpy.float64]) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
         scaling = (x / thickness) ** m
         sigma = scaling * sigma_max
         kappa = 1 + scaling * (kappa_max - 1)
diff --git a/meanas/test/conftest.py b/meanas/test/conftest.py
index 311aae3..ba6a3a8 100644
--- a/meanas/test/conftest.py
+++ b/meanas/test/conftest.py
@@ -4,7 +4,8 @@ Test fixtures
 
 """
 from typing import Tuple, Iterable, List, Any
-import numpy        # type: ignore
+import numpy
+from numpy.typing import NDArray, ArrayLike
 import pytest       # type: ignore
 
 from .utils import PRNG
@@ -34,11 +35,12 @@ def epsilon_fg(request: FixtureRequest) -> Iterable[float]:
 
 
 @pytest.fixture(scope='module', params=['center', '000', 'random'])
-def epsilon(request: FixtureRequest,
-            shape: Tuple[int, ...],
-            epsilon_bg: float,
-            epsilon_fg: float,
-            ) -> Iterable[numpy.ndarray]:
+def epsilon(
+        request: FixtureRequest,
+        shape: Tuple[int, ...],
+        epsilon_bg: float,
+        epsilon_fg: float,
+        ) -> Iterable[NDArray[numpy.float64]]:
     is3d = (numpy.array(shape) == 1).sum() == 0
     if is3d:
         if request.param == '000':
@@ -72,10 +74,11 @@ def dx(request: FixtureRequest) -> Iterable[float]:
 
 
 @pytest.fixture(scope='module', params=['uniform', 'centerbig'])
-def dxes(request: FixtureRequest,
-         shape: Tuple[int, ...],
-         dx: float,
-         ) -> Iterable[List[List[numpy.ndarray]]]:
+def dxes(
+        request: FixtureRequest,
+        shape: Tuple[int, ...],
+        dx: float,
+        ) -> Iterable[List[List[NDArray[numpy.float64]]]]:
     if request.param == 'uniform':
         dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
     elif request.param == 'centerbig':
diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py
index 076cb52..fef80fd 100644
--- a/meanas/test/test_fdfd.py
+++ b/meanas/test/test_fdfd.py
@@ -1,7 +1,8 @@
 from typing import List, Tuple, Iterable, Optional
 import dataclasses
 import pytest       # type: ignore
-import numpy        # type: ignore
+import numpy
+from numpy.typing import NDArray, ArrayLike
 #from numpy.testing import assert_allclose, assert_array_equal
 
 from .. import fdfd
@@ -59,12 +60,12 @@ def omega(request: FixtureRequest) -> Iterable[float]:
 
 
 @pytest.fixture(params=[None])
-def pec(request: FixtureRequest) -> Iterable[Optional[numpy.ndarray]]:
+def pec(request: FixtureRequest) -> Iterable[Optional[NDArray[numpy.float64]]]:
     yield request.param
 
 
 @pytest.fixture(params=[None])
-def pmc(request: FixtureRequest) -> Iterable[Optional[numpy.ndarray]]:
+def pmc(request: FixtureRequest) -> Iterable[Optional[NDArray[numpy.float64]]]:
     yield request.param
 
 
@@ -75,10 +76,11 @@ def pmc(request: FixtureRequest) -> Iterable[Optional[numpy.ndarray]]:
 
 
 @pytest.fixture(params=['diag'])        # 'center'
-def j_distribution(request: FixtureRequest,
-                   shape: Tuple[int, ...],
-                   j_mag: float,
-                   ) -> Iterable[numpy.ndarray]:
+def j_distribution(
+        request: FixtureRequest,
+        shape: Tuple[int, ...],
+        j_mag: float,
+        ) -> Iterable[NDArray[numpy.float64]]:
     j = numpy.zeros(shape, dtype=complex)
     center_mask = numpy.zeros(shape, dtype=bool)
     center_mask[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = True
@@ -94,24 +96,25 @@ def j_distribution(request: FixtureRequest,
 @dataclasses.dataclass()
 class FDResult:
     shape: Tuple[int, ...]
-    dxes: List[List[numpy.ndarray]]
-    epsilon: numpy.ndarray
+    dxes: List[List[NDArray[numpy.float64]]]
+    epsilon: NDArray[numpy.float64]
     omega: complex
-    j: numpy.ndarray
-    e: numpy.ndarray
-    pmc: numpy.ndarray
-    pec: numpy.ndarray
+    j: NDArray[numpy.float64]
+    e: NDArray[numpy.float64]
+    pmc: Optional[NDArray[numpy.float64]]
+    pec: Optional[NDArray[numpy.float64]]
 
 
 @pytest.fixture()
-def sim(request: FixtureRequest,
+def sim(
+        request: FixtureRequest,
         shape: Tuple[int, ...],
-        epsilon: numpy.ndarray,
-        dxes: List[List[numpy.ndarray]],
-        j_distribution: numpy.ndarray,
+        epsilon: NDArray[numpy.float64],
+        dxes: List[List[NDArray[numpy.float64]]],
+        j_distribution: NDArray[numpy.float64],
         omega: float,
-        pec: Optional[numpy.ndarray],
-        pmc: Optional[numpy.ndarray],
+        pec: Optional[NDArray[numpy.float64]],
+        pmc: Optional[NDArray[numpy.float64]],
         ) -> FDResult:
     """
     Build simulation from parts
diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py
index cf9c05a..30eb32d 100644
--- a/meanas/test/test_fdfd_pml.py
+++ b/meanas/test/test_fdfd_pml.py
@@ -1,7 +1,8 @@
 from typing import Optional, Tuple, Iterable, List
 import pytest       # type: ignore
-import numpy        # type: ignore
-from numpy.testing import assert_allclose   # type: ignore
+import numpy
+from numpy.typing import NDArray, ArrayLike
+from numpy.testing import assert_allclose
 
 from .. import fdfd
 from ..fdmath import vec, unvec, dx_lists_mut
@@ -48,12 +49,12 @@ def omega(request: FixtureRequest) -> Iterable[float]:
 
 
 @pytest.fixture(params=[None])
-def pec(request: FixtureRequest) -> Iterable[Optional[numpy.ndarray]]:
+def pec(request: FixtureRequest) -> Iterable[Optional[NDArray[numpy.float64]]]:
     yield request.param
 
 
 @pytest.fixture(params=[None])
-def pmc(request: FixtureRequest) -> Iterable[Optional[numpy.ndarray]]:
+def pmc(request: FixtureRequest) -> Iterable[Optional[NDArray[numpy.float64]]]:
     yield request.param
 
 
@@ -70,13 +71,14 @@ def src_polarity(request: FixtureRequest) -> Iterable[int]:
 
 
 @pytest.fixture()
-def j_distribution(request: FixtureRequest,
-                   shape: Tuple[int, ...],
-                   epsilon: numpy.ndarray,
-                   dxes: dx_lists_mut,
-                   omega: float,
-                   src_polarity: int,
-                   ) -> Iterable[numpy.ndarray]:
+def j_distribution(
+        request: FixtureRequest,
+        shape: Tuple[int, ...],
+        epsilon: NDArray[numpy.float64],
+        dxes: dx_lists_mut,
+        omega: float,
+        src_polarity: int,
+        ) -> Iterable[NDArray[numpy.float64]]:
     j = numpy.zeros(shape, dtype=complex)
 
     dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0]    # Propagation axis
@@ -108,47 +110,60 @@ def j_distribution(request: FixtureRequest,
 
 
 @pytest.fixture()
-def epsilon(request: FixtureRequest,
-            shape: Tuple[int, ...],
-            epsilon_bg: float,
-            epsilon_fg: float,
-            ) -> Iterable[numpy.ndarray]:
+def epsilon(
+        request: FixtureRequest,
+        shape: Tuple[int, ...],
+        epsilon_bg: float,
+        epsilon_fg: float,
+        ) -> Iterable[NDArray[numpy.float64]]:
     epsilon = numpy.full(shape, epsilon_fg, dtype=float)
     yield epsilon
 
 
 @pytest.fixture(params=['uniform'])
-def dxes(request: FixtureRequest,
-         shape: Tuple[int, ...],
-         dx: float,
-         omega: float,
-         epsilon_fg: float,
-         ) -> Iterable[List[List[numpy.ndarray]]]:
+def dxes(
+        request: FixtureRequest,
+        shape: Tuple[int, ...],
+        dx: float,
+        omega: float,
+        epsilon_fg: float,
+        ) -> Iterable[List[List[NDArray[numpy.float64]]]]:
     if request.param == 'uniform':
         dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
     dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0]    # Propagation axis
     for axis in (dim,):
         for polarity in (-1, 1):
-            dxes = fdfd.scpml.stretch_with_scpml(dxes, axis=axis, polarity=polarity,
-                                                 omega=omega, epsilon_effective=epsilon_fg,
-                                                 thickness=10)
+            dxes = fdfd.scpml.stretch_with_scpml(
+                dxes,
+                axis=axis,
+                polarity=polarity,
+                omega=omega,
+                epsilon_effective=epsilon_fg,
+                thickness=10,
+                )
     yield dxes
 
 
 @pytest.fixture()
-def sim(request: FixtureRequest,
+def sim(
+        request: FixtureRequest,
         shape: Tuple[int, ...],
-        epsilon: numpy.ndarray,
+        epsilon: NDArray[numpy.float64],
         dxes: dx_lists_mut,
-        j_distribution: numpy.ndarray,
+        j_distribution: NDArray[numpy.float64],
         omega: float,
-        pec: Optional[numpy.ndarray],
-        pmc: Optional[numpy.ndarray],
+        pec: Optional[NDArray[numpy.float64]],
+        pmc: Optional[NDArray[numpy.float64]],
         ) -> FDResult:
     j_vec = vec(j_distribution)
     eps_vec = vec(epsilon)
-    e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec,
-                                 matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11})
+    e_vec = fdfd.solvers.generic(
+        J=j_vec,
+        omega=omega,
+        dxes=dxes,
+        epsilon=eps_vec,
+        matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11},
+        )
     e = unvec(e_vec, shape[1:])
 
     sim = FDResult(
diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py
index 8f5e013..b46a3ca 100644
--- a/meanas/test/test_fdtd.py
+++ b/meanas/test/test_fdtd.py
@@ -1,8 +1,9 @@
-from typing import List, Tuple, Iterable
+from typing import List, Tuple, Iterable, Any, Dict
 import dataclasses
 import pytest       # type: ignore
-import numpy        # type: ignore
-#from numpy.testing import assert_allclose, assert_array_equal       # type: ignore
+import numpy
+from numpy.typing import NDArray, ArrayLike
+#from numpy.testing import assert_allclose, assert_array_equal
 
 from .. import fdtd
 from .utils import assert_close, assert_fields_close, PRNG
@@ -32,8 +33,10 @@ def test_initial_energy(sim: 'TDResult') -> None:
 
     dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0)
     u0 = (j0 * j0.conj() / sim.epsilon * dV).sum(axis=0)
-    args = {'dxes': sim.dxes,
-            'epsilon': sim.epsilon}
+    args: Dict[str, Any] = {
+        'dxes': sim.dxes,
+        'epsilon': sim.epsilon,
+        }
 
     # Make sure initial energy and E dot J are correct
     energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=h1, **args)
@@ -49,8 +52,10 @@ def test_energy_conservation(sim: 'TDResult') -> None:
     e0 = sim.es[0]
     j0 = sim.js[0]
     u = fdtd.delta_energy_j(j0=j0, e1=e0, dxes=sim.dxes).sum()
-    args = {'dxes': sim.dxes,
-            'epsilon': sim.epsilon}
+    args: Dict[str, Any] = {
+        'dxes': sim.dxes,
+        'epsilon': sim.epsilon,
+        }
 
     for ii in range(1, 8):
         u_hstep = fdtd.energy_hstep(e0=sim.es[ii - 1], h1=sim.hs[ii], e2=sim.es[ii],     **args)
@@ -65,8 +70,10 @@ def test_energy_conservation(sim: 'TDResult') -> None:
 
 
 def test_poynting_divergence(sim: 'TDResult') -> None:
-    args = {'dxes': sim.dxes,
-            'epsilon': sim.epsilon}
+    args: Dict[str, Any] = {
+        'dxes': sim.dxes,
+        'epsilon': sim.epsilon,
+        }
 
     u_eprev = None
     for ii in range(1, 8):
@@ -96,8 +103,10 @@ def test_poynting_planes(sim: 'TDResult') -> None:
     if mask.sum() > 1:
         pytest.skip('test_poynting_planes can only test single point sources, got {}'.format(mask.sum()))
 
-    args = {'dxes': sim.dxes,
-            'epsilon': sim.epsilon}
+    args: Dict[str, Any] = {
+        'dxes': sim.dxes,
+        'epsilon': sim.epsilon,
+        }
 
     mx = numpy.roll(mask, -1, axis=0)
     my = numpy.roll(mask, -1, axis=1)
@@ -149,13 +158,13 @@ def dt(request: FixtureRequest) -> Iterable[float]:
 class TDResult:
     shape: Tuple[int, ...]
     dt: float
-    dxes: List[List[numpy.ndarray]]
-    epsilon: numpy.ndarray
-    j_distribution: numpy.ndarray
+    dxes: List[List[NDArray[numpy.float64]]]
+    epsilon: NDArray[numpy.float64]
+    j_distribution: NDArray[numpy.float64]
     j_steps: Tuple[int, ...]
-    es: List[numpy.ndarray] = dataclasses.field(default_factory=list)
-    hs: List[numpy.ndarray] = dataclasses.field(default_factory=list)
-    js: List[numpy.ndarray] = dataclasses.field(default_factory=list)
+    es: List[NDArray[numpy.float64]] = dataclasses.field(default_factory=list)
+    hs: List[NDArray[numpy.float64]] = dataclasses.field(default_factory=list)
+    js: List[NDArray[numpy.float64]] = dataclasses.field(default_factory=list)
 
 
 @pytest.fixture(params=[(0, 4, 8)])  # (0,)
@@ -164,10 +173,11 @@ def j_steps(request: FixtureRequest) -> Iterable[Tuple[int, ...]]:
 
 
 @pytest.fixture(params=['center', 'random'])
-def j_distribution(request: FixtureRequest,
-                   shape: Tuple[int, ...],
-                   j_mag: float,
-                   ) -> Iterable[numpy.ndarray]:
+def j_distribution(
+        request: FixtureRequest,
+        shape: Tuple[int, ...],
+        j_mag: float,
+        ) -> Iterable[NDArray[numpy.float64]]:
     j = numpy.zeros(shape)
     if request.param == 'center':
         j[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = j_mag
@@ -179,12 +189,13 @@ def j_distribution(request: FixtureRequest,
 
 
 @pytest.fixture()
-def sim(request: FixtureRequest,
+def sim(
+        request: FixtureRequest,
         shape: Tuple[int, ...],
-        epsilon: numpy.ndarray,
-        dxes: List[List[numpy.ndarray]],
+        epsilon: NDArray[numpy.float64],
+        dxes: List[List[NDArray[numpy.float64]]],
         dt: float,
-        j_distribution: numpy.ndarray,
+        j_distribution: NDArray[numpy.float64],
         j_steps: Tuple[int, ...],
         ) -> TDResult:
     is3d = (numpy.array(shape) == 1).sum() == 0
diff --git a/meanas/test/utils.py b/meanas/test/utils.py
index f76b910..b4cb3ab 100644
--- a/meanas/test/utils.py
+++ b/meanas/test/utils.py
@@ -1,24 +1,27 @@
 from typing import Any
-import numpy        # type: ignore
+import numpy
+from typing import ArrayLike
 
 
 PRNG = numpy.random.RandomState(12345)
 
 
-def assert_fields_close(x: numpy.ndarray,
-                        y: numpy.ndarray,
-                        *args: Any,
-                        **kwargs: Any,
-                        ) -> None:
+def assert_fields_close(
+        x: ArrayLike,
+        y: ArrayLike,
+        *args: Any,
+        **kwargs: Any,
+        ) -> None:
     numpy.testing.assert_allclose(
         x, y, verbose=False,
         err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(x, -1),
                                                        numpy.rollaxis(y, -1)), *args, **kwargs)
 
-def assert_close(x: numpy.ndarray,
-                 y: numpy.ndarray,
-                 *args: Any,
-                 **kwargs: Any,
-                 ) -> None:
+def assert_close(
+        x: ArrayLike,
+        y: ArrayLike,
+        *args: Any,
+        **kwargs: Any,
+        ) -> None:
     numpy.testing.assert_allclose(x, y, *args, **kwargs)
 

From 71d6ceec1e56bc5502780e6dcd7a75b90452e11d Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 14:32:54 -0700
Subject: [PATCH 266/437] update to new fdtd approach

---
 examples/fdtd.py | 25 ++++++++++---------------
 1 file changed, 10 insertions(+), 15 deletions(-)

diff --git a/examples/fdtd.py b/examples/fdtd.py
index 026e16a..fac5f81 100644
--- a/examples/fdtd.py
+++ b/examples/fdtd.py
@@ -11,6 +11,7 @@ import numpy
 import h5py
 
 from meanas import fdtd
+from meanas.fdtd import cpml_params, updates_with_cpml
 from masque import Pattern, shapes
 import gridlock
 import pcgen
@@ -127,19 +128,15 @@ def main():
     e = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)]
     h = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)]
 
-    update_e = fdtd.maxwell_e(dt)
-    update_h = fdtd.maxwell_h(dt)
+    dxes = [grid.dxyz, grid.autoshifted_dxyz()]
 
     # PMLs in every direction
-    pml_e_funcs = []
-    pml_h_funcs = []
-    pml_fields = {}
-    for d in (0, 1, 2):
-        for p in (-1, 1):
-            ef, hf, psis = fdtd.cpml(direction=d, polarity=p, dt=dt, epsilon=epsilon, epsilon_eff=n_slab**2, dtype=dtype)
-            pml_e_funcs.append(ef)
-            pml_h_funcs.append(hf)
-            pml_fields.update(psis)
+    pml_params = [[cpml_params(axis=dd, polarity=pp, dt=dt,
+                               thickness=pml_thickness, epsilon_eff=1.0**2)
+                   for pp in (-1, +1)]
+                  for dd in range(3)]
+    update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt,
+                                           dxes=dxes, epsilon=epsilon)
 
     # Source parameters and function
     w = 2 * numpy.pi * dx / wl
@@ -155,12 +152,10 @@ def main():
     output_file = h5py.File('simulation_output.h5', 'w')
     start = time.perf_counter()
     for t in range(max_t):
-        [f(e, h, epsilon) for f in pml_e_funcs]
-        update_e(e, h, epsilon)
+        update_E(e, h, epsilon)
 
         e[1][tuple(grid.shape//2)] += field_source(t)
-        [f(e, h) for f in pml_h_funcs]
-        update_h(e, h)
+        update_H(e, h)
 
         print('iteration {}: average {} iterations per sec'.format(t, (t+1)/(time.perf_counter()-start)))
         sys.stdout.flush()

From ec9a6edc85c8c2f48ea8aeea74bf045a62fb3b54 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 14:33:36 -0700
Subject: [PATCH 267/437] be explicit about stack axis

---
 meanas/fdfd/bloch.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 872781c..1c07564 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -198,9 +198,9 @@ def maxwell_operator(
     shape = epsilon[0].shape + (1,)
     k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
-    epsilon = numpy.stack(epsilon, 3)
+    epsilon = numpy.stack(epsilon, axis=3)
     if mu is not None:
-        mu = numpy.stack(mu, 3)
+        mu = numpy.stack(mu, axis=3)
 
     def operator(h: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
         """
@@ -272,7 +272,7 @@ def hmn_2_exyz(
         Function for converting `h_mn` into `E_xyz`
     """
     shape = epsilon[0].shape + (1,)
-    epsilon = numpy.stack(epsilon, 3)
+    epsilon = numpy.stack(epsilon, axis=3)
 
     k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
@@ -350,12 +350,12 @@ def inverse_maxwell_operator_approx(
         Function which applies the approximate inverse of the maxwell operator to `h_mn`.
     """
     shape = epsilon[0].shape + (1,)
-    epsilon = numpy.stack(epsilon, 3)
+    epsilon = numpy.stack(epsilon, axis=3)
 
     k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
     if mu is not None:
-        mu = numpy.stack(mu, 3)
+        mu = numpy.stack(mu, axis=3)
 
     def operator(h: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
         """

From d4fcfa1e07f6cd0da4732575905818a9cc1b1e0c Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 14:33:47 -0700
Subject: [PATCH 268/437] fix var name

---
 meanas/fdmath/functional.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py
index aad8a33..2cc5172 100644
--- a/meanas/fdmath/functional.py
+++ b/meanas/fdmath/functional.py
@@ -133,7 +133,7 @@ def curl_forward_parts(
 def curl_back_parts(
         dx_h: Optional[Sequence[NDArray[numpy.float_]]] = None,
         ) -> Callable:
-    Dx, Dy, Dz = deriv_back(dx_e)
+    Dx, Dy, Dz = deriv_back(dx_h)
 
     def mkparts_back(h: fdfield_t) -> Tuple[Tuple[fdfield_t, ...]]:
         return ((-Dz(h[1]),  Dy(h[2])),

From ad1ec6acfb4ad3e6d0fdbe90e5ff17f536afda2d Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 14:35:30 -0700
Subject: [PATCH 269/437] Back off on FFTW args

multithreading in particular seems pretty detrimental
---
 meanas/fdfd/bloch.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 1c07564..a300ab7 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -105,9 +105,9 @@ try:
     pyfftw.interfaces.cache.enable()
     pyfftw.interfaces.cache.set_keepalive_time(3600)
     fftw_args = {
-        'threads': multiprocessing.cpu_count(),
+        #'threads': multiprocessing.cpu_count(),
         'overwrite_input': True,
-        'planner_effort': 'FFTW_EXHAUSTIVE',
+        'planner_effort': 'FFTW_PATIENT',
         }
 
     def fftn(*args: Any, **kwargs: Any) -> NDArray[numpy.float64]:

From 31e6e0ec60dda73da9f4f4a2749f82ee9e7e9161 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 17:09:14 -0700
Subject: [PATCH 270/437] just check if file exists rather than trying to open

---
 examples/bloch.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/examples/bloch.py b/examples/bloch.py
index 75fc833..2624876 100644
--- a/examples/bloch.py
+++ b/examples/bloch.py
@@ -32,12 +32,11 @@ def pyfftw_load_wisdom(path):
     except ImportError as e:
         pass
 
-    try:
+    if path.exists():
         with open(path, 'rb') as f:
             wisdom = pickle.load(f)
         pyfftw.import_wisdom(wisdom)
-    except FileNotFoundError as e:
-        pass
+
 
 logger.info('Drawing grid...')
 dx = 40

From 52df24ad9840149718d07d08a669c2fb6de2e6cc Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 17:17:44 -0700
Subject: [PATCH 271/437] Major typing updates

Split field types to differentiate between complex and purely-real

Fix lots of numpy-related stuff
---
 meanas/eigensolvers.py         | 16 ++++-----
 meanas/fdfd/bloch.py           | 50 ++++++++++++++--------------
 meanas/fdfd/farfield.py        | 10 +++---
 meanas/fdfd/functional.py      | 60 +++++++++++++++++-----------------
 meanas/fdfd/operators.py       | 12 +++----
 meanas/fdfd/scpml.py           | 15 ++++-----
 meanas/fdfd/solvers.py         |  6 ++--
 meanas/fdfd/waveguide_2d.py    | 26 +++++++--------
 meanas/fdfd/waveguide_3d.py    | 14 ++++----
 meanas/fdfd/waveguide_cyl.py   |  8 ++---
 meanas/fdmath/__init__.py      |  3 +-
 meanas/fdmath/functional.py    |  8 ++---
 meanas/fdmath/types.py         |  9 +++++
 meanas/fdmath/vectorization.py | 28 +++++++++++-----
 meanas/fdtd/pml.py             | 47 ++++++++++++++++----------
 meanas/test/test_fdfd.py       | 15 ++++++---
 meanas/test/test_fdfd_pml.py   |  4 +--
 meanas/test/utils.py           | 11 ++++---
 18 files changed, 191 insertions(+), 151 deletions(-)

diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py
index aa8b9ba..4b96f60 100644
--- a/meanas/eigensolvers.py
+++ b/meanas/eigensolvers.py
@@ -11,9 +11,9 @@ import scipy.sparse.linalg as spalg   # type: ignore
 
 def power_iteration(
         operator: sparse.spmatrix,
-        guess_vector: Optional[NDArray[numpy.float64]] = None,
+        guess_vector: Optional[NDArray[numpy.complex128]] = None,
         iterations: int = 20,
-        ) -> Tuple[complex, NDArray[numpy.float64]]:
+        ) -> Tuple[complex, NDArray[numpy.complex128]]:
     """
     Use power iteration to estimate the dominant eigenvector of a matrix.
 
@@ -26,7 +26,7 @@ def power_iteration(
         (Largest-magnitude eigenvalue, Corresponding eigenvector estimate)
     """
     if numpy.any(numpy.equal(guess_vector, None)):
-        v = numpy.random.rand(operator.shape[0])
+        v = numpy.random.rand(operator.shape[0]) + 1j * numpy.random.rand(operator.shape[0])
     else:
         v = guess_vector
 
@@ -41,11 +41,11 @@ def power_iteration(
 
 def rayleigh_quotient_iteration(
         operator: Union[sparse.spmatrix, spalg.LinearOperator],
-        guess_vector: NDArray[numpy.float64],
+        guess_vector: NDArray[numpy.complex128],
         iterations: int = 40,
         tolerance: float = 1e-13,
-        solver: Optional[Callable[..., NDArray[numpy.float64]]] = None,
-        ) -> Tuple[complex, NDArray[numpy.float64]]:
+        solver: Optional[Callable[..., NDArray[numpy.complex128]]] = None,
+        ) -> Tuple[complex, NDArray[numpy.complex128]]:
     """
     Use Rayleigh quotient iteration to refine an eigenvector guess.
 
@@ -78,7 +78,7 @@ def rayleigh_quotient_iteration(
                     matvec=lambda v: eigval * v,
                     )
         if solver is None:
-            def solver(A: spalg.LinearOperator, b: ArrayLike) -> NDArray[numpy.float64]:
+            def solver(A: spalg.LinearOperator, b: ArrayLike) -> NDArray[numpy.complex128]:
                 return spalg.bicgstab(A, b)[0]
     assert(solver is not None)
 
@@ -99,7 +99,7 @@ def signed_eigensolve(
         operator: Union[sparse.spmatrix, spalg.LinearOperator],
         how_many: int,
         negative: bool = False,
-        ) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
+        ) -> Tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Find the largest-magnitude positive-only (or negative-only) eigenvalues and
      eigenvectors of the provided matrix.
diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index a300ab7..0e3ca51 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -80,7 +80,7 @@ This module contains functions for generating and solving the
 
 '''
 
-from typing import Tuple, Callable, Any, List, Optional, cast, Union
+from typing import Tuple, Callable, Any, List, Optional, cast, Union, Sequence
 import logging
 import numpy
 from numpy import pi, real, trace
@@ -91,7 +91,8 @@ import scipy.optimize                   # type: ignore
 from scipy.linalg import norm           # type: ignore
 import scipy.sparse.linalg as spalg     # type: ignore
 
-from ..fdmath import fdfield_t
+from ..fdmath import fdfield_t, cfdfield_t
+
 
 logger = logging.getLogger(__name__)
 
@@ -110,10 +111,10 @@ try:
         'planner_effort': 'FFTW_PATIENT',
         }
 
-    def fftn(*args: Any, **kwargs: Any) -> NDArray[numpy.float64]:
+    def fftn(*args: Any, **kwargs: Any) -> NDArray[numpy.complex128]:
         return pyfftw.interfaces.numpy_fft.fftn(*args, **kwargs, **fftw_args)
 
-    def ifftn(*args: Any, **kwargs: Any) -> NDArray[numpy.float64]:
+    def ifftn(*args: Any, **kwargs: Any) -> NDArray[numpy.complex128]:
         return pyfftw.interfaces.numpy_fft.ifftn(*args, **kwargs, **fftw_args)
 
 except ImportError:
@@ -124,7 +125,7 @@ except ImportError:
 def generate_kmn(
         k0: ArrayLike,
         G_matrix: ArrayLike,
-        shape: ArrayLike,
+        shape: Sequence[int],
         ) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
     """
     Generate a (k, m, n) orthogonal basis for each k-vector in the simulation grid.
@@ -142,7 +143,7 @@ def generate_kmn(
     k0 = numpy.array(k0)
 
     Gi_grids = numpy.meshgrid(*(fftfreq(n, 1 / n) for n in shape[:3]), indexing='ij')
-    Gi = numpy.stack(Gi_grids, axis=3)
+    Gi = numpy.moveaxis(Gi_grids, 0, -1)
 
     k_G = k0[None, None, None, :] - Gi
     k_xyz = numpy.rollaxis(G_matrix @ numpy.rollaxis(k_G, 3, 2), 3, 2)
@@ -169,7 +170,7 @@ def maxwell_operator(
         G_matrix: ArrayLike,
         epsilon: fdfield_t,
         mu: Optional[fdfield_t] = None
-        ) -> Callable[[NDArray[numpy.float64]], NDArray[numpy.float64]]:
+        ) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
     """
     Generate the Maxwell operator
 
@@ -198,11 +199,11 @@ def maxwell_operator(
     shape = epsilon[0].shape + (1,)
     k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
-    epsilon = numpy.stack(epsilon, axis=3)
+    epsilon = numpy.moveaxis(epsilon, 0, -1)
     if mu is not None:
-        mu = numpy.stack(mu, axis=3)
+        mu = numpy.moveaxis(mu, 0, -1)
 
-    def operator(h: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
+    def operator(h: NDArray[numpy.complex128]) -> NDArray[numpy.complex128]:
         """
         Maxwell operator for Bloch eigenmode simulation.
 
@@ -251,7 +252,7 @@ def hmn_2_exyz(
         k0: ArrayLike,
         G_matrix: ArrayLike,
         epsilon: fdfield_t,
-        ) -> Callable[[NDArray[numpy.float64]], fdfield_t]:
+        ) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]:
     """
     Generate an operator which converts a vectorized spatial-frequency-space
      `h_mn` into an E-field distribution, i.e.
@@ -272,11 +273,11 @@ def hmn_2_exyz(
         Function for converting `h_mn` into `E_xyz`
     """
     shape = epsilon[0].shape + (1,)
-    epsilon = numpy.stack(epsilon, axis=3)
+    epsilon = numpy.moveaxis(epsilon, 0, -1)
 
     k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
-    def operator(h: NDArray[numpy.float64]) -> fdfield_t:
+    def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
         hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
         d_xyz = (n * hin_m
                - m * hin_n) * k_mag
@@ -291,7 +292,7 @@ def hmn_2_hxyz(
         k0: ArrayLike,
         G_matrix: ArrayLike,
         epsilon: fdfield_t
-        ) -> Callable[[NDArray[numpy.float64]], fdfield_t]:
+        ) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]:
     """
     Generate an operator which converts a vectorized spatial-frequency-space
      `h_mn` into an H-field distribution, i.e.
@@ -314,7 +315,7 @@ def hmn_2_hxyz(
     shape = epsilon[0].shape + (1,)
     _k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
-    def operator(h: NDArray[numpy.float64]) -> fdfield_t:
+    def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
         hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
         h_xyz = (m * hin_m
                + n * hin_n)
@@ -328,7 +329,7 @@ def inverse_maxwell_operator_approx(
         G_matrix: ArrayLike,
         epsilon: fdfield_t,
         mu: Optional[fdfield_t] = None,
-        ) -> Callable[[NDArray[numpy.float64]], NDArray[numpy.float64]]:
+        ) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
     """
     Generate an approximate inverse of the Maxwell operator,
 
@@ -350,14 +351,13 @@ def inverse_maxwell_operator_approx(
         Function which applies the approximate inverse of the maxwell operator to `h_mn`.
     """
     shape = epsilon[0].shape + (1,)
-    epsilon = numpy.stack(epsilon, axis=3)
+    epsilon = numpy.moveaxis(epsilon, 0, -1)
+    if mu is not None:
+        mu = numpy.moveaxis(mu, 0, -1)
 
     k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
-    if mu is not None:
-        mu = numpy.stack(mu, axis=3)
-
-    def operator(h: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
+    def operator(h: NDArray[numpy.complex128]) -> NDArray[numpy.complex128]:
         """
         Approximate inverse Maxwell operator for Bloch eigenmode simulation.
 
@@ -462,7 +462,7 @@ def eigsolve(
         tolerance: float = 1e-20,
         max_iters: int = 10000,
         reset_iters: int = 100,
-        ) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
+        ) -> Tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector
      k0 of the specified structure.
@@ -697,11 +697,11 @@ def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_to
 '''
 
 def _rtrace_AtB(
-        A: NDArray[numpy.float64],
-        B: Union[NDArray[numpy.float64], float],
+        A: NDArray[numpy.complex128],
+        B: Union[NDArray[numpy.complex128], float],
         ) -> float:
     return real(numpy.sum(A.conj() * B))
 
-def _symmetrize(A: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
+def _symmetrize(A: NDArray[numpy.complex128]) -> NDArray[numpy.complex128]:
     return (A + A.conj().T) * 0.5
 
diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py
index e1a725d..bd02eb9 100644
--- a/meanas/fdfd/farfield.py
+++ b/meanas/fdfd/farfield.py
@@ -6,12 +6,12 @@ import numpy
 from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift
 from numpy import pi
 
-from ..fdmath import fdfield_t
+from ..fdmath import cfdfield_t
 
 
 def near_to_farfield(
-        E_near: fdfield_t,
-        H_near: fdfield_t,
+        E_near: cfdfield_t,
+        H_near: cfdfield_t,
         dx: float,
         dy: float,
         padded_size: List[int] = None
@@ -122,8 +122,8 @@ def near_to_farfield(
 
 
 def far_to_nearfield(
-        E_far: fdfield_t,
-        H_far: fdfield_t,
+        E_far: cfdfield_t,
+        H_far: cfdfield_t,
         dkx: float,
         dky: float,
         padded_size: List[int] = None
diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py
index 9a83910..1a1506b 100644
--- a/meanas/fdfd/functional.py
+++ b/meanas/fdfd/functional.py
@@ -2,13 +2,13 @@
 Functional versions of many FDFD operators. These can be useful for performing
  FDFD calculations without needing to construct large matrices in memory.
 
-The functions generated here expect `fdfield_t` inputs with shape (3, X, Y, Z),
-e.g. E = [E_x, E_y, E_z] where each component has shape (X, Y, Z)
+The functions generated here expect `cfdfield_t` inputs with shape (3, X, Y, Z),
+e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z)
 """
 from typing import Callable, Tuple, Optional
 import numpy
 
-from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t
+from ..fdmath import dx_lists_t, fdfield_t, cfdfield_t, cfdfield_updater_t
 from ..fdmath.functional import curl_forward, curl_back
 
 
@@ -19,8 +19,8 @@ def e_full(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: fdfield_t,
-        mu: fdfield_t = None
-        ) -> fdfield_updater_t:
+        mu: Optional[fdfield_t] = None
+        ) -> cfdfield_updater_t:
     """
     Wave operator for use with E-field. See `operators.e_full` for details.
 
@@ -37,13 +37,13 @@ def e_full(
     ch = curl_back(dxes[1])
     ce = curl_forward(dxes[0])
 
-    def op_1(e: fdfield_t) -> fdfield_t:
+    def op_1(e: cfdfield_t) -> cfdfield_t:
         curls = ch(ce(e))
-        return curls - omega ** 2 * epsilon * e         # type: ignore      # issues with numpy/mypy
+        return curls - omega ** 2 * epsilon * e
 
-    def op_mu(e: fdfield_t) -> fdfield_t:
-        curls = ch(mu * ce(e))
-        return curls - omega ** 2 * epsilon * e         # type: ignore      # issues with numpy/mypy
+    def op_mu(e: cfdfield_t) -> cfdfield_t:
+        curls = ch(mu * ce(e))          # type: ignore   # mu = None ok because we don't return the function
+        return curls - omega ** 2 * epsilon * e
 
     if numpy.any(numpy.equal(mu, None)):
         return op_1
@@ -56,7 +56,7 @@ def eh_full(
         dxes: dx_lists_t,
         epsilon: fdfield_t,
         mu: fdfield_t = None
-        ) -> Callable[[fdfield_t, fdfield_t], Tuple[fdfield_t, fdfield_t]]:
+        ) -> Callable[[cfdfield_t, cfdfield_t], Tuple[cfdfield_t, cfdfield_t]]:
     """
     Wave operator for full (both E and H) field representation.
     See `operators.eh_full`.
@@ -74,13 +74,13 @@ def eh_full(
     ch = curl_back(dxes[1])
     ce = curl_forward(dxes[0])
 
-    def op_1(e: fdfield_t, h: fdfield_t) -> Tuple[fdfield_t, fdfield_t]:
+    def op_1(e: cfdfield_t, h: cfdfield_t) -> Tuple[cfdfield_t, cfdfield_t]:
         return (ch(h) - 1j * omega * epsilon * e,
-                ce(e) + 1j * omega * h)                 # type: ignore    # issues with numpy/mypy
+                ce(e) + 1j * omega * h)
 
-    def op_mu(e: fdfield_t, h: fdfield_t) -> Tuple[fdfield_t, fdfield_t]:
+    def op_mu(e: cfdfield_t, h: cfdfield_t) -> Tuple[cfdfield_t, cfdfield_t]:
         return (ch(h) - 1j * omega * epsilon * e,
-                ce(e) + 1j * omega * mu * h)            # type: ignore    # issues with numpy/mypy
+                ce(e) + 1j * omega * mu * h)            # type: ignore   # mu=None ok
 
     if numpy.any(numpy.equal(mu, None)):
         return op_1
@@ -92,7 +92,7 @@ def e2h(
         omega: complex,
         dxes: dx_lists_t,
         mu: Optional[fdfield_t] = None,
-        ) -> fdfield_updater_t:
+        ) -> cfdfield_updater_t:
     """
     Utility operator for converting the `E` field into the `H` field.
     For use with `e_full` -- assumes that there is no magnetic current `M`.
@@ -108,11 +108,11 @@ def e2h(
     """
     ce = curl_forward(dxes[0])
 
-    def e2h_1_1(e: fdfield_t) -> fdfield_t:
-        return ce(e) / (-1j * omega)            # type: ignore    # issues with numpy/mypy
+    def e2h_1_1(e: cfdfield_t) -> cfdfield_t:
+        return ce(e) / (-1j * omega)
 
-    def e2h_mu(e: fdfield_t) -> fdfield_t:
-        return ce(e) / (-1j * omega * mu)       # type: ignore    # issues with numpy/mypy
+    def e2h_mu(e: cfdfield_t) -> cfdfield_t:
+        return ce(e) / (-1j * omega * mu)       # type: ignore   # mu=None ok
 
     if numpy.any(numpy.equal(mu, None)):
         return e2h_1_1
@@ -124,7 +124,7 @@ def m2j(
         omega: complex,
         dxes: dx_lists_t,
         mu: Optional[fdfield_t] = None,
-        ) -> fdfield_updater_t:
+        ) -> cfdfield_updater_t:
     """
     Utility operator for converting magnetic current `M` distribution
     into equivalent electric current distribution `J`.
@@ -141,13 +141,13 @@ def m2j(
     """
     ch = curl_back(dxes[1])
 
-    def m2j_mu(m: fdfield_t) -> fdfield_t:
-        J = ch(m / mu) / (-1j * omega)
-        return J                          # type: ignore    # issues with numpy/mypy
+    def m2j_mu(m: cfdfield_t) -> cfdfield_t:
+        J = ch(m / mu) / (-1j * omega)          # type: ignore  # mu=None ok
+        return J
 
-    def m2j_1(m: fdfield_t) -> fdfield_t:
+    def m2j_1(m: cfdfield_t) -> cfdfield_t:
         J = ch(m) / (-1j * omega)
-        return J                          # type: ignore    # issues with numpy/mypy
+        return J
 
     if numpy.any(numpy.equal(mu, None)):
         return m2j_1
@@ -161,7 +161,7 @@ def e_tfsf_source(
         dxes: dx_lists_t,
         epsilon: fdfield_t,
         mu: Optional[fdfield_t] = None,
-        ) -> fdfield_updater_t:
+        ) -> cfdfield_updater_t:
     """
     Operator that turns an E-field distribution into a total-field/scattered-field
     (TFSF) source.
@@ -182,13 +182,13 @@ def e_tfsf_source(
     # TODO documentation
     A = e_full(omega, dxes, epsilon, mu)
 
-    def op(e: fdfield_t) -> fdfield_t:
+    def op(e: cfdfield_t) -> cfdfield_t:
         neg_iwj = A(TF_region * e) - TF_region * A(e)
         return neg_iwj / (-1j * omega)
     return op
 
 
-def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[fdfield_t, fdfield_t], fdfield_t]:
+def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], cfdfield_t]:
     """
     Generates a function that takes the single-frequency `E` and `H` fields
     and calculates the cross product `E` x `H` = $E \\times H$ as required
@@ -210,7 +210,7 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[fdfield_t, fdfield_t], fdf
     Returns:
         Function `f` that returns E x H as required for the poynting vector.
     """
-    def exh(e: fdfield_t, h: fdfield_t) -> fdfield_t:
+    def exh(e: cfdfield_t, h: cfdfield_t) -> cfdfield_t:
         s = numpy.empty_like(e)
         ex = e[0] * dxes[0][0][:, None, None]
         ey = e[1] * dxes[0][1][None, :, None]
diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py
index f36080b..ea45cba 100644
--- a/meanas/fdfd/operators.py
+++ b/meanas/fdfd/operators.py
@@ -31,7 +31,7 @@ from typing import Tuple, Optional
 import numpy
 import scipy.sparse as sparse       # type: ignore
 
-from ..fdmath import vec, dx_lists_t, vfdfield_t
+from ..fdmath import vec, dx_lists_t, vfdfield_t, vcfdfield_t
 from ..fdmath.operators import shift_with_mirror, shift_circ, curl_forward, curl_back
 
 
@@ -91,7 +91,7 @@ def e_full(
     if numpy.any(numpy.equal(mu, None)):
         m_div = sparse.eye(epsilon.size)
     else:
-        m_div = sparse.diags(1 / mu)                # type: ignore  # checked mu is not None
+        m_div = sparse.diags(1 / mu)
 
     op = pe @ (ch @ pm @ m_div @ ce - omega**2 * e) @ pe
     return op
@@ -275,7 +275,7 @@ def e2h(
     op = curl_forward(dxes[0]) / (-1j * omega)
 
     if not numpy.any(numpy.equal(mu, None)):
-        op = sparse.diags(1 / mu) @ op              # type: ignore  # checked mu is not None
+        op = sparse.diags(1 / mu) @ op
 
     if not numpy.any(numpy.equal(pmc, None)):
         op = sparse.diags(numpy.where(pmc, 0, 1)) @ op
@@ -303,12 +303,12 @@ def m2j(
     op = curl_back(dxes[1]) / (1j * omega)
 
     if not numpy.any(numpy.equal(mu, None)):
-        op = op @ sparse.diags(1 / mu)            # type: ignore  # checked mu is not None
+        op = op @ sparse.diags(1 / mu)
 
     return op
 
 
-def poynting_e_cross(e: vfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
+def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
     """
     Operator for computing the Poynting vector, containing the
     (E x) portion of the Poynting vector.
@@ -336,7 +336,7 @@ def poynting_e_cross(e: vfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
     return P
 
 
-def poynting_h_cross(h: vfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
+def poynting_h_cross(h: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
     """
     Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector.
 
diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py
index 0f9c92c..de38854 100644
--- a/meanas/fdfd/scpml.py
+++ b/meanas/fdfd/scpml.py
@@ -11,7 +11,7 @@ from numpy.typing import ArrayLike, NDArray
 __author__ = 'Jan Petykiewicz'
 
 
-s_function_t = Callable[[float], float]
+s_function_t = Callable[[NDArray[numpy.float64]], NDArray[numpy.float64]]
 """Typedef for s-functions, see `prepare_s_function()`"""
 
 
@@ -39,8 +39,8 @@ def prepare_s_function(
 
 
 def uniform_grid_scpml(
-        shape: ArrayLike,    # ints
-        thicknesses: ArrayLike,  # ints
+        shape: Sequence[int],
+        thicknesses: Sequence[int],
         omega: float,
         epsilon_effective: float = 1.0,
         s_function: Optional[s_function_t] = None,
@@ -70,12 +70,11 @@ def uniform_grid_scpml(
     if s_function is None:
         s_function = prepare_s_function()
 
+    shape = tuple(shape)
+    thicknesses = tuple(thicknesses)
+
     # Normalized distance to nearest boundary
-    def ll(
-            u: NDArray[numpy.float64],
-            n: NDArray[numpy.float64],
-            t: NDArray[numpy.float64],
-            ) -> NDArray[numpy.float64]:
+    def ll(u: NDArray[numpy.float64], n: int, t: int) -> NDArray[numpy.float64]:
         return ((t - u).clip(0) + (u - (n - t)).clip(0)) / t
 
     dx_a = [numpy.array(numpy.inf)] * 3
diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py
index 0688966..6ced908 100644
--- a/meanas/fdfd/solvers.py
+++ b/meanas/fdfd/solvers.py
@@ -10,7 +10,7 @@ from numpy.typing import ArrayLike, NDArray
 from numpy.linalg import norm
 import scipy.sparse.linalg          # type: ignore
 
-from ..fdmath import dx_lists_t, vfdfield_t
+from ..fdmath import dx_lists_t, vfdfield_t, vcfdfield_t
 from . import operators
 
 
@@ -65,7 +65,7 @@ def _scipy_qmr(
 def generic(
         omega: complex,
         dxes: dx_lists_t,
-        J: vfdfield_t,
+        J: vcfdfield_t,
         epsilon: vfdfield_t,
         mu: vfdfield_t = None,
         pec: vfdfield_t = None,
@@ -73,7 +73,7 @@ def generic(
         adjoint: bool = False,
         matrix_solver: Callable[..., ArrayLike] = _scipy_qmr,
         matrix_solver_opts: Optional[Dict[str, Any]] = None,
-        ) -> vfdfield_t:
+        ) -> vcfdfield_t:
     """
     Conjugate gradient FDFD solver using CSR sparse matrices.
 
diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 177d8d3..69d0b19 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -185,7 +185,7 @@ from numpy.linalg import norm
 import scipy.sparse as sparse       # type: ignore
 
 from ..fdmath.operators import deriv_forward, deriv_back, cross
-from ..fdmath import unvec, dx_lists_t, vfdfield_t
+from ..fdmath import unvec, dx_lists_t, vfdfield_t, vcfdfield_t
 from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
 
 
@@ -335,7 +335,7 @@ def normalized_fields_e(
         epsilon: vfdfield_t,
         mu: Optional[vfdfield_t] = None,
         prop_phase: float = 0,
-        ) -> Tuple[vfdfield_t, vfdfield_t]:
+        ) -> Tuple[vcfdfield_t, vcfdfield_t]:
     """
     Given a vector `e_xy` containing the vectorized E_x and E_y fields,
      returns normalized, vectorized E and H fields for the system.
@@ -370,7 +370,7 @@ def normalized_fields_h(
         epsilon: vfdfield_t,
         mu: Optional[vfdfield_t] = None,
         prop_phase: float = 0,
-        ) -> Tuple[vfdfield_t, vfdfield_t]:
+        ) -> Tuple[vcfdfield_t, vcfdfield_t]:
     """
     Given a vector `h_xy` containing the vectorized H_x and H_y fields,
      returns normalized, vectorized E and H fields for the system.
@@ -398,14 +398,14 @@ def normalized_fields_h(
 
 
 def _normalized_fields(
-        e: ArrayLike,
-        h: ArrayLike,
+        e: vcfdfield_t,
+        h: vcfdfield_t,
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
         mu: Optional[vfdfield_t] = None,
         prop_phase: float = 0,
-        ) -> Tuple[vfdfield_t, vfdfield_t]:
+        ) -> Tuple[vcfdfield_t, vcfdfield_t]:
     # TODO documentation
     shape = [s.size for s in dxes[0]]
     dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
@@ -581,7 +581,7 @@ def e2h(
     """
     op = curl_e(wavenumber, dxes) / (-1j * omega)
     if not numpy.any(numpy.equal(mu, None)):
-        op = sparse.diags(1 / mu) @ op          # type: ignore   # checked that mu is not None
+        op = sparse.diags(1 / mu) @ op
     return op
 
 
@@ -649,7 +649,7 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
 
 
 def h_err(
-        h: vfdfield_t,
+        h: vcfdfield_t,
         wavenumber: complex,
         omega: complex,
         dxes: dx_lists_t,
@@ -684,12 +684,12 @@ def h_err(
 
 
 def e_err(
-        e: vfdfield_t,
+        e: vcfdfield_t,
         wavenumber: complex,
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: vfdfield_t = Optional[None]
+        mu: Optional[vfdfield_t] = None,
         ) -> float:
     """
     Calculates the relative error in the E field
@@ -711,7 +711,7 @@ def e_err(
     if numpy.any(numpy.equal(mu, None)):
         op = ch @ ce @ e - omega ** 2 * (epsilon * e)
     else:
-        mu_inv = sparse.diags(1 / mu)          # type: ignore   # checked that mu is not None
+        mu_inv = sparse.diags(1 / mu)
         op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e)
 
     return norm(op) / norm(e)
@@ -724,7 +724,7 @@ def solve_modes(
         epsilon: vfdfield_t,
         mu: vfdfield_t = None,
         mode_margin: int = 2,
-        ) -> Tuple[NDArray[numpy.float64], List[complex]]:
+        ) -> Tuple[NDArray[numpy.float64], NDArray[numpy.complex128]]:
     """
     Given a 2D region, attempts to solve for the eigenmode with the specified mode numbers.
 
@@ -771,7 +771,7 @@ def solve_mode(
         mode_number: int,
         *args: Any,
         **kwargs: Any,
-        ) -> Tuple[vfdfield_t, complex]:
+        ) -> Tuple[vcfdfield_t, complex]:
     """
     Wrapper around `solve_modes()` that solves for a single mode.
 
diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py
index a6a2cba..9051123 100644
--- a/meanas/fdfd/waveguide_3d.py
+++ b/meanas/fdfd/waveguide_3d.py
@@ -8,7 +8,7 @@ from typing import Dict, Optional, Sequence, Union, Any
 import numpy
 from numpy.typing import NDArray
 
-from ..fdmath import vec, unvec, dx_lists_t, fdfield_t
+from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, cfdfield_t
 from . import operators, waveguide_2d
 
 
@@ -106,7 +106,7 @@ def solve_mode(
 
 
 def compute_source(
-        E: fdfield_t,
+        E: cfdfield_t,
         wavenumber: complex,
         omega: complex,
         dxes: dx_lists_t,
@@ -115,7 +115,7 @@ def compute_source(
         slices: Sequence[slice],
         epsilon: fdfield_t,
         mu: Optional[fdfield_t] = None,
-        ) -> fdfield_t:
+        ) -> cfdfield_t:
     """
     Given an eigenmode obtained by `solve_mode`, returns the current source distribution
     necessary to position a unidirectional source at the slice location.
@@ -152,13 +152,13 @@ def compute_source(
 
 
 def compute_overlap_e(
-        E: fdfield_t,
+        E: cfdfield_t,
         wavenumber: complex,
         dxes: dx_lists_t,
         axis: int,
         polarity: int,
         slices: Sequence[slice],
-        ) -> fdfield_t:                 # TODO DOCS
+        ) -> cfdfield_t:                 # TODO DOCS
     """
     Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the
     mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn)
@@ -200,13 +200,13 @@ def compute_overlap_e(
 
 
 def expand_e(
-        E: fdfield_t,
+        E: cfdfield_t,
         wavenumber: complex,
         dxes: dx_lists_t,
         axis: int,
         polarity: int,
         slices: Sequence[slice],
-        ) -> fdfield_t:
+        ) -> cfdfield_t:
     """
     Given an eigenmode obtained by `solve_mode`, expands the E-field from the 2D
     slice where the mode was calculated to the entire domain (along the propagation
diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index a1a0633..2286d4c 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -12,7 +12,7 @@ from typing import Dict, Union
 import numpy
 import scipy.sparse as sparse       # type: ignore
 
-from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t
+from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t, cfdfield_t
 from ..fdmath.operators import deriv_forward, deriv_back
 from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
 
@@ -85,7 +85,7 @@ def solve_mode(
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
         r0: float,
-        ) -> Dict[str, Union[complex, fdfield_t]]:
+        ) -> Dict[str, Union[complex, cfdfield_t]]:
     """
     TODO: fixup
     Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
@@ -103,8 +103,8 @@ def solve_mode(
     Returns:
         ```
         {
-            'E': List[NDArray[numpy.float_]],
-            'H': List[NDArray[numpy.float_]],
+            'E': List[NDArray[numpy.complex_]],
+            'H': List[NDArray[numpy.complex_]],
             'wavenumber': complex,
         }
         ```
diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py
index 4b629f9..40c68e7 100644
--- a/meanas/fdmath/__init__.py
+++ b/meanas/fdmath/__init__.py
@@ -741,7 +741,8 @@ the true values can be multiplied back in after the simulation is complete if no
 normalized results are needed.
 """
 
-from .types import fdfield_t, vfdfield_t, dx_lists_t, dx_lists_mut, fdfield_updater_t
+from .types import fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t, dx_lists_t, dx_lists_mut
+from .types import fdfield_updater_t, cfdfield_updater_t
 from .vectorization import vec, unvec
 from . import operators, functional, types, vectorization
 
diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py
index 2cc5172..27cd44e 100644
--- a/meanas/fdmath/functional.py
+++ b/meanas/fdmath/functional.py
@@ -24,7 +24,7 @@ def deriv_forward(
     Returns:
         List of functions for taking forward derivatives along each axis.
     """
-    if dx_e:
+    if dx_e is not None:
         derivs = (lambda f: (numpy.roll(f, -1, axis=0) - f) / dx_e[0][:, None, None],
                   lambda f: (numpy.roll(f, -1, axis=1) - f) / dx_e[1][None, :, None],
                   lambda f: (numpy.roll(f, -1, axis=2) - f) / dx_e[2][None, None, :])
@@ -48,7 +48,7 @@ def deriv_back(
     Returns:
         List of functions for taking forward derivatives along each axis.
     """
-    if dx_h:
+    if dx_h is not None:
         derivs = (lambda f: (f - numpy.roll(f, 1, axis=0)) / dx_h[0][:, None, None],
                   lambda f: (f - numpy.roll(f, 1, axis=1)) / dx_h[1][None, :, None],
                   lambda f: (f - numpy.roll(f, 1, axis=2)) / dx_h[2][None, None, :])
@@ -122,7 +122,7 @@ def curl_forward_parts(
         ) -> Callable:
     Dx, Dy, Dz = deriv_forward(dx_e)
 
-    def mkparts_fwd(e: fdfield_t) -> Tuple[Tuple[fdfield_t, ...]]:
+    def mkparts_fwd(e: fdfield_t) -> Tuple[Tuple[fdfield_t, fdfield_t], ...]:
         return ((-Dz(e[1]),  Dy(e[2])),
                 ( Dz(e[0]), -Dx(e[2])),
                 (-Dy(e[0]),  Dx(e[1])))
@@ -135,7 +135,7 @@ def curl_back_parts(
         ) -> Callable:
     Dx, Dy, Dz = deriv_back(dx_h)
 
-    def mkparts_back(h: fdfield_t) -> Tuple[Tuple[fdfield_t, ...]]:
+    def mkparts_back(h: fdfield_t) -> Tuple[Tuple[fdfield_t, fdfield_t], ...]:
         return ((-Dz(h[1]),  Dy(h[2])),
                 ( Dz(h[0]), -Dx(h[2])),
                 (-Dy(h[0]),  Dx(h[1])))
diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py
index 2dd0040..aae9594 100644
--- a/meanas/fdmath/types.py
+++ b/meanas/fdmath/types.py
@@ -13,6 +13,12 @@ fdfield_t = NDArray[numpy.float_]
 vfdfield_t = NDArray[numpy.float_]
 """Linearized vector field (single vector of length 3*X*Y*Z)"""
 
+cfdfield_t = NDArray[numpy.complex_]
+"""Complex vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)"""
+
+vcfdfield_t = NDArray[numpy.complex_]
+"""Linearized complex vector field (single vector of length 3*X*Y*Z)"""
+
 
 dx_lists_t = Sequence[Sequence[NDArray[numpy.float_]]]
 """
@@ -31,3 +37,6 @@ dx_lists_mut = MutableSequence[MutableSequence[NDArray[numpy.float_]]]
 
 fdfield_updater_t = Callable[..., fdfield_t]
 """Convenience type for functions which take and return an fdfield_t"""
+
+cfdfield_updater_t = Callable[..., cfdfield_t]
+"""Convenience type for functions which take and return an cfdfield_t"""
diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py
index 6b6c49e..ef97f7c 100644
--- a/meanas/fdmath/vectorization.py
+++ b/meanas/fdmath/vectorization.py
@@ -4,11 +4,11 @@ and a 1D array representation of that field `[f_x0, f_x1, f_x2,... f_y0,... f_z0
 Vectorized versions of the field use row-major (ie., C-style) ordering.
 """
 
-from typing import Optional, overload, Union, List
+from typing import Optional, overload, Union, Sequence
 import numpy
 from numpy.typing import ArrayLike
 
-from .types import fdfield_t, vfdfield_t
+from .types import fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t
 
 
 @overload
@@ -16,10 +16,18 @@ def vec(f: None) -> None:
     pass
 
 @overload
-def vec(f: Union[fdfield_t, List[ArrayLike]]) -> vfdfield_t:
+def vec(f: fdfield_t) -> vfdfield_t:
     pass
 
-def vec(f: Optional[Union[fdfield_t, List[ArrayLike]]]) -> Optional[vfdfield_t]:
+@overload
+def vec(f: cfdfield_t) -> vcfdfield_t:
+    pass
+
+@overload
+def vec(f: ArrayLike) -> Union[vfdfield_t, vcfdfield_t]:
+    pass
+
+def vec(f: Union[fdfield_t, cfdfield_t, ArrayLike, None]) -> Union[vfdfield_t, vcfdfield_t, None]:
     """
     Create a 1D ndarray from a 3D vector field which spans a 1-3D region.
 
@@ -38,14 +46,18 @@ def vec(f: Optional[Union[fdfield_t, List[ArrayLike]]]) -> Optional[vfdfield_t]:
 
 
 @overload
-def unvec(v: None, shape: ArrayLike) -> None:
+def unvec(v: None, shape: Sequence[int]) -> None:
     pass
 
 @overload
-def unvec(v: vfdfield_t, shape: ArrayLike) -> fdfield_t:
+def unvec(v: vfdfield_t, shape: Sequence[int]) -> fdfield_t:
     pass
 
-def unvec(v: Optional[vfdfield_t], shape: ArrayLike) -> Optional[fdfield_t]:
+@overload
+def unvec(v: vcfdfield_t, shape: Sequence[int]) -> cfdfield_t:
+    pass
+
+def unvec(v: Union[vfdfield_t, vcfdfield_t, None], shape: Sequence[int]) -> Union[fdfield_t, cfdfield_t, None]:
     """
     Perform the inverse of vec(): take a 1D ndarray and output a 3D field
      of form `[f_x, f_y, f_z]` where each of `f_*` is a len(shape)-dimensional
@@ -62,5 +74,5 @@ def unvec(v: Optional[vfdfield_t], shape: ArrayLike) -> Optional[fdfield_t]:
     """
     if numpy.any(numpy.equal(v, None)):
         return None
-    return v.reshape((3, *shape), order='C')            # type: ignore  # already check v is not None
+    return v.reshape((3, *shape), order='C')
 
diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py
index 6f7aff7..1781485 100644
--- a/meanas/fdtd/pml.py
+++ b/meanas/fdtd/pml.py
@@ -8,8 +8,9 @@ PML implementations
 # TODO retest pmls!
 
 from typing import List, Callable, Tuple, Dict, Sequence, Any, Optional
+from copy import deepcopy
 import numpy
-from typing import NDArray
+from numpy.typing import NDArray, DTypeLike
 
 from ..fdmath import fdfield_t, dx_lists_t
 from ..fdmath.functional import deriv_forward, deriv_back
@@ -97,34 +98,38 @@ def updates_with_cpml(
          dxes: dx_lists_t,
          epsilon: fdfield_t,
          *,
-         dtype: numpy.dtype = numpy.float32,
-         ) -> Tuple[Callable[[fdfield_t, fdfield_t], None],
-                    Callable[[fdfield_t, fdfield_t], None]]:
+         dtype: DTypeLike = numpy.float32,
+         ) -> Tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None],
+                    Callable[[fdfield_t, fdfield_t, fdfield_t], None]]:
 
     Dfx, Dfy, Dfz = deriv_forward(dxes[1])
     Dbx, Dby, Dbz = deriv_back(dxes[1])
 
-    psi_E = [[None, None], [None, None], [None, None]]
-    psi_H = [[None, None], [None, None], [None, None]]
-    params_E = [[None, None], [None, None], [None, None]]
-    params_H = [[None, None], [None, None], [None, None]]
+
+    psi_E: List[List[Tuple[Any, Any]]] = [[(None, None) for _ in range(2)] for _ in range(3)]
+    psi_H: List[List[Tuple[Any, Any]]] = deepcopy(psi_E)
+    params_E: List[List[Tuple[Any, Any, Any, Any]]] = [[(None, None, None, None) for _ in range(2)] for _ in range(3)]
+    params_H: List[List[Tuple[Any, Any, Any, Any]]] = deepcopy(params_E)
 
     for axis in range(3):
         for pp, polarity in enumerate((-1, 1)):
-            if cpml_params[axis][pp] is None:
+            cpml_param = cpml_params[axis][pp]
+            if cpml_param is None:
                 psi_E[axis][pp] = (None, None)
                 psi_H[axis][pp] = (None, None)
                 continue
 
-            cpml_param = cpml_params[axis][pp]
-
             region = cpml_param['region']
             region_shape = epsilon[0][region].shape
 
-            psi_E[axis][pp] = (numpy.zeros(region_shape, dtype=dtype),
-                                    numpy.zeros(region_shape, dtype=dtype))
-            psi_H[axis][pp] = (numpy.zeros(region_shape, dtype=dtype),
-                                    numpy.zeros(region_shape, dtype=dtype))
+            psi_E[axis][pp] = (
+                numpy.zeros(region_shape, dtype=dtype),
+                numpy.zeros(region_shape, dtype=dtype),
+                )
+            psi_H[axis][pp] = (
+                numpy.zeros(region_shape, dtype=dtype),
+                numpy.zeros(region_shape, dtype=dtype),
+                )
             params_E[axis][pp] = cpml_param['param_e'] + (region,)
             params_H[axis][pp] = cpml_param['param_h'] + (region,)
 
@@ -132,7 +137,11 @@ def updates_with_cpml(
     pE = numpy.empty_like(epsilon, dtype=dtype)
     pH = numpy.empty_like(epsilon, dtype=dtype)
 
-    def update_E(e: fdfield_t, h: fdfield_t, epsilon: fdfield_t) -> None:
+    def update_E(
+            e: fdfield_t,
+            h: fdfield_t,
+            epsilon: fdfield_t,
+            ) -> None:
         dyHx = Dby(h[0])
         dzHx = Dbz(h[0])
         dxHy = Dbx(h[1])
@@ -175,7 +184,11 @@ def updates_with_cpml(
         e[2] += dt / epsilon[2] * (dxHy - dyHx + pE[2])
 
 
-    def update_H(e: fdfield_t, h: fdfield_t, mu: fdfield_t = (1, 1, 1)) -> None:
+    def update_H(
+            e: fdfield_t,
+            h: fdfield_t,
+            mu: fdfield_t = numpy.ones(3),
+            ) -> None:
         dyEx = Dfy(e[0])
         dzEx = Dfz(e[0])
         dxEy = Dfx(e[1])
diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py
index fef80fd..e5a5875 100644
--- a/meanas/test/test_fdfd.py
+++ b/meanas/test/test_fdfd.py
@@ -99,8 +99,8 @@ class FDResult:
     dxes: List[List[NDArray[numpy.float64]]]
     epsilon: NDArray[numpy.float64]
     omega: complex
-    j: NDArray[numpy.float64]
-    e: NDArray[numpy.float64]
+    j: NDArray[numpy.complex128]
+    e: NDArray[numpy.complex128]
     pmc: Optional[NDArray[numpy.float64]]
     pec: Optional[NDArray[numpy.float64]]
 
@@ -111,7 +111,7 @@ def sim(
         shape: Tuple[int, ...],
         epsilon: NDArray[numpy.float64],
         dxes: List[List[NDArray[numpy.float64]]],
-        j_distribution: NDArray[numpy.float64],
+        j_distribution: NDArray[numpy.complex128],
         omega: float,
         pec: Optional[NDArray[numpy.float64]],
         pmc: Optional[NDArray[numpy.float64]],
@@ -134,8 +134,13 @@ def sim(
 
     j_vec = vec(j_distribution)
     eps_vec = vec(epsilon)
-    e_vec = fdfd.solvers.generic(J=j_vec, omega=omega, dxes=dxes, epsilon=eps_vec,
-                                 matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11})
+    e_vec = fdfd.solvers.generic(
+        J=j_vec,
+        omega=omega,
+        dxes=dxes,
+        epsilon=eps_vec,
+        matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11},
+        )
     e = unvec(e_vec, shape[1:])
 
     sim = FDResult(
diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py
index 30eb32d..ff6e4c2 100644
--- a/meanas/test/test_fdfd_pml.py
+++ b/meanas/test/test_fdfd_pml.py
@@ -78,7 +78,7 @@ def j_distribution(
         dxes: dx_lists_mut,
         omega: float,
         src_polarity: int,
-        ) -> Iterable[NDArray[numpy.float64]]:
+        ) -> Iterable[NDArray[numpy.complex128]]:
     j = numpy.zeros(shape, dtype=complex)
 
     dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0]    # Propagation axis
@@ -150,7 +150,7 @@ def sim(
         shape: Tuple[int, ...],
         epsilon: NDArray[numpy.float64],
         dxes: dx_lists_mut,
-        j_distribution: NDArray[numpy.float64],
+        j_distribution: NDArray[numpy.complex128],
         omega: float,
         pec: Optional[NDArray[numpy.float64]],
         pmc: Optional[NDArray[numpy.float64]],
diff --git a/meanas/test/utils.py b/meanas/test/utils.py
index b4cb3ab..8d26d11 100644
--- a/meanas/test/utils.py
+++ b/meanas/test/utils.py
@@ -1,14 +1,15 @@
 from typing import Any
+
 import numpy
-from typing import ArrayLike
+from numpy.typing import ArrayLike, NDArray
 
 
 PRNG = numpy.random.RandomState(12345)
 
 
 def assert_fields_close(
-        x: ArrayLike,
-        y: ArrayLike,
+        x: NDArray,
+        y: NDArray,
         *args: Any,
         **kwargs: Any,
         ) -> None:
@@ -18,8 +19,8 @@ def assert_fields_close(
                                                        numpy.rollaxis(y, -1)), *args, **kwargs)
 
 def assert_close(
-        x: ArrayLike,
-        y: ArrayLike,
+        x: NDArray,
+        y: NDArray,
         *args: Any,
         **kwargs: Any,
         ) -> None:

From 4e240988c944ee8f26025a9266173e570c623e5e Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 17:27:44 -0700
Subject: [PATCH 272/437] use "is None" to check for default args

numpy.any(numpy.equal(x, None)) is more general, because
`numpy.array(None) is not None`, but checking for that doesn't make much
sense if you're already type-checking
---
 meanas/eigensolvers.py         |  2 +-
 meanas/fdfd/functional.py      |  8 ++++----
 meanas/fdfd/operators.py       | 22 +++++++++++-----------
 meanas/fdfd/waveguide_2d.py    | 14 +++++++-------
 meanas/fdmath/vectorization.py |  4 ++--
 5 files changed, 25 insertions(+), 25 deletions(-)

diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py
index 4b96f60..6b3fba2 100644
--- a/meanas/eigensolvers.py
+++ b/meanas/eigensolvers.py
@@ -25,7 +25,7 @@ def power_iteration(
     Returns:
         (Largest-magnitude eigenvalue, Corresponding eigenvector estimate)
     """
-    if numpy.any(numpy.equal(guess_vector, None)):
+    if guess_vector is None:
         v = numpy.random.rand(operator.shape[0]) + 1j * numpy.random.rand(operator.shape[0])
     else:
         v = guess_vector
diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py
index 1a1506b..cfc6187 100644
--- a/meanas/fdfd/functional.py
+++ b/meanas/fdfd/functional.py
@@ -45,7 +45,7 @@ def e_full(
         curls = ch(mu * ce(e))          # type: ignore   # mu = None ok because we don't return the function
         return curls - omega ** 2 * epsilon * e
 
-    if numpy.any(numpy.equal(mu, None)):
+    if mu is None:
         return op_1
     else:
         return op_mu
@@ -82,7 +82,7 @@ def eh_full(
         return (ch(h) - 1j * omega * epsilon * e,
                 ce(e) + 1j * omega * mu * h)            # type: ignore   # mu=None ok
 
-    if numpy.any(numpy.equal(mu, None)):
+    if mu is None:
         return op_1
     else:
         return op_mu
@@ -114,7 +114,7 @@ def e2h(
     def e2h_mu(e: cfdfield_t) -> cfdfield_t:
         return ce(e) / (-1j * omega * mu)       # type: ignore   # mu=None ok
 
-    if numpy.any(numpy.equal(mu, None)):
+    if mu is None:
         return e2h_1_1
     else:
         return e2h_mu
@@ -149,7 +149,7 @@ def m2j(
         J = ch(m) / (-1j * omega)
         return J
 
-    if numpy.any(numpy.equal(mu, None)):
+    if mu is None:
         return m2j_1
     else:
         return m2j_mu
diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py
index ea45cba..86d9fc8 100644
--- a/meanas/fdfd/operators.py
+++ b/meanas/fdfd/operators.py
@@ -77,18 +77,18 @@ def e_full(
     ch = curl_back(dxes[1])
     ce = curl_forward(dxes[0])
 
-    if numpy.any(numpy.equal(pec, None)):
+    if pec is None:
         pe = sparse.eye(epsilon.size)
     else:
         pe = sparse.diags(numpy.where(pec, 0, 1))     # Set pe to (not PEC)
 
-    if numpy.any(numpy.equal(pmc, None)):
+    if pmc is None:
         pm = sparse.eye(epsilon.size)
     else:
         pm = sparse.diags(numpy.where(pmc, 0, 1))     # set pm to (not PMC)
 
     e = sparse.diags(epsilon)
-    if numpy.any(numpy.equal(mu, None)):
+    if mu is None:
         m_div = sparse.eye(epsilon.size)
     else:
         m_div = sparse.diags(1 / mu)
@@ -161,12 +161,12 @@ def h_full(
     ch = curl_back(dxes[1])
     ce = curl_forward(dxes[0])
 
-    if numpy.any(numpy.equal(pec, None)):
+    if pec is None:
         pe = sparse.eye(epsilon.size)
     else:
         pe = sparse.diags(numpy.where(pec, 0, 1))    # set pe to (not PEC)
 
-    if numpy.any(numpy.equal(pmc, None)):
+    if pmc is None:
         pm = sparse.eye(epsilon.size)
     else:
         pm = sparse.diags(numpy.where(pmc, 0, 1))    # Set pe to (not PMC)
@@ -227,19 +227,19 @@ def eh_full(
     Returns:
         Sparse matrix containing the wave operator.
     """
-    if numpy.any(numpy.equal(pec, None)):
+    if pec is None:
         pe = sparse.eye(epsilon.size)
     else:
         pe = sparse.diags(numpy.where(pec, 0, 1))    # set pe to (not PEC)
 
-    if numpy.any(numpy.equal(pmc, None)):
+    if pmc is None:
         pm = sparse.eye(epsilon.size)
     else:
         pm = sparse.diags(numpy.where(pmc, 0, 1))    # set pm to (not PMC)
 
     iwe = pe @ (1j * omega * sparse.diags(epsilon)) @ pe
     iwm = 1j * omega
-    if not numpy.any(numpy.equal(mu, None)):
+    if mu is not None:
         iwm *= sparse.diags(mu)
     iwm = pm @ iwm @ pm
 
@@ -274,10 +274,10 @@ def e2h(
     """
     op = curl_forward(dxes[0]) / (-1j * omega)
 
-    if not numpy.any(numpy.equal(mu, None)):
+    if mu is not None:
         op = sparse.diags(1 / mu) @ op
 
-    if not numpy.any(numpy.equal(pmc, None)):
+    if pmc is not None:
         op = sparse.diags(numpy.where(pmc, 0, 1)) @ op
 
     return op
@@ -302,7 +302,7 @@ def m2j(
     """
     op = curl_back(dxes[1]) / (1j * omega)
 
-    if not numpy.any(numpy.equal(mu, None)):
+    if mu is not None:
         op = op @ sparse.diags(1 / mu)
 
     return op
diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 69d0b19..3ac95d5 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -239,7 +239,7 @@ def operator_e(
     Returns:
         Sparse matrix representation of the operator.
     """
-    if numpy.any(numpy.equal(mu, None)):
+    if mu is None:
         mu = numpy.ones_like(epsilon)
 
     Dfx, Dfy = deriv_forward(dxes[0])
@@ -306,7 +306,7 @@ def operator_h(
     Returns:
         Sparse matrix representation of the operator.
     """
-    if numpy.any(numpy.equal(mu, None)):
+    if mu is None:
         mu = numpy.ones_like(epsilon)
 
     Dfx, Dfy = deriv_forward(dxes[0])
@@ -513,7 +513,7 @@ def hxy2h(
     Dfx, Dfy = deriv_forward(dxes[0])
     hxy2hz = sparse.hstack((Dfx, Dfy)) / (1j * wavenumber)
 
-    if not numpy.any(numpy.equal(mu, None)):
+    if mu is not None:
         mu_parts = numpy.split(mu, 3)
         mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1])))
         mu_z_inv = sparse.diags(1 / mu_parts[2])
@@ -547,7 +547,7 @@ def exy2e(
     Dbx, Dby = deriv_back(dxes[1])
     exy2ez = sparse.hstack((Dbx, Dby)) / (1j * wavenumber)
 
-    if not numpy.any(numpy.equal(epsilon, None)):
+    if epsilon is not None:
         epsilon_parts = numpy.split(epsilon, 3)
         epsilon_xy = sparse.diags(numpy.hstack((epsilon_parts[0], epsilon_parts[1])))
         epsilon_z_inv = sparse.diags(1 / epsilon_parts[2])
@@ -580,7 +580,7 @@ def e2h(
         Sparse matrix representation of the operator.
     """
     op = curl_e(wavenumber, dxes) / (-1j * omega)
-    if not numpy.any(numpy.equal(mu, None)):
+    if mu is not None:
         op = sparse.diags(1 / mu) @ op
     return op
 
@@ -675,7 +675,7 @@ def h_err(
 
     eps_inv = sparse.diags(1 / epsilon)
 
-    if numpy.any(numpy.equal(mu, None)):
+    if mu is None:
         op = ce @ eps_inv @ ch @ h - omega ** 2 * h
     else:
         op = ce @ eps_inv @ ch @ h - omega ** 2 * (mu * h)
@@ -708,7 +708,7 @@ def e_err(
     ce = curl_e(wavenumber, dxes)
     ch = curl_h(wavenumber, dxes)
 
-    if numpy.any(numpy.equal(mu, None)):
+    if mu is None:
         op = ch @ ce @ e - omega ** 2 * (epsilon * e)
     else:
         mu_inv = sparse.diags(1 / mu)
diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py
index ef97f7c..5d9e932 100644
--- a/meanas/fdmath/vectorization.py
+++ b/meanas/fdmath/vectorization.py
@@ -40,7 +40,7 @@ def vec(f: Union[fdfield_t, cfdfield_t, ArrayLike, None]) -> Union[vfdfield_t, v
     Returns:
         1D ndarray containing the linearized field (or `None`)
     """
-    if numpy.any(numpy.equal(f, None)):
+    if f is None:
         return None
     return numpy.ravel(f, order='C')
 
@@ -72,7 +72,7 @@ def unvec(v: Union[vfdfield_t, vcfdfield_t, None], shape: Sequence[int]) -> Unio
     Returns:
         `[f_x, f_y, f_z]` where each `f_` is a `len(shape)` dimensional ndarray (or `None`)
     """
-    if numpy.any(numpy.equal(v, None)):
+    if v is None:
         return None
     return v.reshape((3, *shape), order='C')
 

From 640b4d1ef7a8b9602482ee8a575884655b50ee42 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 17:27:58 -0700
Subject: [PATCH 273/437] Make find-k also return eigenvalues

---
 meanas/fdfd/bloch.py | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 0e3ca51..3ffc1b6 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -414,7 +414,7 @@ def find_k(
         k_min: float = 0,
         k_max: float = 0.5,
         solve_callback: Optional[Callable] = None
-        ) -> Tuple[NDArray[numpy.float64], float]:
+        ) -> Tuple[float, float, NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Search for a bloch vector that has a given frequency.
 
@@ -430,12 +430,16 @@ def find_k(
         band: Which band to search in. Default 0 (lowest frequency).
 
     Returns:
-        `(k, actual_frequency)`
-        The found k-vector and its frequency.
+        `(k, actual_frequency, eigenvalues, eigenvectors)`
+        The found k-vector and its frequency, along with all eigenvalues and eigenvectors.
     """
     direction = numpy.array(direction) / norm(direction)
 
+    n = None
+    v = None
+
     def get_f(k0_mag: float, band: int = 0) -> float:
+        nonlocal n, v
         k0 = direction * k0_mag                         # type: ignore
         n, v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu)
         f = numpy.sqrt(numpy.abs(numpy.real(n[band])))
@@ -450,7 +454,10 @@ def find_k(
         bounds=(k_min, k_max),
         options={'xatol': abs(tolerance)},
         )
-    return res.x * direction, res.fun + frequency
+
+    assert n is not None
+    assert v is not None
+    return float(res.x * direction), float(res.fun + frequency), n, v
 
 
 def eigsolve(

From ad7c27eee38b82fd037ccec0934fa82b8473db93 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 4 Oct 2022 17:29:13 -0700
Subject: [PATCH 274/437] bump version to v0.8

---
 meanas/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/__init__.py b/meanas/__init__.py
index 3b426e3..b35edce 100644
--- a/meanas/__init__.py
+++ b/meanas/__init__.py
@@ -6,7 +6,7 @@ See the readme or `import meanas; help(meanas)` for more info.
 
 import pathlib
 
-__version__ = '0.7'
+__version__ = '0.8'
 __author__ = 'Jan Petykiewicz'
 
 

From edfd9a49c6ef1bc74b127f906bd43170a5e7dc33 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Thu, 6 Oct 2022 13:45:04 -0700
Subject: [PATCH 275/437] formatting

---
 meanas/test/utils.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/meanas/test/utils.py b/meanas/test/utils.py
index 8d26d11..cc7e839 100644
--- a/meanas/test/utils.py
+++ b/meanas/test/utils.py
@@ -16,7 +16,10 @@ def assert_fields_close(
     numpy.testing.assert_allclose(
         x, y, verbose=False,
         err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(x, -1),
-                                                       numpy.rollaxis(y, -1)), *args, **kwargs)
+                                                       numpy.rollaxis(y, -1)),
+        *args,
+        **kwargs,
+        )
 
 def assert_close(
         x: NDArray,

From 86feb5461ce1c8152b73b029bbc61cebf137ddfb Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 14 Nov 2022 12:19:55 -0800
Subject: [PATCH 276/437] don't use patient planner by default

---
 meanas/fdfd/bloch.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 3ffc1b6..fb60d6b 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -108,7 +108,7 @@ try:
     fftw_args = {
         #'threads': multiprocessing.cpu_count(),
         'overwrite_input': True,
-        'planner_effort': 'FFTW_PATIENT',
+        #'planner_effort': 'FFTW_PATIENT',
         }
 
     def fftn(*args: Any, **kwargs: Any) -> NDArray[numpy.complex128]:

From a82d8dfc7e0cd1dbf0bccae9b31779ebdcd806ec Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 14 Nov 2022 12:20:33 -0800
Subject: [PATCH 277/437] pass k_bounds and k_guess instad of just k_min and
 k_max

---
 meanas/fdfd/bloch.py | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index fb60d6b..39cc788 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -411,9 +411,9 @@ def find_k(
         epsilon: fdfield_t,
         mu: Optional[fdfield_t] = None,
         band: int = 0,
-        k_min: float = 0,
-        k_max: float = 0.5,
         solve_callback: Optional[Callable] = None
+        k_bounds: Tuple[float, float] = (0, 0.5),
+        k_guess: Optional[float] = None,
         ) -> Tuple[float, float, NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Search for a bloch vector that has a given frequency.
@@ -428,6 +428,8 @@ def find_k(
         mu: Magnetic permability distribution for the simulation.
             Default None (1 everywhere).
         band: Which band to search in. Default 0 (lowest frequency).
+        k_bounds: Minimum and maximum values for k. Default (0, 0.5).
+        k_guess: Initial value for k.
 
     Returns:
         `(k, actual_frequency, eigenvalues, eigenvectors)`
@@ -435,6 +437,12 @@ def find_k(
     """
     direction = numpy.array(direction) / norm(direction)
 
+    k_bounds = tuple(sorted(k_bounds))
+    assert len(k_bounds) == 2
+
+    if k_guess is None:
+        k_guess = sum(k_bounds) / 2
+
     n = None
     v = None
 
@@ -449,9 +457,9 @@ def find_k(
 
     res = scipy.optimize.minimize_scalar(
         lambda x: abs(get_f(x, band) - frequency),
-        (k_min + k_max) / 2,
+        k_guess,
         method='Bounded',
-        bounds=(k_min, k_max),
+        bounds=k_bounds,
         options={'xatol': abs(tolerance)},
         )
 

From c7c71a3a8236970b92a8599799a4dbaaf5be7426 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 14 Nov 2022 12:20:50 -0800
Subject: [PATCH 278/437] add per-iteration callback

---
 meanas/fdfd/bloch.py | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 39cc788..ea2740f 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -411,9 +411,10 @@ def find_k(
         epsilon: fdfield_t,
         mu: Optional[fdfield_t] = None,
         band: int = 0,
-        solve_callback: Optional[Callable] = None
         k_bounds: Tuple[float, float] = (0, 0.5),
         k_guess: Optional[float] = None,
+        solve_callback: Optional[Callable[[...], None]] = None,
+        iter_callback: Optional[Callable[[...], None]] = None,
         ) -> Tuple[float, float, NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Search for a bloch vector that has a given frequency.
@@ -430,6 +431,8 @@ def find_k(
         band: Which band to search in. Default 0 (lowest frequency).
         k_bounds: Minimum and maximum values for k. Default (0, 0.5).
         k_guess: Initial value for k.
+        solve_callback: TODO
+        iter_callback: TODO
 
     Returns:
         `(k, actual_frequency, eigenvalues, eigenvectors)`
@@ -449,7 +452,7 @@ def find_k(
     def get_f(k0_mag: float, band: int = 0) -> float:
         nonlocal n, v
         k0 = direction * k0_mag                         # type: ignore
-        n, v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu)
+        n, v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu, callback=iter_callback)
         f = numpy.sqrt(numpy.abs(numpy.real(n[band])))
         if solve_callback:
             solve_callback(k0_mag, n, v, f)
@@ -477,6 +480,7 @@ def eigsolve(
         tolerance: float = 1e-20,
         max_iters: int = 10000,
         reset_iters: int = 100,
+        callback: Optional[Callable[[...], None]] = None,
         ) -> Tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector
@@ -491,6 +495,9 @@ def eigsolve(
             Default `None` (1 everywhere).
         tolerance: Solver stops when fractional change in the objective
                    `trace(Z.H @ A @ Z @ inv(Z Z.H))` is smaller than the tolerance
+        max_iters: TODO
+        reset_iters: TODO
+        callback: TODO
 
     Returns:
         `(eigenvalues, eigenvectors)` where `eigenvalues[i]` corresponds to the
@@ -659,6 +666,9 @@ def eigsolve(
         #prev_theta = theta
         prev_E = E
 
+        if callback:
+            callback()
+
     '''
     Recover eigenvectors from Z
     '''

From 4a9198ade77157633791e621897478e1b3572d9b Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 14 Nov 2022 12:39:29 -0800
Subject: [PATCH 279/437] allow setting initial guess

---
 meanas/fdfd/bloch.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index ea2740f..08a9654 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -480,6 +480,7 @@ def eigsolve(
         tolerance: float = 1e-20,
         max_iters: int = 10000,
         reset_iters: int = 100,
+        y0: Optional[ArrayLike] = None,
         callback: Optional[Callable[[...], None]] = None,
         ) -> Tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
@@ -498,6 +499,7 @@ def eigsolve(
         max_iters: TODO
         reset_iters: TODO
         callback: TODO
+        y0: TODO, initial guess
 
     Returns:
         `(eigenvalues, eigenvectors)` where `eigenvalues[i]` corresponds to the
@@ -526,7 +528,6 @@ def eigsolve(
     #prev_theta = 0.5
     D = numpy.zeros(shape=y_shape, dtype=complex)
 
-    y0 = None
     if y0 is None:
         Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape)
     else:

From 8f8c130c2fee766186c99c414409b2957a16c0f6 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 14 Nov 2022 12:39:41 -0800
Subject: [PATCH 280/437] use previous result as next guess

---
 meanas/fdfd/bloch.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 08a9654..994298c 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -452,7 +452,7 @@ def find_k(
     def get_f(k0_mag: float, band: int = 0) -> float:
         nonlocal n, v
         k0 = direction * k0_mag                         # type: ignore
-        n, v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu, callback=iter_callback)
+        n, v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu, y0=v, callback=iter_callback)
         f = numpy.sqrt(numpy.abs(numpy.real(n[band])))
         if solve_callback:
             solve_callback(k0_mag, n, v, f)

From 4144e6dc37670ada93651ffadfeec359bdb167e5 Mon Sep 17 00:00:00 2001
From: jan 
Date: Sat, 19 Nov 2022 19:33:50 -0800
Subject: [PATCH 281/437] update comment

---
 meanas/fdmath/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py
index 40c68e7..f010945 100644
--- a/meanas/fdmath/__init__.py
+++ b/meanas/fdmath/__init__.py
@@ -13,7 +13,7 @@ Discrete fields are stored in one of two forms:
       discrete indices referring to positions on the x, y, and z axes respectively.
     + For a vector field, the first index specifies which vector component is accessed:
       `E[:, m, n, p] = [Ex[m, n, p], Ey[m, n, p], Ez[m, n, p]]`.
-- The `vfdfield_t` form is simply a vectorzied (i.e. 1D) version of the `field_t`,
+- The `vfdfield_t` form is simply a vectorzied (i.e. 1D) version of the `fdfield_t`,
     as obtained by `meanas.fdmath.vectorization.vec` (effectively just `numpy.ravel`)
 
 Operators which act on fields also come in two forms:

From 23d5a160c8a3082c82e80ebaa0f4e473069f202f Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 20 Nov 2022 19:55:11 -0800
Subject: [PATCH 282/437] use f-string

---
 examples/fdtd.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/fdtd.py b/examples/fdtd.py
index fac5f81..8dc0d98 100644
--- a/examples/fdtd.py
+++ b/examples/fdtd.py
@@ -61,7 +61,7 @@ def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern:
     xyr[:, 2] *= radius
 
     pat = Pattern()
-    pat.name = 'L3p-a{:g}r{:g}rp{:g}'.format(a, radius, kwargs['perturbed_radius'])
+    pat.name = f'L3p-a{a:g}r{radius:g}rp{kwargs["perturbed_radius"]:g}'
     pat.shapes += [shapes.Circle(radius=r, offset=(x, y),
                                  dose=kwargs['hole_dose'],
                                  layer=kwargs['hole_layer'])

From 68a98183886328dd0d3f3777c5fa3b511b2c85f2 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 20 Nov 2022 19:55:51 -0800
Subject: [PATCH 283/437] use moveaxis instead of deprecated rollaxis

---
 meanas/fdfd/bloch.py | 6 +++---
 meanas/test/utils.py | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 994298c..96c8ea8 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -146,7 +146,7 @@ def generate_kmn(
     Gi = numpy.moveaxis(Gi_grids, 0, -1)
 
     k_G = k0[None, None, None, :] - Gi
-    k_xyz = numpy.rollaxis(G_matrix @ numpy.rollaxis(k_G, 3, 2), 3, 2)
+    k_xyz = numpy.moveaxis(G_matrix @ numpy.moveaxis(k_G, 3, 2), 3, 2)
 
     m = numpy.broadcast_to([0, 1, 0], tuple(shape[:3]) + (3,)).astype(float)
     n = numpy.broadcast_to([0, 0, 1], tuple(shape[:3]) + (3,)).astype(float)
@@ -283,7 +283,7 @@ def hmn_2_exyz(
                - m * hin_n) * k_mag
 
         # divide by epsilon
-        return numpy.array([ei for ei in numpy.rollaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3)])         # TODO avoid copy
+        return numpy.array([ei for ei in numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0)])         # TODO avoid copy
 
     return operator
 
@@ -319,7 +319,7 @@ def hmn_2_hxyz(
         hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
         h_xyz = (m * hin_m
                + n * hin_n)
-        return numpy.array([ifftn(hi) for hi in numpy.rollaxis(h_xyz, 3)])
+        return numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)])
 
     return operator
 
diff --git a/meanas/test/utils.py b/meanas/test/utils.py
index cc7e839..81246e3 100644
--- a/meanas/test/utils.py
+++ b/meanas/test/utils.py
@@ -15,8 +15,8 @@ def assert_fields_close(
         ) -> None:
     numpy.testing.assert_allclose(
         x, y, verbose=False,
-        err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(x, -1),
-                                                       numpy.rollaxis(y, -1)),
+        err_msg='Fields did not match:\n{}\n{}'.format(numpy.moveaxis(x, -1, 0),
+                                                       numpy.moveaxis(y, -1, 0)),
         *args,
         **kwargs,
         )

From bec0137c992504c1b59a14063d58e79d50ed5059 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 20 Nov 2022 19:56:45 -0800
Subject: [PATCH 284/437] update type hints and formatting

---
 meanas/fdfd/bloch.py        | 10 +++++-----
 meanas/fdfd/farfield.py     |  6 +++---
 meanas/fdfd/functional.py   |  4 ++--
 meanas/fdfd/solvers.py      |  6 +++---
 meanas/fdfd/waveguide_2d.py |  2 +-
 meanas/fdtd/base.py         | 12 +++++++++---
 6 files changed, 23 insertions(+), 17 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 96c8ea8..c4c4b60 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -413,8 +413,8 @@ def find_k(
         band: int = 0,
         k_bounds: Tuple[float, float] = (0, 0.5),
         k_guess: Optional[float] = None,
-        solve_callback: Optional[Callable[[...], None]] = None,
-        iter_callback: Optional[Callable[[...], None]] = None,
+        solve_callback: Optional[Callable[..., None]] = None,
+        iter_callback: Optional[Callable[..., None]] = None,
         ) -> Tuple[float, float, NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Search for a bloch vector that has a given frequency.
@@ -440,7 +440,7 @@ def find_k(
     """
     direction = numpy.array(direction) / norm(direction)
 
-    k_bounds = tuple(sorted(k_bounds))
+    k_bounds = tuple(sorted(k_bounds))    # type: ignore    # we know the length already...
     assert len(k_bounds) == 2
 
     if k_guess is None:
@@ -481,7 +481,7 @@ def eigsolve(
         max_iters: int = 10000,
         reset_iters: int = 100,
         y0: Optional[ArrayLike] = None,
-        callback: Optional[Callable[[...], None]] = None,
+        callback: Optional[Callable[..., None]] = None,
         ) -> Tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector
@@ -531,7 +531,7 @@ def eigsolve(
     if y0 is None:
         Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape)
     else:
-        Z = y0
+        Z = numpy.array(y0, copy=False)
 
     while True:
         Z *= num_modes / norm(Z)
diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py
index bd02eb9..07c026d 100644
--- a/meanas/fdfd/farfield.py
+++ b/meanas/fdfd/farfield.py
@@ -1,7 +1,7 @@
 """
 Functions for performing near-to-farfield transformation (and the reverse).
 """
-from typing import Dict, List, Any
+from typing import Dict, List, Any, Union, Sequence
 import numpy
 from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift
 from numpy import pi
@@ -14,7 +14,7 @@ def near_to_farfield(
         H_near: cfdfield_t,
         dx: float,
         dy: float,
-        padded_size: List[int] = None
+        padded_size: Union[List[int], int, None] = None
         ) -> Dict[str, Any]:
     """
     Compute the farfield, i.e. the distribution of the fields after propagation
@@ -126,7 +126,7 @@ def far_to_nearfield(
         H_far: cfdfield_t,
         dkx: float,
         dky: float,
-        padded_size: List[int] = None
+        padded_size: Union[List[int], int, None] = None
         ) -> Dict[str, Any]:
     """
     Compute the farfield, i.e. the distribution of the fields after propagation
diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py
index cfc6187..3af38b2 100644
--- a/meanas/fdfd/functional.py
+++ b/meanas/fdfd/functional.py
@@ -19,7 +19,7 @@ def e_full(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: fdfield_t,
-        mu: Optional[fdfield_t] = None
+        mu: Optional[fdfield_t] = None,
         ) -> cfdfield_updater_t:
     """
     Wave operator for use with E-field. See `operators.e_full` for details.
@@ -55,7 +55,7 @@ def eh_full(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: fdfield_t,
-        mu: fdfield_t = None
+        mu: Optional[fdfield_t] = None,
         ) -> Callable[[cfdfield_t, cfdfield_t], Tuple[cfdfield_t, cfdfield_t]]:
     """
     Wave operator for full (both E and H) field representation.
diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py
index 6ced908..19cb418 100644
--- a/meanas/fdfd/solvers.py
+++ b/meanas/fdfd/solvers.py
@@ -67,9 +67,9 @@ def generic(
         dxes: dx_lists_t,
         J: vcfdfield_t,
         epsilon: vfdfield_t,
-        mu: vfdfield_t = None,
-        pec: vfdfield_t = None,
-        pmc: vfdfield_t = None,
+        mu: Optional[vfdfield_t] = None,
+        pec: Optional[vfdfield_t] = None,
+        pmc: Optional[vfdfield_t] = None,
         adjoint: bool = False,
         matrix_solver: Callable[..., ArrayLike] = _scipy_qmr,
         matrix_solver_opts: Optional[Dict[str, Any]] = None,
diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 3ac95d5..e9aed43 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -722,7 +722,7 @@ def solve_modes(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: vfdfield_t = None,
+        mu: Optional[vfdfield_t] = None,
         mode_margin: int = 2,
         ) -> Tuple[NDArray[numpy.float64], NDArray[numpy.complex128]]:
     """
diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py
index 753eb71..1c43652 100644
--- a/meanas/fdtd/base.py
+++ b/meanas/fdtd/base.py
@@ -3,7 +3,7 @@ Basic FDTD field updates
 
 
 """
-from typing import Union
+from typing import Union, Optional
 
 from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t
 from ..fdmath.functional import curl_forward, curl_back
@@ -12,7 +12,10 @@ from ..fdmath.functional import curl_forward, curl_back
 __author__ = 'Jan Petykiewicz'
 
 
-def maxwell_e(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t:
+def maxwell_e(
+        dt: float,
+        dxes: Optional[dx_lists_t] = None,
+        ) -> fdfield_updater_t:
     """
     Build a function which performs a portion the time-domain E-field update,
 
@@ -64,7 +67,10 @@ def maxwell_e(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t:
     return me_fun
 
 
-def maxwell_h(dt: float, dxes: dx_lists_t = None) -> fdfield_updater_t:
+def maxwell_h(
+        dt: float,
+        dxes: Optional[dx_lists_t] = None,
+        ) -> fdfield_updater_t:
     """
     Build a function which performs part of the time-domain H-field update,
 

From 03c15c84865939a4a11daa1f8ecbbca804fa4cc9 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 22 Nov 2022 14:44:43 -0800
Subject: [PATCH 285/437] store ZtAZ instead of AZU

U is small (~number of modes)^2
---
 meanas/fdfd/bloch.py | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index c4c4b60..4f6d90c 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -550,15 +550,16 @@ def eigsolve(
         break
 
     for i in range(max_iters):
-        ZtZ = Z.conj().T @ Z
+        Zt = Z.conj().T
+        ZtZ = Zt @ Z
         U = numpy.linalg.inv(ZtZ)
         AZ = scipy_op @ Z
-        AZU = AZ @ U
-        ZtAZU = Z.conj().T @ AZU
+        ZtAZ = Zt @ AZ
+        ZtAZU = ZtAZ @ U
         E_signed = real(trace(ZtAZU))
         sgn = numpy.sign(E_signed)
         E = numpy.abs(E_signed)
-        G = (AZU - Z @ U @ ZtAZU) * sgn
+        G = (AZ @ U - Z @ U @ ZtAZU) * sgn
 
         if i > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7):
             logger.info('Optimization succeded: {} - 5e-8 < {} * {} / 2'.format(abs(E - prev_E), tolerance, E + prev_E))
@@ -577,14 +578,12 @@ def eigsolve(
         d_scale = num_modes / norm(D)
         D *= d_scale
 
-        ZtAZ = Z.conj().T @ AZ
-
         AD = scipy_op @ D
         DtD = D.conj().T @ D
         DtAD = D.conj().T @ AD
 
-        symZtD = _symmetrize(Z.conj().T @ D)
-        symZtAD = _symmetrize(Z.conj().T @ AD)
+        symZtD = _symmetrize(Zt @ D)
+        symZtAD = _symmetrize(Zt @ AD)
 
         Qi_memo: List[Optional[float]] = [None, None]
 
@@ -673,7 +672,7 @@ def eigsolve(
     '''
     Recover eigenvectors from Z
     '''
-    U = numpy.linalg.inv(Z.conj().T @ Z)
+    U = numpy.linalg.inv(ZtZ)
     Y = Z @ scipy.linalg.sqrtm(U)
     W = Y.conj().T @ (scipy_op @ Y)
 

From a64afcbe4d02b983a445378ecf4cb6856b2ae79a Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 22 Nov 2022 14:44:54 -0800
Subject: [PATCH 286/437] Add more info to log message

---
 meanas/fdfd/bloch.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 4f6d90c..cb6056c 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -562,7 +562,10 @@ def eigsolve(
         G = (AZ @ U - Z @ U @ ZtAZU) * sgn
 
         if i > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7):
-            logger.info('Optimization succeded: {} - 5e-8 < {} * {} / 2'.format(abs(E - prev_E), tolerance, E + prev_E))
+            logger.info('Optimization succeded: '
+                f'[change in trace] {abs(E - prev_E)} - 5e-8 '
+                f'< {tolerance} [tolerance] * {(E + prev_E) / 2} [value of trace]'
+                )
             break
 
         KG = scipy_iop @ G

From dfbb845bee56475d282100b600c7b149d8b6d3fc Mon Sep 17 00:00:00 2001
From: jan 
Date: Tue, 22 Nov 2022 22:21:31 -0800
Subject: [PATCH 287/437] add some comments

---
 meanas/fdfd/bloch.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index cb6056c..848cd3d 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -559,7 +559,8 @@ def eigsolve(
         E_signed = real(trace(ZtAZU))
         sgn = numpy.sign(E_signed)
         E = numpy.abs(E_signed)
-        G = (AZ @ U - Z @ U @ ZtAZU) * sgn
+        G = (AZ @ U - Z @ U @ ZtAZU) * sgn     # G = AZU projected onto the space orthonormal to Z
+                                               #  via (1 - ZUZt)
 
         if i > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7):
             logger.info('Optimization succeded: '
@@ -568,8 +569,8 @@ def eigsolve(
                 )
             break
 
-        KG = scipy_iop @ G
-        traceGtKG = _rtrace_AtB(G, KG)
+        KG = scipy_iop @ G          # Preconditioned steepest descent direction
+        traceGtKG = _rtrace_AtB(G, KG)      #
 
         if prev_traceGtKG == 0 or i % reset_iters == 0:
             logger.info('CG reset')

From ff395277b0517f57780e7c7c8566e3f4ac5e4679 Mon Sep 17 00:00:00 2001
From: jan 
Date: Tue, 22 Nov 2022 22:21:51 -0800
Subject: [PATCH 288/437] add another comment about minmization

---
 meanas/fdfd/bloch.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 848cd3d..7640354 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -582,6 +582,13 @@ def eigsolve(
         d_scale = num_modes / norm(D)
         D *= d_scale
 
+        # Now know the direction (D), but need to find how far to go (alpha)
+        # We are still minimizing E = tr((Z + alpha D)t A (Z + alpha D) U')
+        #         = tr(ZtAZU' + alpha DtAZU' + alpha ZtADU' + alpha**2 DtADU')
+
+        # where U' = inv((Z + alpha D)t (Z + alpha D))
+        #          = inv(ZtZ + alpha ZtD + alpha DtZ + alpha**2 DtD)
+
         AD = scipy_op @ D
         DtD = D.conj().T @ D
         DtAD = D.conj().T @ AD

From b7bd825bceb598da8813c2e9ab22cdfe291a5d27 Mon Sep 17 00:00:00 2001
From: jan 
Date: Tue, 22 Nov 2022 22:23:00 -0800
Subject: [PATCH 289/437] make saving previous GtKG direction more obvious

---
 meanas/fdfd/bloch.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 7640354..6344275 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -577,6 +577,7 @@ def eigsolve(
             gamma = 0.0
         else:
             gamma = traceGtKG / prev_traceGtKG
+        prev_traceGtKG = traceGtKG
 
         D = gamma / d_scale * D + KG
         d_scale = num_modes / norm(D)
@@ -673,7 +674,6 @@ def eigsolve(
         Z *= numpy.cos(theta)
         Z += D * numpy.sin(theta)
 
-        prev_traceGtKG = traceGtKG
         #prev_theta = theta
         prev_E = E
 

From 697770ce9765555274233eca97ff2b7890df307c Mon Sep 17 00:00:00 2001
From: jan 
Date: Thu, 24 Nov 2022 23:16:25 -0800
Subject: [PATCH 290/437] improve top level bloch comment

---
 meanas/fdfd/bloch.py | 28 +++++++++++++++++++++-------
 1 file changed, 21 insertions(+), 7 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 6344275..7749c8c 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -5,7 +5,7 @@ This module contains functions for generating and solving the
  3D Bloch eigenproblem. The approach is to transform the problem
  into the (spatial) fourier domain, transforming the equation
 
-    1/mu * curl(1/eps * curl(H)) = (w/c)^2 H
+    1/mu * curl(1/eps * curl(H_eigenmode)) = (w/c)^2 H_eigenmode
 
  into
 
@@ -20,29 +20,43 @@ This module contains functions for generating and solving the
 
  Since `k` and `H` are orthogonal for each plane wave, we can use each
  `k` to create an orthogonal basis (k, m, n), with `k x m = n`, and
- `|m| = |n| = 1`. The cross products are then simplified with
+ `|m| = |n| = 1`. The cross products are then simplified as follows:
+
+  - `h` is shorthand for `H_k`
+  - `(...)_xyz` denotes the `(x, y, z)` basis
+  - `(...)_kmn` denotes the `(k, m, n)` basis
+  - `hm` is the component of `h` in the `m` direction, etc.
+
+  We know
 
     k @ h = kx hx + ky hy + kz hz = 0 = hk
     h = hk + hm + hn = hm + hn
     k = kk + km + kn = kk = |k|
 
+  We can write
+
     k x h = (ky hz - kz hy,
              kz hx - kx hz,
-             kx hy - ky hx)
+             kx hy - ky hx)_xyz
           = ((k x h) @ k, (k x h) @ m, (k x h) @ n)_kmn
           = (0, (m x k) @ h, (n x k) @ h)_kmn         # triple product ordering
           = (0, kk (-n @ h), kk (m @ h))_kmn          # (m x k) = -|k| n, etc.
           = |k| (0, -h @ n, h @ m)_kmn
 
+  which gives us a straightforward way to perform the cross product
+  while simultaneously transforming into the `_kmn` basis.
+  We can also write
+
     k x h = (km hn - kn hm,
              kn hk - kk hn,
              kk hm - km hk)_kmn
           = (0, -kk hn, kk hm)_kmn
-          = (-kk hn)(mx, my, mz) + (kk hm)(nx, ny, nz)
-          = |k| (hm * (nx, ny, nz) - hn * (mx, my, mz))
+          = (-kk hn)(mx, my, mz)_xyz + (kk hm)(nx, ny, nz)_xyz
+          = |k| (hm * (nx, ny, nz)_xyz
+               - hn * (mx, my, mz)_xyz)
 
- where `h` is shorthand for `H_k`, `(...)_kmn` deontes the `(k, m, n)` basis,
- and e.g. `hm` is the component of `h` in the `m` direction.
+  which gives us a way to perform the cross product while simultaneously
+  trasnforming back into the `_xyz` basis.
 
  We can also simplify `conv(X_k, Y_k)` as `fftn(X * ifftn(Y_k))`.
 

From 09aa9761c6ff824146229e7f5e2705b048d64166 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 27 Nov 2022 12:36:53 -0800
Subject: [PATCH 291/437] Use same variable names as in code

---
 meanas/fdfd/bloch.py | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 7749c8c..ce4a41b 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -600,9 +600,17 @@ def eigsolve(
         # Now know the direction (D), but need to find how far to go (alpha)
         # We are still minimizing E = tr((Z + alpha D)t A (Z + alpha D) U')
         #         = tr(ZtAZU' + alpha DtAZU' + alpha ZtADU' + alpha**2 DtADU')
+        #         = tr((ZtAZ + 2 alpha sym(DtAZ) + alpha**2 DtAD) U')
+        #         = tr(R U')
+        #         = tr(R Qi) = tr(R inv(Q))
 
-        # where U' = inv((Z + alpha D)t (Z + alpha D))
-        #          = inv(ZtZ + alpha ZtD + alpha DtZ + alpha**2 DtD)
+        # where
+        #   R = ZtAZ + 2 alpha sym(DtAZ) + alpha**2 DtAD
+        #
+        #   Q = (Z + alpha D)t (Z + alpha D)
+        #     = inv(ZtZ + alpha ZtD + alpha DtZ + alpha**2 DtD)
+        #
+        #   Qi = inv(Q) = U'
 
         AD = scipy_op @ D
         DtD = D.conj().T @ D

From 7cf90fe0de14d0181e5bdb4b54d2773712897a98 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 22 May 2023 09:52:52 -0700
Subject: [PATCH 292/437] use keepdims instead of readding dims

---
 meanas/fdfd/bloch.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index ce4a41b..5d62145 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -241,8 +241,8 @@ def maxwell_operator(
         e_xyz = fftn(ifftn(d_xyz, axes=range(3)) / epsilon, axes=range(3))
 
         # cross product and transform into mn basis
-        b_m = numpy.sum(e_xyz * n, axis=3)[:, :, :, None] * -k_mag
-        b_n = numpy.sum(e_xyz * m, axis=3)[:, :, :, None] * +k_mag
+        b_m = numpy.sum(e_xyz * n, axis=3, keepdims=True) * -k_mag
+        b_n = numpy.sum(e_xyz * m, axis=3, keepdims=True) * +k_mag
 
         if mu is None:
             h_m, h_n = b_m, b_n
@@ -409,8 +409,8 @@ def inverse_maxwell_operator_approx(
         d_xyz = fftn(ifftn(e_xyz, axes=range(3)) * epsilon, axes=range(3))
 
         # cross product and transform into mn basis   crossinv_t2c
-        h_m = numpy.sum(d_xyz * n, axis=3)[:, :, :, None] / +k_mag
-        h_n = numpy.sum(d_xyz * m, axis=3)[:, :, :, None] / -k_mag
+        h_m = numpy.sum(d_xyz * n, axis=3, keepdims=True) / +k_mag
+        h_n = numpy.sum(d_xyz * m, axis=3, keepdims=True) / -k_mag
 
         return numpy.hstack((h_m.ravel(), h_n.ravel()))
 

From fd1a83b5b9c792279f262109fe13ee71b129c51d Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 22 May 2023 09:53:30 -0700
Subject: [PATCH 293/437] cleaner way to ravel

---
 meanas/fdfd/bloch.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 5d62145..8b99930 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -257,7 +257,9 @@ def maxwell_operator(
             # transform back to mn
             h_m = numpy.sum(h_xyz * m, axis=3)
             h_n = numpy.sum(h_xyz * n, axis=3)
-        return numpy.hstack((h_m.ravel(), h_n.ravel()))
+
+        h = numpy.concatenate((h_m, h_n), axis=None, out=h)     # ravel and merge
+        return h
 
     return operator
 
@@ -412,7 +414,8 @@ def inverse_maxwell_operator_approx(
         h_m = numpy.sum(d_xyz * n, axis=3, keepdims=True) / +k_mag
         h_n = numpy.sum(d_xyz * m, axis=3, keepdims=True) / -k_mag
 
-        return numpy.hstack((h_m.ravel(), h_n.ravel()))
+        h = numpy.concatenate((h_m, h_n), axis=None, out=h)
+        return h
 
     return operator
 

From 5c7deedb70a20902989e3db9c43a3d1e98d6fb70 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 22 May 2023 09:53:56 -0700
Subject: [PATCH 294/437] do more in-place

---
 meanas/fdfd/bloch.py | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 8b99930..edbba75 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -225,9 +225,11 @@ def maxwell_operator(
 
         Args:
             h: Raveled h_mn; size `2 * epsilon[0].size`.
+                Altered in-place.
 
         Returns:
-            Raveled conv(1/mu_k, ik x conv(1/eps_k, ik x h_mn)).
+            Raveled conv(1/mu_k, ik x conv(1/eps_k, ik x h_mn)), returned
+            and overwritten in-place of `h`.
         """
         hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
 
@@ -238,7 +240,9 @@ def maxwell_operator(
                - m * hin_n) * k_mag
 
         # divide by epsilon
-        e_xyz = fftn(ifftn(d_xyz, axes=range(3)) / epsilon, axes=range(3))
+        temp = ifftn(d_xyz, axes=range(3))   # reuses d_xyz if using pyfftw
+        temp /= epsilon
+        e_xyz = fftn(temp, axes=range(3))
 
         # cross product and transform into mn basis
         b_m = numpy.sum(e_xyz * n, axis=3, keepdims=True) * -k_mag
@@ -252,7 +256,9 @@ def maxwell_operator(
                    + n * b_n[:, :, :, None])
 
             # divide by mu
-            h_xyz = fftn(ifftn(b_xyz, axes=range(3)) / mu, axes=range(3))
+            temp = ifftn(b_xyz, axes=range(3))
+            temp /= mu
+            h_xyz = fftn(temp, axes=range(3))
 
             # transform back to mn
             h_m = numpy.sum(h_xyz * m, axis=3)
@@ -397,7 +403,9 @@ def inverse_maxwell_operator_approx(
                    + n * hin_n[:, :, :, None])
 
             # multiply by mu
-            b_xyz = fftn(ifftn(h_xyz, axes=range(3)) * mu, axes=range(3))
+            temp = ifftn(h_xyz, axes=range(3))
+            temp *= mu
+            b_xyz = fftn(temp, axes=range(3))
 
             # transform back to mn
             b_m = numpy.sum(b_xyz * m, axis=3)
@@ -408,7 +416,9 @@ def inverse_maxwell_operator_approx(
                - m * b_n) / k_mag
 
         # multiply by epsilon
-        d_xyz = fftn(ifftn(e_xyz, axes=range(3)) * epsilon, axes=range(3))
+        temp = ifftn(e_xyz, axes=range(3))
+        temp *= epsilon
+        d_xyz = fftn(temp, axes=range(3))
 
         # cross product and transform into mn basis   crossinv_t2c
         h_m = numpy.sum(d_xyz * n, axis=3, keepdims=True) / +k_mag

From 3bf56c16c125f55a08f3572c7e13a229f1f4133a Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 22 May 2023 09:55:47 -0700
Subject: [PATCH 295/437] More in-place ops

Z.copy() is needed since op is in-place now
---
 meanas/fdfd/bloch.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index edbba75..b0b1cb9 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -576,11 +576,14 @@ def eigsolve(
             continue
         break
 
+    Zt = numpy.empty(Z.shape[::-1])
+    AZ = numpy.empty(Z.shape)
+
     for i in range(max_iters):
-        Zt = Z.conj().T
+        Zt = numpy.conj(Z.T, out=Zt)
         ZtZ = Zt @ Z
         U = numpy.linalg.inv(ZtZ)
-        AZ = scipy_op @ Z
+        AZ = scipy_op @ Z.copy()
         ZtAZ = Zt @ AZ
         ZtAZU = ZtAZ @ U
         E_signed = real(trace(ZtAZU))

From 7009e505e7cfbf02d531598d2a9d46774755a692 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 22 May 2023 10:52:55 -0700
Subject: [PATCH 296/437] fix accidental creation of array(None)

---
 meanas/fdfd/waveguide_2d.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index e9aed43..95c0316 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -747,7 +747,8 @@ def solve_modes(
     Solve for the largest-magnitude eigenvalue of the real operator
     '''
     dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
-    A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu))
+    mu_real = None if mu is None else numpy.real(mu)
+    A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), mu_real)
 
     eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
     e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)]

From 44465f1bc9b64102f2ea81d70223aafbea664b10 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 22 May 2023 10:53:13 -0700
Subject: [PATCH 297/437] modernize type annotations

---
 meanas/eigensolvers.py         | 26 +++++++--------
 meanas/fdfd/bloch.py           | 59 ++++++++++++++++++----------------
 meanas/fdfd/farfield.py        | 36 +++++++++++----------
 meanas/fdfd/functional.py      | 18 +++++------
 meanas/fdfd/operators.py       | 31 +++++++++---------
 meanas/fdfd/scpml.py           | 16 ++++-----
 meanas/fdfd/waveguide_2d.py    | 42 ++++++++++++------------
 meanas/fdfd/waveguide_3d.py    | 14 ++++----
 meanas/fdfd/waveguide_cyl.py   |  7 ++--
 meanas/fdmath/__init__.py      |  2 +-
 meanas/fdmath/functional.py    | 22 ++++++-------
 meanas/fdmath/operators.py     |  6 ++--
 meanas/fdmath/vectorization.py |  8 ++---
 meanas/fdtd/base.py            | 10 +++---
 meanas/fdtd/boundaries.py      | 10 +++---
 meanas/fdtd/energy.py          | 49 ++++++++++++++--------------
 meanas/fdtd/pml.py             | 35 +++++++++-----------
 meanas/test/conftest.py        | 12 +++----
 meanas/test/test_fdfd.py       | 26 +++++++--------
 meanas/test/test_fdfd_pml.py   | 26 +++++++--------
 meanas/test/test_fdtd.py       | 34 ++++++++++----------
 meanas/test/utils.py           |  4 +--
 22 files changed, 245 insertions(+), 248 deletions(-)

diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py
index 6b3fba2..ac64f5c 100644
--- a/meanas/eigensolvers.py
+++ b/meanas/eigensolvers.py
@@ -1,7 +1,7 @@
 """
 Solvers for eigenvalue / eigenvector problems
 """
-from typing import Tuple, Callable, Optional, Union
+from typing import Callable
 import numpy
 from numpy.typing import NDArray, ArrayLike
 from numpy.linalg import norm
@@ -11,9 +11,9 @@ import scipy.sparse.linalg as spalg   # type: ignore
 
 def power_iteration(
         operator: sparse.spmatrix,
-        guess_vector: Optional[NDArray[numpy.complex128]] = None,
+        guess_vector: NDArray[numpy.complex128] | None = None,
         iterations: int = 20,
-        ) -> Tuple[complex, NDArray[numpy.complex128]]:
+        ) -> tuple[complex, NDArray[numpy.complex128]]:
     """
     Use power iteration to estimate the dominant eigenvector of a matrix.
 
@@ -40,12 +40,12 @@ def power_iteration(
 
 
 def rayleigh_quotient_iteration(
-        operator: Union[sparse.spmatrix, spalg.LinearOperator],
+        operator: sparse.spmatrix | spalg.LinearOperator,
         guess_vector: NDArray[numpy.complex128],
         iterations: int = 40,
         tolerance: float = 1e-13,
-        solver: Optional[Callable[..., NDArray[numpy.complex128]]] = None,
-        ) -> Tuple[complex, NDArray[numpy.complex128]]:
+        solver: Callable[..., NDArray[numpy.complex128]] | None = None,
+        ) -> tuple[complex, NDArray[numpy.complex128]]:
     """
     Use Rayleigh quotient iteration to refine an eigenvector guess.
 
@@ -73,14 +73,14 @@ def rayleigh_quotient_iteration(
     except TypeError:
         def shift(eigval: float) -> spalg.LinearOperator:
             return spalg.LinearOperator(
-                    shape=operator.shape,
-                    dtype=operator.dtype,
-                    matvec=lambda v: eigval * v,
-                    )
+                shape=operator.shape,
+                dtype=operator.dtype,
+                matvec=lambda v: eigval * v,
+                )
         if solver is None:
             def solver(A: spalg.LinearOperator, b: ArrayLike) -> NDArray[numpy.complex128]:
                 return spalg.bicgstab(A, b)[0]
-    assert(solver is not None)
+    assert solver is not None
 
     v = numpy.squeeze(guess_vector)
     v /= norm(v)
@@ -96,10 +96,10 @@ def rayleigh_quotient_iteration(
 
 
 def signed_eigensolve(
-        operator: Union[sparse.spmatrix, spalg.LinearOperator],
+        operator: sparse.spmatrix | spalg.LinearOperator,
         how_many: int,
         negative: bool = False,
-        ) -> Tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
+        ) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Find the largest-magnitude positive-only (or negative-only) eigenvalues and
      eigenvectors of the provided matrix.
diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index b0b1cb9..720fef4 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -94,7 +94,7 @@ This module contains functions for generating and solving the
 
 '''
 
-from typing import Tuple, Callable, Any, List, Optional, cast, Union, Sequence
+from typing import Callable, Any, cast, Sequence
 import logging
 import numpy
 from numpy import pi, real, trace
@@ -140,7 +140,7 @@ def generate_kmn(
         k0: ArrayLike,
         G_matrix: ArrayLike,
         shape: Sequence[int],
-        ) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
+        ) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
     """
     Generate a (k, m, n) orthogonal basis for each k-vector in the simulation grid.
 
@@ -155,8 +155,9 @@ def generate_kmn(
             All are given in the xyz basis (e.g. `|k|[0,0,0] = norm(G_matrix @ k0)`).
     """
     k0 = numpy.array(k0)
+    G_matrix = numpy.array(G_matrix, copy=False)
 
-    Gi_grids = numpy.meshgrid(*(fftfreq(n, 1 / n) for n in shape[:3]), indexing='ij')
+    Gi_grids = numpy.array(numpy.meshgrid(*(fftfreq(n, 1 / n) for n in shape[:3]), indexing='ij'))
     Gi = numpy.moveaxis(Gi_grids, 0, -1)
 
     k_G = k0[None, None, None, :] - Gi
@@ -183,7 +184,7 @@ def maxwell_operator(
         k0: ArrayLike,
         G_matrix: ArrayLike,
         epsilon: fdfield_t,
-        mu: Optional[fdfield_t] = None
+        mu: fdfield_t | None = None
         ) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
     """
     Generate the Maxwell operator
@@ -237,7 +238,7 @@ def maxwell_operator(
 
         # cross product and transform into xyz basis
         d_xyz = (n * hin_m
-               - m * hin_n) * k_mag
+               - m * hin_n) * k_mag         # noqa: E128
 
         # divide by epsilon
         temp = ifftn(d_xyz, axes=range(3))   # reuses d_xyz if using pyfftw
@@ -253,7 +254,7 @@ def maxwell_operator(
         else:
             # transform from mn to xyz
             b_xyz = (m * b_m[:, :, :, None]
-                   + n * b_n[:, :, :, None])
+                   + n * b_n[:, :, :, None])    # noqa: E128
 
             # divide by mu
             temp = ifftn(b_xyz, axes=range(3))
@@ -302,7 +303,7 @@ def hmn_2_exyz(
     def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
         hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
         d_xyz = (n * hin_m
-               - m * hin_n) * k_mag
+               - m * hin_n) * k_mag         # noqa: E128
 
         # divide by epsilon
         return numpy.array([ei for ei in numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0)])         # TODO avoid copy
@@ -340,7 +341,7 @@ def hmn_2_hxyz(
     def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
         hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
         h_xyz = (m * hin_m
-               + n * hin_n)
+               + n * hin_n)     # noqa: E128
         return numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)])
 
     return operator
@@ -350,7 +351,7 @@ def inverse_maxwell_operator_approx(
         k0: ArrayLike,
         G_matrix: ArrayLike,
         epsilon: fdfield_t,
-        mu: Optional[fdfield_t] = None,
+        mu: fdfield_t | None = None,
         ) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
     """
     Generate an approximate inverse of the Maxwell operator,
@@ -400,7 +401,7 @@ def inverse_maxwell_operator_approx(
         else:
             # transform from mn to xyz
             h_xyz = (m * hin_m[:, :, :, None]
-                   + n * hin_n[:, :, :, None])
+                   + n * hin_n[:, :, :, None])  # noqa: E128
 
             # multiply by mu
             temp = ifftn(h_xyz, axes=range(3))
@@ -413,7 +414,7 @@ def inverse_maxwell_operator_approx(
 
         # cross product and transform into xyz basis
         e_xyz = (n * b_m
-               - m * b_n) / k_mag
+               - m * b_n) / k_mag  # noqa: E128
 
         # multiply by epsilon
         temp = ifftn(e_xyz, axes=range(3))
@@ -436,13 +437,13 @@ def find_k(
         direction: ArrayLike,
         G_matrix: ArrayLike,
         epsilon: fdfield_t,
-        mu: Optional[fdfield_t] = None,
+        mu: fdfield_t | None = None,
         band: int = 0,
-        k_bounds: Tuple[float, float] = (0, 0.5),
-        k_guess: Optional[float] = None,
-        solve_callback: Optional[Callable[..., None]] = None,
-        iter_callback: Optional[Callable[..., None]] = None,
-        ) -> Tuple[float, float, NDArray[numpy.complex128], NDArray[numpy.complex128]]:
+        k_bounds: tuple[float, float] = (0, 0.5),
+        k_guess: float | None = None,
+        solve_callback: Callable[..., None] | None = None,
+        iter_callback: Callable[..., None] | None = None,
+        ) -> tuple[float, float, NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Search for a bloch vector that has a given frequency.
 
@@ -503,13 +504,13 @@ def eigsolve(
         k0: ArrayLike,
         G_matrix: ArrayLike,
         epsilon: fdfield_t,
-        mu: Optional[fdfield_t] = None,
+        mu: fdfield_t | None = None,
         tolerance: float = 1e-20,
         max_iters: int = 10000,
         reset_iters: int = 100,
-        y0: Optional[ArrayLike] = None,
-        callback: Optional[Callable[..., None]] = None,
-        ) -> Tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
+        y0: ArrayLike | None = None,
+        callback: Callable[..., None] | None = None,
+        ) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector
      k0 of the specified structure.
@@ -555,6 +556,7 @@ def eigsolve(
     #prev_theta = 0.5
     D = numpy.zeros(shape=y_shape, dtype=complex)
 
+    Z: NDArray[numpy.complex128]
     if y0 is None:
         Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape)
     else:
@@ -589,11 +591,12 @@ def eigsolve(
         E_signed = real(trace(ZtAZU))
         sgn = numpy.sign(E_signed)
         E = numpy.abs(E_signed)
-        G = (AZ @ U - Z @ U @ ZtAZU) * sgn     # G = AZU projected onto the space orthonormal to Z
-                                               #  via (1 - ZUZt)
+        G = (AZ @ U - Z @ U @ ZtAZU) * sgn      # G = AZU projected onto the space orthonormal to Z
+                                                #  via (1 - ZUZt)
 
         if i > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7):
-            logger.info('Optimization succeded: '
+            logger.info(
+                'Optimization succeded: '
                 f'[change in trace] {abs(E - prev_E)} - 5e-8 '
                 f'< {tolerance} [tolerance] * {(E + prev_E) / 2} [value of trace]'
                 )
@@ -635,7 +638,7 @@ def eigsolve(
         symZtD = _symmetrize(Zt @ D)
         symZtAD = _symmetrize(Zt @ AD)
 
-        Qi_memo: List[Optional[float]] = [None, None]
+        Qi_memo: list[float | None] = [None, None]
 
         def Qi_func(theta: float) -> float:
             nonlocal Qi_memo
@@ -659,8 +662,8 @@ def eigsolve(
                 else:
                     raise Exception('Inexplicable singularity in trace_func')
             Qi_memo[0] = theta
-            Qi_memo[1] = Qi
-            return Qi
+            Qi_memo[1] = cast(float, Qi)
+            return cast(float, Qi)
 
         def trace_func(theta: float) -> float:
             c = numpy.cos(theta)
@@ -772,7 +775,7 @@ def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_to
 
 def _rtrace_AtB(
         A: NDArray[numpy.complex128],
-        B: Union[NDArray[numpy.complex128], float],
+        B: NDArray[numpy.complex128] | float,
         ) -> float:
     return real(numpy.sum(A.conj() * B))
 
diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py
index 07c026d..5c1caf0 100644
--- a/meanas/fdfd/farfield.py
+++ b/meanas/fdfd/farfield.py
@@ -1,7 +1,7 @@
 """
 Functions for performing near-to-farfield transformation (and the reverse).
 """
-from typing import Dict, List, Any, Union, Sequence
+from typing import Any, Sequence, cast
 import numpy
 from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift
 from numpy import pi
@@ -14,8 +14,8 @@ def near_to_farfield(
         H_near: cfdfield_t,
         dx: float,
         dy: float,
-        padded_size: Union[List[int], int, None] = None
-        ) -> Dict[str, Any]:
+        padded_size: list[int] | int | None = None
+        ) -> dict[str, Any]:
     """
     Compute the farfield, i.e. the distribution of the fields after propagation
       through several wavelengths of uniform medium.
@@ -62,14 +62,15 @@ def near_to_farfield(
         padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int)
     if not hasattr(padded_size, '__len__'):
         padded_size = (padded_size, padded_size)            # type: ignore  # checked if sequence
+    padded_shape = cast(Sequence[int], padded_size)
 
-    En_fft = [fftshift(fft2(fftshift(Eni), s=padded_size)) for Eni in E_near]
-    Hn_fft = [fftshift(fft2(fftshift(Hni), s=padded_size)) for Hni in H_near]
+    En_fft = [fftshift(fft2(fftshift(Eni), s=padded_shape)) for Eni in E_near]
+    Hn_fft = [fftshift(fft2(fftshift(Hni), s=padded_shape)) for Hni in H_near]
 
     # Propagation vectors kx, ky
     k  = 2 * pi
-    kxx = 2 * pi * fftshift(fftfreq(padded_size[0], dx))
-    kyy = 2 * pi * fftshift(fftfreq(padded_size[1], dy))
+    kxx = 2 * pi * fftshift(fftfreq(padded_shape[0], dx))
+    kyy = 2 * pi * fftshift(fftfreq(padded_shape[1], dy))
 
     kx, ky = numpy.meshgrid(kxx, kyy, indexing='ij')
     kxy2 = kx * kx + ky * ky
@@ -85,14 +86,14 @@ def near_to_farfield(
 
     # Normalized vector potentials N, L
     N = [-Hn_fft[1] * cos_phi * cos_th + Hn_fft[0] * cos_phi * sin_th,
-          Hn_fft[1] * sin_th + Hn_fft[0] * cos_th]
+          Hn_fft[1] * sin_th + Hn_fft[0] * cos_th]                      # noqa: E127
     L = [ En_fft[1] * cos_phi * cos_th - En_fft[0] * cos_phi * sin_th,
-         -En_fft[1] * sin_th - En_fft[0] * cos_th]
+         -En_fft[1] * sin_th - En_fft[0] * cos_th]                      # noqa: E128
 
     E_far = [-L[1] - N[0],
-              L[0] - N[1]]
+              L[0] - N[1]]      # noqa: E127
     H_far = [-E_far[1],
-              E_far[0]]
+              E_far[0]]         # noqa: E127
 
     theta = numpy.arctan2(ky, kx)
     phi = numpy.arccos(cos_phi)
@@ -126,8 +127,8 @@ def far_to_nearfield(
         H_far: cfdfield_t,
         dkx: float,
         dky: float,
-        padded_size: Union[List[int], int, None] = None
-        ) -> Dict[str, Any]:
+        padded_size: list[int] | int | None = None
+        ) -> dict[str, Any]:
     """
     Compute the farfield, i.e. the distribution of the fields after propagation
       through several wavelengths of uniform medium.
@@ -170,6 +171,7 @@ def far_to_nearfield(
         padded_size = (2 ** numpy.ceil(numpy.log2(s))).astype(int)
     if not hasattr(padded_size, '__len__'):
         padded_size = (padded_size, padded_size)            # type: ignore  # checked if sequence
+    padded_shape = cast(Sequence[int], padded_size)
 
     k = 2 * pi
     kxs = fftshift(fftfreq(s[0], 1 / (s[0] * dkx)))
@@ -203,9 +205,9 @@ def far_to_nearfield(
 
     # Normalized vector potentials N, L
     L = [0.5 * E_far[1],
-        -0.5 * E_far[0]]
+        -0.5 * E_far[0]]    # noqa: E128
     N = [L[1],
-        -L[0]]
+        -L[0]]  # noqa: E128
 
     En_fft = [-( L[0] * sin_th + L[1] * cos_phi * cos_th) / cos_phi,
               -(-L[0] * cos_th + L[1] * cos_phi * sin_th) / cos_phi]
@@ -217,8 +219,8 @@ def far_to_nearfield(
         En_fft[i][cos_phi == 0] = 0
         Hn_fft[i][cos_phi == 0] = 0
 
-    E_near = [ifftshift(ifft2(ifftshift(Ei), s=padded_size)) for Ei in En_fft]
-    H_near = [ifftshift(ifft2(ifftshift(Hi), s=padded_size)) for Hi in Hn_fft]
+    E_near = [ifftshift(ifft2(ifftshift(Ei), s=padded_shape)) for Ei in En_fft]
+    H_near = [ifftshift(ifft2(ifftshift(Hi), s=padded_shape)) for Hi in Hn_fft]
 
     dx = 2 * pi / (s[0] * dkx)
     dy = 2 * pi / (s[0] * dky)
diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py
index 3af38b2..745a536 100644
--- a/meanas/fdfd/functional.py
+++ b/meanas/fdfd/functional.py
@@ -5,7 +5,7 @@ Functional versions of many FDFD operators. These can be useful for performing
 The functions generated here expect `cfdfield_t` inputs with shape (3, X, Y, Z),
 e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z)
 """
-from typing import Callable, Tuple, Optional
+from typing import Callable
 import numpy
 
 from ..fdmath import dx_lists_t, fdfield_t, cfdfield_t, cfdfield_updater_t
@@ -19,7 +19,7 @@ def e_full(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: fdfield_t,
-        mu: Optional[fdfield_t] = None,
+        mu: fdfield_t | None = None,
         ) -> cfdfield_updater_t:
     """
     Wave operator for use with E-field. See `operators.e_full` for details.
@@ -55,8 +55,8 @@ def eh_full(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: fdfield_t,
-        mu: Optional[fdfield_t] = None,
-        ) -> Callable[[cfdfield_t, cfdfield_t], Tuple[cfdfield_t, cfdfield_t]]:
+        mu: fdfield_t | None = None,
+        ) -> Callable[[cfdfield_t, cfdfield_t], tuple[cfdfield_t, cfdfield_t]]:
     """
     Wave operator for full (both E and H) field representation.
     See `operators.eh_full`.
@@ -74,11 +74,11 @@ def eh_full(
     ch = curl_back(dxes[1])
     ce = curl_forward(dxes[0])
 
-    def op_1(e: cfdfield_t, h: cfdfield_t) -> Tuple[cfdfield_t, cfdfield_t]:
+    def op_1(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]:
         return (ch(h) - 1j * omega * epsilon * e,
                 ce(e) + 1j * omega * h)
 
-    def op_mu(e: cfdfield_t, h: cfdfield_t) -> Tuple[cfdfield_t, cfdfield_t]:
+    def op_mu(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]:
         return (ch(h) - 1j * omega * epsilon * e,
                 ce(e) + 1j * omega * mu * h)            # type: ignore   # mu=None ok
 
@@ -91,7 +91,7 @@ def eh_full(
 def e2h(
         omega: complex,
         dxes: dx_lists_t,
-        mu: Optional[fdfield_t] = None,
+        mu: fdfield_t | None = None,
         ) -> cfdfield_updater_t:
     """
     Utility operator for converting the `E` field into the `H` field.
@@ -123,7 +123,7 @@ def e2h(
 def m2j(
         omega: complex,
         dxes: dx_lists_t,
-        mu: Optional[fdfield_t] = None,
+        mu: fdfield_t | None = None,
         ) -> cfdfield_updater_t:
     """
     Utility operator for converting magnetic current `M` distribution
@@ -160,7 +160,7 @@ def e_tfsf_source(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: fdfield_t,
-        mu: Optional[fdfield_t] = None,
+        mu: fdfield_t | None = None,
         ) -> cfdfield_updater_t:
     """
     Operator that turns an E-field distribution into a total-field/scattered-field
diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py
index 86d9fc8..f7c1dc7 100644
--- a/meanas/fdfd/operators.py
+++ b/meanas/fdfd/operators.py
@@ -27,7 +27,6 @@ The following operators are included:
 - Cross product matrices
 """
 
-from typing import Tuple, Optional
 import numpy
 import scipy.sparse as sparse       # type: ignore
 
@@ -42,9 +41,9 @@ def e_full(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
-        pec: Optional[vfdfield_t] = None,
-        pmc: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
+        pec: vfdfield_t | None = None,
+        pmc: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
     """
     Wave operator
@@ -99,7 +98,7 @@ def e_full(
 
 def e_full_preconditioners(
         dxes: dx_lists_t,
-        ) -> Tuple[sparse.spmatrix, sparse.spmatrix]:
+        ) -> tuple[sparse.spmatrix, sparse.spmatrix]:
     """
     Left and right preconditioners `(Pl, Pr)` for symmetrizing the `e_full` wave operator.
 
@@ -128,9 +127,9 @@ def h_full(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
-        pec: Optional[vfdfield_t] = None,
-        pmc: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
+        pec: vfdfield_t | None = None,
+        pmc: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
     """
     Wave operator
@@ -185,9 +184,9 @@ def eh_full(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
-        pec: Optional[vfdfield_t] = None,
-        pmc: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
+        pec: vfdfield_t | None = None,
+        pmc: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
     """
     Wave operator for `[E, H]` field representation. This operator implements Maxwell's
@@ -254,8 +253,8 @@ def eh_full(
 def e2h(
         omega: complex,
         dxes: dx_lists_t,
-        mu: Optional[vfdfield_t] = None,
-        pmc: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
+        pmc: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
     """
     Utility operator for converting the E field into the H field.
@@ -286,7 +285,7 @@ def e2h(
 def m2j(
         omega: complex,
         dxes: dx_lists_t,
-        mu: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
     """
     Operator for converting a magnetic current M into an electric current J.
@@ -368,7 +367,7 @@ def e_tfsf_source(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
     """
     Operator that turns a desired E-field distribution into a
@@ -399,7 +398,7 @@ def e_boundary_source(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
         periodic_mask_edges: bool = False,
         ) -> sparse.spmatrix:
     """
diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py
index de38854..bc056e1 100644
--- a/meanas/fdfd/scpml.py
+++ b/meanas/fdfd/scpml.py
@@ -2,10 +2,10 @@
 Functions for creating stretched coordinate perfectly matched layer (PML) absorbers.
 """
 
-from typing import Sequence, Union, Callable, Optional, List
+from typing import Sequence, Callable
 
 import numpy
-from numpy.typing import ArrayLike, NDArray
+from numpy.typing import NDArray
 
 
 __author__ = 'Jan Petykiewicz'
@@ -43,8 +43,8 @@ def uniform_grid_scpml(
         thicknesses: Sequence[int],
         omega: float,
         epsilon_effective: float = 1.0,
-        s_function: Optional[s_function_t] = None,
-        ) -> List[List[NDArray[numpy.float64]]]:
+        s_function: s_function_t | None = None,
+        ) -> list[list[NDArray[numpy.float64]]]:
     """
     Create dx arrays for a uniform grid with a cell width of 1 and a pml.
 
@@ -86,7 +86,7 @@ def uniform_grid_scpml(
     for k, th in enumerate(thicknesses):
         s = shape[k]
         if th > 0:
-            sr = numpy.arange(s)
+            sr = numpy.arange(s, dtype=numpy.float64)
             dx_a[k] = 1 + 1j * s_function(ll(sr,       s, th)) / s_correction
             dx_b[k] = 1 + 1j * s_function(ll(sr + 0.5, s, th)) / s_correction
         else:
@@ -96,14 +96,14 @@ def uniform_grid_scpml(
 
 
 def stretch_with_scpml(
-        dxes: List[List[NDArray[numpy.float64]]],
+        dxes: list[list[NDArray[numpy.float64]]],
         axis: int,
         polarity: int,
         omega: float,
         epsilon_effective: float = 1.0,
         thickness: int = 10,
-        s_function: Optional[s_function_t] = None,
-        ) -> List[List[NDArray[numpy.float64]]]:
+        s_function: s_function_t | None = None,
+        ) -> list[list[NDArray[numpy.float64]]]:
     """
         Stretch dxes to contain a stretched-coordinate PML (SCPML) in one direction along one axis.
 
diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 95c0316..dce3573 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -178,7 +178,7 @@ to account for numerical dispersion if the result is introduced into a space wit
 """
 # TODO update module docs
 
-from typing import List, Tuple, Optional, Any
+from typing import Any
 import numpy
 from numpy.typing import NDArray, ArrayLike
 from numpy.linalg import norm
@@ -196,7 +196,7 @@ def operator_e(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
     """
     Waveguide operator of the form
@@ -263,7 +263,7 @@ def operator_h(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
     """
     Waveguide operator of the form
@@ -333,9 +333,9 @@ def normalized_fields_e(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
         prop_phase: float = 0,
-        ) -> Tuple[vcfdfield_t, vcfdfield_t]:
+        ) -> tuple[vcfdfield_t, vcfdfield_t]:
     """
     Given a vector `e_xy` containing the vectorized E_x and E_y fields,
      returns normalized, vectorized E and H fields for the system.
@@ -368,9 +368,9 @@ def normalized_fields_h(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
         prop_phase: float = 0,
-        ) -> Tuple[vcfdfield_t, vcfdfield_t]:
+        ) -> tuple[vcfdfield_t, vcfdfield_t]:
     """
     Given a vector `h_xy` containing the vectorized H_x and H_y fields,
      returns normalized, vectorized E and H fields for the system.
@@ -403,9 +403,9 @@ def _normalized_fields(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
         prop_phase: float = 0,
-        ) -> Tuple[vcfdfield_t, vcfdfield_t]:
+        ) -> tuple[vcfdfield_t, vcfdfield_t]:
     # TODO documentation
     shape = [s.size for s in dxes[0]]
     dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
@@ -445,7 +445,7 @@ def exy2h(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None
+        mu: vfdfield_t | None = None
         ) -> sparse.spmatrix:
     """
     Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
@@ -471,7 +471,7 @@ def hxy2e(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None
+        mu: vfdfield_t | None = None
         ) -> sparse.spmatrix:
     """
     Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
@@ -495,7 +495,7 @@ def hxy2e(
 def hxy2h(
         wavenumber: complex,
         dxes: dx_lists_t,
-        mu: Optional[vfdfield_t] = None
+        mu: vfdfield_t | None = None
         ) -> sparse.spmatrix:
     """
     Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
@@ -564,7 +564,7 @@ def e2h(
         wavenumber: complex,
         omega: complex,
         dxes: dx_lists_t,
-        mu: Optional[vfdfield_t] = None
+        mu: vfdfield_t | None = None
         ) -> sparse.spmatrix:
     """
     Returns an operator which, when applied to a vectorized E eigenfield, produces
@@ -654,7 +654,7 @@ def h_err(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None
+        mu: vfdfield_t | None = None
         ) -> float:
     """
     Calculates the relative error in the H field
@@ -680,7 +680,7 @@ def h_err(
     else:
         op = ce @ eps_inv @ ch @ h - omega ** 2 * (mu * h)
 
-    return norm(op) / norm(h)
+    return float(norm(op) / norm(h))
 
 
 def e_err(
@@ -689,7 +689,7 @@ def e_err(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
         ) -> float:
     """
     Calculates the relative error in the E field
@@ -714,17 +714,17 @@ def e_err(
         mu_inv = sparse.diags(1 / mu)
         op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e)
 
-    return norm(op) / norm(e)
+    return float(norm(op) / norm(e))
 
 
 def solve_modes(
-        mode_numbers: List[int],
+        mode_numbers: list[int],
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
         mode_margin: int = 2,
-        ) -> Tuple[NDArray[numpy.float64], NDArray[numpy.complex128]]:
+        ) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Given a 2D region, attempts to solve for the eigenmode with the specified mode numbers.
 
@@ -772,7 +772,7 @@ def solve_mode(
         mode_number: int,
         *args: Any,
         **kwargs: Any,
-        ) -> Tuple[vcfdfield_t, complex]:
+        ) -> tuple[vcfdfield_t, complex]:
     """
     Wrapper around `solve_modes()` that solves for a single mode.
 
diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py
index 9051123..7f994d3 100644
--- a/meanas/fdfd/waveguide_3d.py
+++ b/meanas/fdfd/waveguide_3d.py
@@ -4,7 +4,7 @@ Tools for working with waveguide modes in 3D domains.
 This module relies heavily on `waveguide_2d` and mostly just transforms
 its parameters into 2D equivalents and expands the results back into 3D.
 """
-from typing import Dict, Optional, Sequence, Union, Any
+from typing import Sequence, Any
 import numpy
 from numpy.typing import NDArray
 
@@ -20,8 +20,8 @@ def solve_mode(
         polarity: int,
         slices: Sequence[slice],
         epsilon: fdfield_t,
-        mu: Optional[fdfield_t] = None,
-        ) -> Dict[str, Union[complex, NDArray[numpy.float_]]]:
+        mu: fdfield_t | None = None,
+        ) -> dict[str, complex | NDArray[numpy.float_]]:
     """
     Given a 3D grid, selects a slice from the grid and attempts to
      solve for an eigenmode propagating through that slice.
@@ -40,8 +40,8 @@ def solve_mode(
     Returns:
         ```
         {
-            'E': List[NDArray[numpy.float_]],
-            'H': List[NDArray[numpy.float_]],
+            'E': list[NDArray[numpy.float_]],
+            'H': list[NDArray[numpy.float_]],
             'wavenumber': complex,
         }
         ```
@@ -63,7 +63,7 @@ def solve_mode(
     dx_prop = 0.5 * dxab_forward.sum()
 
     # Reduce to 2D and solve the 2D problem
-    args_2d: Dict[str, Any] = {
+    args_2d: dict[str, Any] = {
         'omega': omega,
         'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes],
         'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]),
@@ -114,7 +114,7 @@ def compute_source(
         polarity: int,
         slices: Sequence[slice],
         epsilon: fdfield_t,
-        mu: Optional[fdfield_t] = None,
+        mu: fdfield_t | None = None,
         ) -> cfdfield_t:
     """
     Given an eigenmode obtained by `solve_mode`, returns the current source distribution
diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 2286d4c..6b3a160 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -8,7 +8,6 @@ As the z-dependence is known, all the functions in this file assume a 2D grid
 """
 # TODO update module docs
 
-from typing import Dict, Union
 import numpy
 import scipy.sparse as sparse       # type: ignore
 
@@ -85,7 +84,7 @@ def solve_mode(
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
         r0: float,
-        ) -> Dict[str, Union[complex, cfdfield_t]]:
+        ) -> dict[str, complex | cfdfield_t]:
     """
     TODO: fixup
     Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
@@ -103,8 +102,8 @@ def solve_mode(
     Returns:
         ```
         {
-            'E': List[NDArray[numpy.complex_]],
-            'H': List[NDArray[numpy.complex_]],
+            'E': list[NDArray[numpy.complex_]],
+            'H': list[NDArray[numpy.complex_]],
             'wavenumber': complex,
         }
         ```
diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py
index f010945..cd62fcd 100644
--- a/meanas/fdmath/__init__.py
+++ b/meanas/fdmath/__init__.py
@@ -8,7 +8,7 @@ Fields, Functions, and Operators
 
 Discrete fields are stored in one of two forms:
 
-- The `fdfield_t` form is a multidimensional `numpy.ndarray`
+- The `fdfield_t` form is a multidimensional `numpy.NDArray`
     + For a scalar field, this is just `U[m, n, p]`, where `m`, `n`, and `p` are
       discrete indices referring to positions on the x, y, and z axes respectively.
     + For a vector field, the first index specifies which vector component is accessed:
diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py
index 27cd44e..e33aa93 100644
--- a/meanas/fdmath/functional.py
+++ b/meanas/fdmath/functional.py
@@ -3,7 +3,7 @@ Math functions for finite difference simulations
 
 Basic discrete calculus etc.
 """
-from typing import Sequence, Tuple, Optional, Callable
+from typing import Sequence, Callable
 
 import numpy
 from numpy.typing import NDArray
@@ -12,8 +12,8 @@ from .types import fdfield_t, fdfield_updater_t
 
 
 def deriv_forward(
-        dx_e: Optional[Sequence[NDArray[numpy.float_]]] = None,
-        ) -> Tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
+        dx_e: Sequence[NDArray[numpy.float_]] | None = None,
+        ) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
     """
     Utility operators for taking discretized derivatives (backward variant).
 
@@ -36,8 +36,8 @@ def deriv_forward(
 
 
 def deriv_back(
-        dx_h: Optional[Sequence[NDArray[numpy.float_]]] = None,
-        ) -> Tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
+        dx_h: Sequence[NDArray[numpy.float_]] | None = None,
+        ) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
     """
     Utility operators for taking discretized derivatives (forward variant).
 
@@ -60,7 +60,7 @@ def deriv_back(
 
 
 def curl_forward(
-        dx_e: Optional[Sequence[NDArray[numpy.float_]]] = None,
+        dx_e: Sequence[NDArray[numpy.float_]] | None = None,
         ) -> fdfield_updater_t:
     """
     Curl operator for use with the E field.
@@ -89,7 +89,7 @@ def curl_forward(
 
 
 def curl_back(
-        dx_h: Optional[Sequence[NDArray[numpy.float_]]] = None,
+        dx_h: Sequence[NDArray[numpy.float_]] | None = None,
         ) -> fdfield_updater_t:
     """
     Create a function which takes the backward curl of a field.
@@ -118,11 +118,11 @@ def curl_back(
 
 
 def curl_forward_parts(
-        dx_e: Optional[Sequence[NDArray[numpy.float_]]] = None,
+        dx_e: Sequence[NDArray[numpy.float_]] | None = None,
         ) -> Callable:
     Dx, Dy, Dz = deriv_forward(dx_e)
 
-    def mkparts_fwd(e: fdfield_t) -> Tuple[Tuple[fdfield_t, fdfield_t], ...]:
+    def mkparts_fwd(e: fdfield_t) -> tuple[tuple[fdfield_t, fdfield_t], ...]:
         return ((-Dz(e[1]),  Dy(e[2])),
                 ( Dz(e[0]), -Dx(e[2])),
                 (-Dy(e[0]),  Dx(e[1])))
@@ -131,11 +131,11 @@ def curl_forward_parts(
 
 
 def curl_back_parts(
-        dx_h: Optional[Sequence[NDArray[numpy.float_]]] = None,
+        dx_h: Sequence[NDArray[numpy.float_]] | None = None,
         ) -> Callable:
     Dx, Dy, Dz = deriv_back(dx_h)
 
-    def mkparts_back(h: fdfield_t) -> Tuple[Tuple[fdfield_t, fdfield_t], ...]:
+    def mkparts_back(h: fdfield_t) -> tuple[tuple[fdfield_t, fdfield_t], ...]:
         return ((-Dz(h[1]),  Dy(h[2])),
                 ( Dz(h[0]), -Dx(h[2])),
                 (-Dy(h[0]),  Dx(h[1])))
diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py
index d90261a..95101c5 100644
--- a/meanas/fdmath/operators.py
+++ b/meanas/fdmath/operators.py
@@ -3,7 +3,7 @@ Matrix operators for finite difference simulations
 
 Basic discrete calculus etc.
 """
-from typing import Sequence, List
+from typing import Sequence
 import numpy
 from numpy.typing import NDArray
 import scipy.sparse as sparse   # type: ignore
@@ -98,7 +98,7 @@ def shift_with_mirror(
 
 def deriv_forward(
         dx_e: Sequence[NDArray[numpy.float_]],
-        ) -> List[sparse.spmatrix]:
+        ) -> list[sparse.spmatrix]:
     """
     Utility operators for taking discretized derivatives (forward variant).
 
@@ -125,7 +125,7 @@ def deriv_forward(
 
 def deriv_back(
         dx_h: Sequence[NDArray[numpy.float_]],
-        ) -> List[sparse.spmatrix]:
+        ) -> list[sparse.spmatrix]:
     """
     Utility operators for taking discretized derivatives (backward variant).
 
diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py
index 5d9e932..0a9f8ad 100644
--- a/meanas/fdmath/vectorization.py
+++ b/meanas/fdmath/vectorization.py
@@ -4,7 +4,7 @@ and a 1D array representation of that field `[f_x0, f_x1, f_x2,... f_y0,... f_z0
 Vectorized versions of the field use row-major (ie., C-style) ordering.
 """
 
-from typing import Optional, overload, Union, Sequence
+from typing import overload, Sequence
 import numpy
 from numpy.typing import ArrayLike
 
@@ -24,10 +24,10 @@ def vec(f: cfdfield_t) -> vcfdfield_t:
     pass
 
 @overload
-def vec(f: ArrayLike) -> Union[vfdfield_t, vcfdfield_t]:
+def vec(f: ArrayLike) -> vfdfield_t | vcfdfield_t:
     pass
 
-def vec(f: Union[fdfield_t, cfdfield_t, ArrayLike, None]) -> Union[vfdfield_t, vcfdfield_t, None]:
+def vec(f: fdfield_t | cfdfield_t | ArrayLike | None) -> vfdfield_t | vcfdfield_t | None:
     """
     Create a 1D ndarray from a 3D vector field which spans a 1-3D region.
 
@@ -57,7 +57,7 @@ def unvec(v: vfdfield_t, shape: Sequence[int]) -> fdfield_t:
 def unvec(v: vcfdfield_t, shape: Sequence[int]) -> cfdfield_t:
     pass
 
-def unvec(v: Union[vfdfield_t, vcfdfield_t, None], shape: Sequence[int]) -> Union[fdfield_t, cfdfield_t, None]:
+def unvec(v: vfdfield_t | vcfdfield_t | None, shape: Sequence[int]) -> fdfield_t | cfdfield_t | None:
     """
     Perform the inverse of vec(): take a 1D ndarray and output a 3D field
      of form `[f_x, f_y, f_z]` where each of `f_*` is a len(shape)-dimensional
diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py
index 1c43652..3891e28 100644
--- a/meanas/fdtd/base.py
+++ b/meanas/fdtd/base.py
@@ -3,8 +3,6 @@ Basic FDTD field updates
 
 
 """
-from typing import Union, Optional
-
 from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t
 from ..fdmath.functional import curl_forward, curl_back
 
@@ -14,7 +12,7 @@ __author__ = 'Jan Petykiewicz'
 
 def maxwell_e(
         dt: float,
-        dxes: Optional[dx_lists_t] = None,
+        dxes: dx_lists_t | None = None,
         ) -> fdfield_updater_t:
     """
     Build a function which performs a portion the time-domain E-field update,
@@ -49,7 +47,7 @@ def maxwell_e(
     else:
         curl_h_fun = curl_back()
 
-    def me_fun(e: fdfield_t, h: fdfield_t, epsilon: Union[fdfield_t, float]) -> fdfield_t:
+    def me_fun(e: fdfield_t, h: fdfield_t, epsilon: fdfield_t | float) -> fdfield_t:
         """
         Update the E-field.
 
@@ -69,7 +67,7 @@ def maxwell_e(
 
 def maxwell_h(
         dt: float,
-        dxes: Optional[dx_lists_t] = None,
+        dxes: dx_lists_t | None = None,
         ) -> fdfield_updater_t:
     """
     Build a function which performs part of the time-domain H-field update,
@@ -105,7 +103,7 @@ def maxwell_h(
     else:
         curl_e_fun = curl_forward()
 
-    def mh_fun(e: fdfield_t, h: fdfield_t, mu: Union[fdfield_t, float, None] = None) -> fdfield_t:
+    def mh_fun(e: fdfield_t, h: fdfield_t, mu: fdfield_t | float | None = None) -> fdfield_t:
         """
         Update the H-field.
 
diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py
index 4171936..652d957 100644
--- a/meanas/fdtd/boundaries.py
+++ b/meanas/fdtd/boundaries.py
@@ -4,7 +4,7 @@ Boundary conditions
 #TODO conducting boundary documentation
 """
 
-from typing import Tuple, Any, List
+from typing import Any
 
 from ..fdmath import fdfield_t, fdfield_updater_t
 
@@ -12,7 +12,7 @@ from ..fdmath import fdfield_t, fdfield_updater_t
 def conducting_boundary(
         direction: int,
         polarity: int
-        ) -> Tuple[fdfield_updater_t, fdfield_updater_t]:
+        ) -> tuple[fdfield_updater_t, fdfield_updater_t]:
     dirs = [0, 1, 2]
     if direction not in dirs:
         raise Exception('Invalid direction: {}'.format(direction))
@@ -20,8 +20,8 @@ def conducting_boundary(
     u, v = dirs
 
     if polarity < 0:
-        boundary_slice = [slice(None)] * 3      # type: List[Any]
-        shifted1_slice = [slice(None)] * 3      # type: List[Any]
+        boundary_slice = [slice(None)] * 3      # type: list[Any]
+        shifted1_slice = [slice(None)] * 3      # type: list[Any]
         boundary_slice[direction] = 0
         shifted1_slice[direction] = 1
 
@@ -42,7 +42,7 @@ def conducting_boundary(
     if polarity > 0:
         boundary_slice = [slice(None)] * 3
         shifted1_slice = [slice(None)] * 3
-        shifted2_slice = [slice(None)] * 3      # type: List[Any]
+        shifted2_slice = [slice(None)] * 3      # type: list[Any]
         boundary_slice[direction] = -1
         shifted1_slice[direction] = -2
         shifted2_slice[direction] = -3
diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py
index ca7d308..75938f3 100644
--- a/meanas/fdtd/energy.py
+++ b/meanas/fdtd/energy.py
@@ -1,4 +1,3 @@
-from typing import Optional, Union
 import numpy
 
 from ..fdmath import dx_lists_t, fdfield_t
@@ -11,7 +10,7 @@ from ..fdmath.functional import deriv_back
 def poynting(
         e: fdfield_t,
         h: fdfield_t,
-        dxes: Optional[dx_lists_t] = None,
+        dxes: dx_lists_t | None = None,
         ) -> fdfield_t:
     """
     Calculate the poynting vector `S` ($S$).
@@ -89,11 +88,11 @@ def poynting(
 
 
 def poynting_divergence(
-        s: Optional[fdfield_t] = None,
+        s: fdfield_t | None = None,
         *,
-        e: Optional[fdfield_t] = None,
-        h: Optional[fdfield_t] = None,
-        dxes: Optional[dx_lists_t] = None,
+        e: fdfield_t | None = None,
+        h: fdfield_t | None = None,
+        dxes: dx_lists_t | None = None,
         ) -> fdfield_t:
     """
     Calculate the divergence of the poynting vector.
@@ -116,9 +115,9 @@ def poynting_divergence(
             energy cell.
     """
     if s is None:
-        assert(e is not None)
-        assert(h is not None)
-        assert(dxes is not None)
+        assert e is not None
+        assert h is not None
+        assert dxes is not None
         s = poynting(e, h, dxes=dxes)
 
     Dx, Dy, Dz = deriv_back()
@@ -130,9 +129,9 @@ def energy_hstep(
         e0: fdfield_t,
         h1: fdfield_t,
         e2: fdfield_t,
-        epsilon: Optional[fdfield_t] = None,
-        mu: Optional[fdfield_t] = None,
-        dxes: Optional[dx_lists_t] = None,
+        epsilon: fdfield_t | None = None,
+        mu: fdfield_t | None = None,
+        dxes: dx_lists_t | None = None,
         ) -> fdfield_t:
     """
     Calculate energy `U` at the time of the provided H-field `h1`.
@@ -158,9 +157,9 @@ def energy_estep(
         h0: fdfield_t,
         e1: fdfield_t,
         h2: fdfield_t,
-        epsilon: Optional[fdfield_t] = None,
-        mu: Optional[fdfield_t] = None,
-        dxes: Optional[dx_lists_t] = None,
+        epsilon: fdfield_t | None = None,
+        mu: fdfield_t | None = None,
+        dxes: dx_lists_t | None = None,
         ) -> fdfield_t:
     """
     Calculate energy `U` at the time of the provided E-field `e1`.
@@ -188,9 +187,9 @@ def delta_energy_h2e(
         h1: fdfield_t,
         e2: fdfield_t,
         h3: fdfield_t,
-        epsilon: Optional[fdfield_t] = None,
-        mu: Optional[fdfield_t] = None,
-        dxes: Optional[dx_lists_t] = None,
+        epsilon: fdfield_t | None = None,
+        mu: fdfield_t | None = None,
+        dxes: dx_lists_t | None = None,
         ) -> fdfield_t:
     """
     Change in energy during the half-step from `h1` to `e2`.
@@ -221,9 +220,9 @@ def delta_energy_e2h(
         e1: fdfield_t,
         h2: fdfield_t,
         e3: fdfield_t,
-        epsilon: Optional[fdfield_t] = None,
-        mu: Optional[fdfield_t] = None,
-        dxes: Optional[dx_lists_t] = None,
+        epsilon: fdfield_t | None = None,
+        mu: fdfield_t | None = None,
+        dxes: dx_lists_t | None = None,
         ) -> fdfield_t:
     """
     Change in energy during the half-step from `e1` to `h2`.
@@ -251,7 +250,7 @@ def delta_energy_e2h(
 def delta_energy_j(
         j0: fdfield_t,
         e1: fdfield_t,
-        dxes: Optional[dx_lists_t] = None,
+        dxes: dx_lists_t | None = None,
         ) -> fdfield_t:
     """
     Calculate
@@ -274,9 +273,9 @@ def delta_energy_j(
 def dxmul(
         ee: fdfield_t,
         hh: fdfield_t,
-        epsilon: Optional[Union[fdfield_t, float]] = None,
-        mu: Optional[Union[fdfield_t, float]] = None,
-        dxes: Optional[dx_lists_t] = None,
+        epsilon: fdfield_t | float | None = None,
+        mu: fdfield_t | float | None = None,
+        dxes: dx_lists_t | None = None,
         ) -> fdfield_t:
     if epsilon is None:
         epsilon = 1
diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py
index 1781485..b11b3b5 100644
--- a/meanas/fdtd/pml.py
+++ b/meanas/fdtd/pml.py
@@ -7,7 +7,7 @@ PML implementations
 """
 # TODO retest pmls!
 
-from typing import List, Callable, Tuple, Dict, Sequence, Any, Optional
+from typing import Callable, Sequence, Any
 from copy import deepcopy
 import numpy
 from numpy.typing import NDArray, DTypeLike
@@ -30,7 +30,7 @@ def cpml_params(
         m: float = 3.5,
         ma: float = 1,
         cfs_alpha: float = 0,
-        ) -> Dict[str, Any]:
+        ) -> dict[str, Any]:
 
     if axis not in range(3):
         raise Exception('Invalid axis: {}'.format(axis))
@@ -59,11 +59,11 @@ def cpml_params(
     else:
         raise Exception('Bad polarity!')
 
-    expand_slice_l: List[Any] = [None, None, None]
+    expand_slice_l: list[Any] = [None, None, None]
     expand_slice_l[axis] = slice(None)
     expand_slice = tuple(expand_slice_l)
 
-    def par(x: NDArray[numpy.float64]) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
+    def par(x: NDArray[numpy.float64]) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
         scaling = (x / thickness) ** m
         sigma = scaling * sigma_max
         kappa = 1 + scaling * (kappa_max - 1)
@@ -93,23 +93,22 @@ def cpml_params(
 
 
 def updates_with_cpml(
-         cpml_params: Sequence[Sequence[Optional[Dict[str, Any]]]],
-         dt: float,
-         dxes: dx_lists_t,
-         epsilon: fdfield_t,
-         *,
-         dtype: DTypeLike = numpy.float32,
-         ) -> Tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None],
-                    Callable[[fdfield_t, fdfield_t, fdfield_t], None]]:
+        cpml_params: Sequence[Sequence[dict[str, Any] | None]],
+        dt: float,
+        dxes: dx_lists_t,
+        epsilon: fdfield_t,
+        *,
+        dtype: DTypeLike = numpy.float32,
+        ) -> tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None],
+                   Callable[[fdfield_t, fdfield_t, fdfield_t], None]]:
 
     Dfx, Dfy, Dfz = deriv_forward(dxes[1])
     Dbx, Dby, Dbz = deriv_back(dxes[1])
 
-
-    psi_E: List[List[Tuple[Any, Any]]] = [[(None, None) for _ in range(2)] for _ in range(3)]
-    psi_H: List[List[Tuple[Any, Any]]] = deepcopy(psi_E)
-    params_E: List[List[Tuple[Any, Any, Any, Any]]] = [[(None, None, None, None) for _ in range(2)] for _ in range(3)]
-    params_H: List[List[Tuple[Any, Any, Any, Any]]] = deepcopy(params_E)
+    psi_E: list[list[tuple[Any, Any]]] = [[(None, None) for _ in range(2)] for _ in range(3)]
+    psi_H: list[list[tuple[Any, Any]]] = deepcopy(psi_E)
+    params_E: list[list[tuple[Any, Any, Any, Any]]] = [[(None, None, None, None) for _ in range(2)] for _ in range(3)]
+    params_H: list[list[tuple[Any, Any, Any, Any]]] = deepcopy(params_E)
 
     for axis in range(3):
         for pp, polarity in enumerate((-1, 1)):
@@ -133,7 +132,6 @@ def updates_with_cpml(
             params_E[axis][pp] = cpml_param['param_e'] + (region,)
             params_H[axis][pp] = cpml_param['param_h'] + (region,)
 
-
     pE = numpy.empty_like(epsilon, dtype=dtype)
     pH = numpy.empty_like(epsilon, dtype=dtype)
 
@@ -183,7 +181,6 @@ def updates_with_cpml(
         e[1] += dt / epsilon[1] * (dzHx - dxHz + pE[1])
         e[2] += dt / epsilon[2] * (dxHy - dyHx + pE[2])
 
-
     def update_H(
             e: fdfield_t,
             h: fdfield_t,
diff --git a/meanas/test/conftest.py b/meanas/test/conftest.py
index ba6a3a8..5dcdbff 100644
--- a/meanas/test/conftest.py
+++ b/meanas/test/conftest.py
@@ -3,9 +3,9 @@
 Test fixtures
 
 """
-from typing import Tuple, Iterable, List, Any
+from typing import Iterable, Any
 import numpy
-from numpy.typing import NDArray, ArrayLike
+from numpy.typing import NDArray
 import pytest       # type: ignore
 
 from .utils import PRNG
@@ -20,7 +20,7 @@ FixtureRequest = Any
                         (5, 5, 5),
                         # (7, 7, 7),
                        ])
-def shape(request: FixtureRequest) -> Iterable[Tuple[int, ...]]:
+def shape(request: FixtureRequest) -> Iterable[tuple[int, ...]]:
     yield (3, *request.param)
 
 
@@ -37,7 +37,7 @@ def epsilon_fg(request: FixtureRequest) -> Iterable[float]:
 @pytest.fixture(scope='module', params=['center', '000', 'random'])
 def epsilon(
         request: FixtureRequest,
-        shape: Tuple[int, ...],
+        shape: tuple[int, ...],
         epsilon_bg: float,
         epsilon_fg: float,
         ) -> Iterable[NDArray[numpy.float64]]:
@@ -76,9 +76,9 @@ def dx(request: FixtureRequest) -> Iterable[float]:
 @pytest.fixture(scope='module', params=['uniform', 'centerbig'])
 def dxes(
         request: FixtureRequest,
-        shape: Tuple[int, ...],
+        shape: tuple[int, ...],
         dx: float,
-        ) -> Iterable[List[List[NDArray[numpy.float64]]]]:
+        ) -> Iterable[list[list[NDArray[numpy.float64]]]]:
     if request.param == 'uniform':
         dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
     elif request.param == 'centerbig':
diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py
index e5a5875..2f7e142 100644
--- a/meanas/test/test_fdfd.py
+++ b/meanas/test/test_fdfd.py
@@ -1,8 +1,8 @@
-from typing import List, Tuple, Iterable, Optional
+from typing import Iterable
 import dataclasses
 import pytest       # type: ignore
 import numpy
-from numpy.typing import NDArray, ArrayLike
+from numpy.typing import NDArray
 #from numpy.testing import assert_allclose, assert_array_equal
 
 from .. import fdfd
@@ -60,12 +60,12 @@ def omega(request: FixtureRequest) -> Iterable[float]:
 
 
 @pytest.fixture(params=[None])
-def pec(request: FixtureRequest) -> Iterable[Optional[NDArray[numpy.float64]]]:
+def pec(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
     yield request.param
 
 
 @pytest.fixture(params=[None])
-def pmc(request: FixtureRequest) -> Iterable[Optional[NDArray[numpy.float64]]]:
+def pmc(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
     yield request.param
 
 
@@ -78,7 +78,7 @@ def pmc(request: FixtureRequest) -> Iterable[Optional[NDArray[numpy.float64]]]:
 @pytest.fixture(params=['diag'])        # 'center'
 def j_distribution(
         request: FixtureRequest,
-        shape: Tuple[int, ...],
+        shape: tuple[int, ...],
         j_mag: float,
         ) -> Iterable[NDArray[numpy.float64]]:
     j = numpy.zeros(shape, dtype=complex)
@@ -95,26 +95,26 @@ def j_distribution(
 
 @dataclasses.dataclass()
 class FDResult:
-    shape: Tuple[int, ...]
-    dxes: List[List[NDArray[numpy.float64]]]
+    shape: tuple[int, ...]
+    dxes: list[list[NDArray[numpy.float64]]]
     epsilon: NDArray[numpy.float64]
     omega: complex
     j: NDArray[numpy.complex128]
     e: NDArray[numpy.complex128]
-    pmc: Optional[NDArray[numpy.float64]]
-    pec: Optional[NDArray[numpy.float64]]
+    pmc: NDArray[numpy.float64] | None
+    pec: NDArray[numpy.float64] | None
 
 
 @pytest.fixture()
 def sim(
         request: FixtureRequest,
-        shape: Tuple[int, ...],
+        shape: tuple[int, ...],
         epsilon: NDArray[numpy.float64],
-        dxes: List[List[NDArray[numpy.float64]]],
+        dxes: list[list[NDArray[numpy.float64]]],
         j_distribution: NDArray[numpy.complex128],
         omega: float,
-        pec: Optional[NDArray[numpy.float64]],
-        pmc: Optional[NDArray[numpy.float64]],
+        pec: NDArray[numpy.float64] | None,
+        pmc: NDArray[numpy.float64] | None,
         ) -> FDResult:
     """
     Build simulation from parts
diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py
index ff6e4c2..d752491 100644
--- a/meanas/test/test_fdfd_pml.py
+++ b/meanas/test/test_fdfd_pml.py
@@ -1,7 +1,7 @@
-from typing import Optional, Tuple, Iterable, List
+from typing import Iterable
 import pytest       # type: ignore
 import numpy
-from numpy.typing import NDArray, ArrayLike
+from numpy.typing import NDArray
 from numpy.testing import assert_allclose
 
 from .. import fdfd
@@ -49,19 +49,19 @@ def omega(request: FixtureRequest) -> Iterable[float]:
 
 
 @pytest.fixture(params=[None])
-def pec(request: FixtureRequest) -> Iterable[Optional[NDArray[numpy.float64]]]:
+def pec(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
     yield request.param
 
 
 @pytest.fixture(params=[None])
-def pmc(request: FixtureRequest) -> Iterable[Optional[NDArray[numpy.float64]]]:
+def pmc(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
     yield request.param
 
 
 @pytest.fixture(params=[(30, 1, 1),
                         (1, 30, 1),
                         (1, 1, 30)])
-def shape(request: FixtureRequest) -> Iterable[Tuple[int, ...]]:
+def shape(request: FixtureRequest) -> Iterable[tuple[int, ...]]:
     yield (3, *request.param)
 
 
@@ -73,7 +73,7 @@ def src_polarity(request: FixtureRequest) -> Iterable[int]:
 @pytest.fixture()
 def j_distribution(
         request: FixtureRequest,
-        shape: Tuple[int, ...],
+        shape: tuple[int, ...],
         epsilon: NDArray[numpy.float64],
         dxes: dx_lists_mut,
         omega: float,
@@ -86,7 +86,7 @@ def j_distribution(
     other_dims.remove(dim)
 
     dx_prop = (dxes[0][dim][shape[dim + 1] // 2]
-             + dxes[1][dim][shape[dim + 1] // 2]) / 2       # TODO is this right for nonuniform dxes?
+             + dxes[1][dim][shape[dim + 1] // 2]) / 2   # noqa: E128   # TODO is this right for nonuniform dxes?
 
     # Mask only contains components orthogonal to propagation direction
     center_mask = numpy.zeros(shape, dtype=bool)
@@ -112,7 +112,7 @@ def j_distribution(
 @pytest.fixture()
 def epsilon(
         request: FixtureRequest,
-        shape: Tuple[int, ...],
+        shape: tuple[int, ...],
         epsilon_bg: float,
         epsilon_fg: float,
         ) -> Iterable[NDArray[numpy.float64]]:
@@ -123,11 +123,11 @@ def epsilon(
 @pytest.fixture(params=['uniform'])
 def dxes(
         request: FixtureRequest,
-        shape: Tuple[int, ...],
+        shape: tuple[int, ...],
         dx: float,
         omega: float,
         epsilon_fg: float,
-        ) -> Iterable[List[List[NDArray[numpy.float64]]]]:
+        ) -> Iterable[list[list[NDArray[numpy.float64]]]]:
     if request.param == 'uniform':
         dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
     dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0]    # Propagation axis
@@ -147,13 +147,13 @@ def dxes(
 @pytest.fixture()
 def sim(
         request: FixtureRequest,
-        shape: Tuple[int, ...],
+        shape: tuple[int, ...],
         epsilon: NDArray[numpy.float64],
         dxes: dx_lists_mut,
         j_distribution: NDArray[numpy.complex128],
         omega: float,
-        pec: Optional[NDArray[numpy.float64]],
-        pmc: Optional[NDArray[numpy.float64]],
+        pec: NDArray[numpy.float64] | None,
+        pmc: NDArray[numpy.float64] | None,
         ) -> FDResult:
     j_vec = vec(j_distribution)
     eps_vec = vec(epsilon)
diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py
index b46a3ca..701275e 100644
--- a/meanas/test/test_fdtd.py
+++ b/meanas/test/test_fdtd.py
@@ -1,8 +1,8 @@
-from typing import List, Tuple, Iterable, Any, Dict
+from typing import Iterable, Any
 import dataclasses
 import pytest       # type: ignore
 import numpy
-from numpy.typing import NDArray, ArrayLike
+from numpy.typing import NDArray
 #from numpy.testing import assert_allclose, assert_array_equal
 
 from .. import fdtd
@@ -33,7 +33,7 @@ def test_initial_energy(sim: 'TDResult') -> None:
 
     dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0)
     u0 = (j0 * j0.conj() / sim.epsilon * dV).sum(axis=0)
-    args: Dict[str, Any] = {
+    args: dict[str, Any] = {
         'dxes': sim.dxes,
         'epsilon': sim.epsilon,
         }
@@ -52,7 +52,7 @@ def test_energy_conservation(sim: 'TDResult') -> None:
     e0 = sim.es[0]
     j0 = sim.js[0]
     u = fdtd.delta_energy_j(j0=j0, e1=e0, dxes=sim.dxes).sum()
-    args: Dict[str, Any] = {
+    args: dict[str, Any] = {
         'dxes': sim.dxes,
         'epsilon': sim.epsilon,
         }
@@ -70,7 +70,7 @@ def test_energy_conservation(sim: 'TDResult') -> None:
 
 
 def test_poynting_divergence(sim: 'TDResult') -> None:
-    args: Dict[str, Any] = {
+    args: dict[str, Any] = {
         'dxes': sim.dxes,
         'epsilon': sim.epsilon,
         }
@@ -103,7 +103,7 @@ def test_poynting_planes(sim: 'TDResult') -> None:
     if mask.sum() > 1:
         pytest.skip('test_poynting_planes can only test single point sources, got {}'.format(mask.sum()))
 
-    args: Dict[str, Any] = {
+    args: dict[str, Any] = {
         'dxes': sim.dxes,
         'epsilon': sim.epsilon,
         }
@@ -156,26 +156,26 @@ def dt(request: FixtureRequest) -> Iterable[float]:
 
 @dataclasses.dataclass()
 class TDResult:
-    shape: Tuple[int, ...]
+    shape: tuple[int, ...]
     dt: float
-    dxes: List[List[NDArray[numpy.float64]]]
+    dxes: list[list[NDArray[numpy.float64]]]
     epsilon: NDArray[numpy.float64]
     j_distribution: NDArray[numpy.float64]
-    j_steps: Tuple[int, ...]
-    es: List[NDArray[numpy.float64]] = dataclasses.field(default_factory=list)
-    hs: List[NDArray[numpy.float64]] = dataclasses.field(default_factory=list)
-    js: List[NDArray[numpy.float64]] = dataclasses.field(default_factory=list)
+    j_steps: tuple[int, ...]
+    es: list[NDArray[numpy.float64]] = dataclasses.field(default_factory=list)
+    hs: list[NDArray[numpy.float64]] = dataclasses.field(default_factory=list)
+    js: list[NDArray[numpy.float64]] = dataclasses.field(default_factory=list)
 
 
 @pytest.fixture(params=[(0, 4, 8)])  # (0,)
-def j_steps(request: FixtureRequest) -> Iterable[Tuple[int, ...]]:
+def j_steps(request: FixtureRequest) -> Iterable[tuple[int, ...]]:
     yield request.param
 
 
 @pytest.fixture(params=['center', 'random'])
 def j_distribution(
         request: FixtureRequest,
-        shape: Tuple[int, ...],
+        shape: tuple[int, ...],
         j_mag: float,
         ) -> Iterable[NDArray[numpy.float64]]:
     j = numpy.zeros(shape)
@@ -191,12 +191,12 @@ def j_distribution(
 @pytest.fixture()
 def sim(
         request: FixtureRequest,
-        shape: Tuple[int, ...],
+        shape: tuple[int, ...],
         epsilon: NDArray[numpy.float64],
-        dxes: List[List[NDArray[numpy.float64]]],
+        dxes: list[list[NDArray[numpy.float64]]],
         dt: float,
         j_distribution: NDArray[numpy.float64],
-        j_steps: Tuple[int, ...],
+        j_steps: tuple[int, ...],
         ) -> TDResult:
     is3d = (numpy.array(shape) == 1).sum() == 0
     if is3d:
diff --git a/meanas/test/utils.py b/meanas/test/utils.py
index 81246e3..00ed3f1 100644
--- a/meanas/test/utils.py
+++ b/meanas/test/utils.py
@@ -1,7 +1,7 @@
 from typing import Any
 
 import numpy
-from numpy.typing import ArrayLike, NDArray
+from numpy.typing import NDArray
 
 
 PRNG = numpy.random.RandomState(12345)
@@ -14,7 +14,7 @@ def assert_fields_close(
         **kwargs: Any,
         ) -> None:
     numpy.testing.assert_allclose(
-        x, y, verbose=False,
+        x, y, verbose=False,            # type: ignore
         err_msg='Fields did not match:\n{}\n{}'.format(numpy.moveaxis(x, -1, 0),
                                                        numpy.moveaxis(y, -1, 0)),
         *args,

From c7e823b0b38a455ba604596f69a1b9e165b87b5f Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 23 May 2023 12:49:55 -0700
Subject: [PATCH 298/437] allow initial value

---
 meanas/fdfd/bloch.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 720fef4..c85b02b 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -443,6 +443,7 @@ def find_k(
         k_guess: float | None = None,
         solve_callback: Callable[..., None] | None = None,
         iter_callback: Callable[..., None] | None = None,
+        v0: NDArray[numpy.complex128] | None = None,
         ) -> tuple[float, float, NDArray[numpy.complex128], NDArray[numpy.complex128]]:
     """
     Search for a bloch vector that has a given frequency.
@@ -475,7 +476,7 @@ def find_k(
         k_guess = sum(k_bounds) / 2
 
     n = None
-    v = None
+    v = v0
 
     def get_f(k0_mag: float, band: int = 0) -> float:
         nonlocal n, v

From 01e7aae41edd462e33d86311e324bb4a0ce507e8 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 23 May 2023 12:50:17 -0700
Subject: [PATCH 299/437] comment updates

---
 meanas/fdfd/bloch.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index c85b02b..1dadea2 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -1,4 +1,4 @@
-'''
+"""
 Bloch eigenmode solver/operators
 
 This module contains functions for generating and solving the
@@ -92,7 +92,7 @@ This module contains functions for generating and solving the
                   epsilon=epsilon,
                   band=0)
 
-'''
+"""
 
 from typing import Callable, Any, cast, Sequence
 import logging
@@ -540,9 +540,9 @@ def eigsolve(
 
     kmag = norm(G_matrix @ k0)
 
-    '''
-    Generate the operators
-    '''
+    #
+    # Generate the operators
+    #
     mop = maxwell_operator(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu)
     imop = inverse_maxwell_operator_approx(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu)
 
@@ -722,9 +722,9 @@ def eigsolve(
         if callback:
             callback()
 
-    '''
-    Recover eigenvectors from Z
-    '''
+    #
+    # Recover eigenvectors from Z
+    #
     U = numpy.linalg.inv(ZtZ)
     Y = Z @ scipy.linalg.sqrtm(U)
     W = Y.conj().T @ (scipy_op @ Y)

From 2a9e482e44cd3b49bdd292ed43e443a89c2612a5 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 23 May 2023 12:51:12 -0700
Subject: [PATCH 300/437] Z is y0 transposed

---
 meanas/fdfd/bloch.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 1dadea2..afada02 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -561,7 +561,7 @@ def eigsolve(
     if y0 is None:
         Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape)
     else:
-        Z = numpy.array(y0, copy=False)
+        Z = numpy.array(y0, copy=False).T
 
     while True:
         Z *= num_modes / norm(Z)

From 98c973743fdc1e5948b18e77b41e8543219a27e1 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 23 May 2023 12:52:17 -0700
Subject: [PATCH 301/437] use `if False` instead of commenting out code

---
 meanas/fdfd/bloch.py | 64 +++++++++++++++++++++++++-------------------
 1 file changed, 37 insertions(+), 27 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index afada02..f92e0c8 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -554,7 +554,7 @@ def eigsolve(
     prev_E = 0.0
     d_scale = 1.0
     prev_traceGtKG = 0.0
-    #prev_theta = 0.5
+    prev_theta = 0.5
     D = numpy.zeros(shape=y_shape, dtype=complex)
 
     Z: NDArray[numpy.complex128]
@@ -674,39 +674,49 @@ def eigsolve(
             trace = _rtrace_AtB(R, Qi)
             return numpy.abs(trace)
 
-        '''
-        def trace_deriv(theta):
-            Qi = Qi_func(theta)
-            c2 = numpy.cos(2 * theta)
-            s2 = numpy.sin(2 * theta)
-            F = -0.5*s2 *  (ZtAZ - DtAD) + c2 * symZtAD
-            trace_deriv = _rtrace_AtB(Qi, F)
+        if False:
+            def trace_deriv(theta):
+                Qi = Qi_func(theta)
+                c2 = numpy.cos(2 * theta)
+                s2 = numpy.sin(2 * theta)
+                F = -0.5*s2 *  (ZtAZ - DtAD) + c2 * symZtAD
+                trace_deriv = _rtrace_AtB(Qi, F)
 
-            G = Qi @ F.conj().T @ Qi.conj().T
-            H = -0.5*s2 * (ZtZ - DtD) + c2 * symZtD
-            trace_deriv -= _rtrace_AtB(G, H)
+                G = Qi @ F.conj().T @ Qi.conj().T
+                H = -0.5*s2 * (ZtZ - DtD) + c2 * symZtD
+                trace_deriv -= _rtrace_AtB(G, H)
 
-            trace_deriv *= 2
-            return trace_deriv * sgn
+                trace_deriv *= 2
+                return trace_deriv * sgn
 
-        U_sZtD = U @ symZtD
+            U_sZtD = U @ symZtD
 
-        dE = 2.0 * (_rtrace_AtB(U, symZtAD) -
-                    _rtrace_AtB(ZtAZU, U_sZtD))
+            dE = 2.0 * (_rtrace_AtB(U, symZtAD) -
+                        _rtrace_AtB(ZtAZU, U_sZtD))
 
-        d2E = 2 * (_rtrace_AtB(U, DtAD) -
-                   _rtrace_AtB(ZtAZU, U @ (DtD - 4 * symZtD @ U_sZtD)) -
-               4 * _rtrace_AtB(U, symZtAD @ U_sZtD))
+            d2E = 2 * (_rtrace_AtB(U, DtAD) -
+                       _rtrace_AtB(ZtAZU, U @ (DtD - 4 * symZtD @ U_sZtD)) -
+                   4 * _rtrace_AtB(U, symZtAD @ U_sZtD))
 
-        # Newton-Raphson to find a root of the first derivative:
-        theta = -dE/d2E
+            # Newton-Raphson to find a root of the first derivative:
+            theta = -dE / d2E
 
-        if d2E < 0 or abs(theta) >= pi:
-            theta = -abs(prev_theta) * numpy.sign(dE)
+            if d2E < 0 or abs(theta) >= pi:
+                theta = -abs(prev_theta) * numpy.sign(dE)
+
+            # theta, new_E, new_dE = linmin(theta, E, dE, 0.1, min(tolerance, 1e-6), 1e-14, 0, -numpy.sign(dE) * K_PI, trace_func)
+            theta, n, _, new_E, _, _new_dE = scipy.optimize.line_search(
+                trace_func,
+                trace_deriv,
+                xk=theta,
+                pk=numpy.ones((1, 1)),
+                gfk=dE,
+                old_fval=E,
+                c1=min(tolerance, 1e-6),
+                c2=0.1,
+                amax=pi,
+                )
 
-        # theta, new_E, new_dE = linmin(theta, E, dE, 0.1, min(tolerance, 1e-6), 1e-14, 0, -numpy.sign(dE) * K_PI, trace_func)
-        theta, n, _, new_E, _, _new_dE = scipy.optimize.line_search(trace_func, trace_deriv, xk=theta, pk=numpy.ones((1,1)), gfk=dE, old_fval=E, c1=min(tolerance, 1e-6), c2=0.1, amax=pi)
-        '''
         result = scipy.optimize.minimize_scalar(trace_func, bounds=(0, pi), tol=tolerance)
         new_E = result.fun
         theta = result.x
@@ -716,7 +726,7 @@ def eigsolve(
         Z *= numpy.cos(theta)
         Z += D * numpy.sin(theta)
 
-        #prev_theta = theta
+        prev_theta = theta
         prev_E = E
 
         if callback:

From 1ec9375359279587fff87f6c203110ebbf4ff3d5 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 23 May 2023 12:52:25 -0700
Subject: [PATCH 302/437] loosen default tolerance

---
 meanas/fdfd/bloch.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index f92e0c8..fccb3e2 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -506,7 +506,7 @@ def eigsolve(
         G_matrix: ArrayLike,
         epsilon: fdfield_t,
         mu: fdfield_t | None = None,
-        tolerance: float = 1e-20,
+        tolerance: float = 1e-7,
         max_iters: int = 10000,
         reset_iters: int = 100,
         y0: ArrayLike | None = None,

From 2c16c3c9ab41c7235a680df007dc03cd753f76b9 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 23 May 2023 12:54:07 -0700
Subject: [PATCH 303/437] Fixup in-place operators

---
 meanas/fdfd/bloch.py | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index fccb3e2..0bfe65a 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -265,7 +265,9 @@ def maxwell_operator(
             h_m = numpy.sum(h_xyz * m, axis=3)
             h_n = numpy.sum(h_xyz * n, axis=3)
 
-        h = numpy.concatenate((h_m, h_n), axis=None, out=h)     # ravel and merge
+        h.shape = (h.size,)
+        h = numpy.concatenate((h_m.ravel(), h_n.ravel()), axis=None, out=h)     # ravel and merge
+        h.shape = (h.size, 1)
         return h
 
     return operator
@@ -425,7 +427,9 @@ def inverse_maxwell_operator_approx(
         h_m = numpy.sum(d_xyz * n, axis=3, keepdims=True) / +k_mag
         h_n = numpy.sum(d_xyz * m, axis=3, keepdims=True) / -k_mag
 
+        h.shape = (h.size,)
         h = numpy.concatenate((h_m, h_n), axis=None, out=h)
+        h.shape = (h.size, 1)
         return h
 
     return operator
@@ -579,8 +583,8 @@ def eigsolve(
             continue
         break
 
-    Zt = numpy.empty(Z.shape[::-1])
-    AZ = numpy.empty(Z.shape)
+    Zt = numpy.empty(Z.shape[::-1], dtype=numpy.complex128)
+    AZ = numpy.empty(Z.shape, dtype=numpy.complex128)
 
     for i in range(max_iters):
         Zt = numpy.conj(Z.T, out=Zt)
@@ -632,7 +636,7 @@ def eigsolve(
         #
         #   Qi = inv(Q) = U'
 
-        AD = scipy_op @ D
+        AD = scipy_op @ D.copy()
         DtD = D.conj().T @ D
         DtAD = D.conj().T @ AD
 
@@ -737,7 +741,7 @@ def eigsolve(
     #
     U = numpy.linalg.inv(ZtZ)
     Y = Z @ scipy.linalg.sqrtm(U)
-    W = Y.conj().T @ (scipy_op @ Y)
+    W = Y.conj().T @ (scipy_op @ Y.copy())
 
     eigvals, W_eigvecs = numpy.linalg.eig(W)
     eigvecs = Y @ W_eigvecs
@@ -746,7 +750,8 @@ def eigsolve(
         v = eigvecs[:, i]
         n = eigvals[i]
         v /= norm(v)
-        eigness = norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v)
+        Av = (scipy_op @ v.copy())[:, 0]
+        eigness = norm(Av - (v.conj() @ Av) * v)
         f = numpy.sqrt(-numpy.real(n))
         df = numpy.sqrt(-numpy.real(n + eigness))
         neff_err = kmag * (1 / df - 1 / f)

From be620f71372c2f7d53509c6a7f873d98138be99a Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 23 May 2023 12:54:18 -0700
Subject: [PATCH 304/437] comment updates

---
 meanas/fdfd/bloch.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 0bfe65a..83c45d8 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -596,8 +596,9 @@ def eigsolve(
         E_signed = real(trace(ZtAZU))
         sgn = numpy.sign(E_signed)
         E = numpy.abs(E_signed)
-        G = (AZ @ U - Z @ U @ ZtAZU) * sgn      # G = AZU projected onto the space orthonormal to Z
-                                                #  via (1 - ZUZt)
+
+        # G = AZU projected onto the space orthonormal to Z via (1 - ZUZt)
+        G = (AZ @ U - Z @ U @ ZtAZU) * sgn
 
         if i > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7):
             logger.info(
@@ -608,7 +609,7 @@ def eigsolve(
             break
 
         KG = scipy_iop @ G          # Preconditioned steepest descent direction
-        traceGtKG = _rtrace_AtB(G, KG)      #
+        traceGtKG = _rtrace_AtB(G, KG)
 
         if prev_traceGtKG == 0 or i % reset_iters == 0:
             logger.info('CG reset')

From d8ec46674d9a0f7fdd4eea9eaf05dcaaf7b76e96 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 23 May 2023 12:56:38 -0700
Subject: [PATCH 305/437] sqrtm increases precision, so cast back to double

---
 meanas/fdfd/bloch.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 83c45d8..9dacdee 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -578,7 +578,7 @@ def eigsolve(
 
         trace_U = real(trace(U))
         if trace_U > 1e8 * num_modes:
-            Z = Z @ scipy.linalg.sqrtm(U).conj().T
+            Z = Z @ scipy.linalg.sqrtm(U).astype(numpy.complex128).conj().T
             prev_traceGtKG = 0
             continue
         break
@@ -741,7 +741,7 @@ def eigsolve(
     # Recover eigenvectors from Z
     #
     U = numpy.linalg.inv(ZtZ)
-    Y = Z @ scipy.linalg.sqrtm(U)
+    Y = Z @ scipy.linalg.sqrtm(U).astype(numpy.complex128)
     W = Y.conj().T @ (scipy_op @ Y.copy())
 
     eigvals, W_eigvecs = numpy.linalg.eig(W)

From b1a5cdcda91fa2476d736b4bf01c12162d98efd8 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 23 May 2023 12:56:48 -0700
Subject: [PATCH 306/437] bloch example updates

---
 examples/bloch.py | 25 ++++++++++++++++---------
 1 file changed, 16 insertions(+), 9 deletions(-)

diff --git a/examples/bloch.py b/examples/bloch.py
index 2624876..8e90368 100644
--- a/examples/bloch.py
+++ b/examples/bloch.py
@@ -20,6 +20,7 @@ def pyfftw_save_wisdom(path):
         pass
 
     path.parent.mkdir(parents=True, exist_ok=True)
+    wisdom = pyfftw.export_wisdom()
     with open(path, 'wb') as f:
         pickle.dump(wisdom, f)
 
@@ -42,11 +43,13 @@ logger.info('Drawing grid...')
 dx = 40
 x_period = 400
 y_period = z_period = 2000
-g = gridlock.Grid([numpy.arange(-x_period/2, x_period/2, dx),
-                   numpy.arange(-1000, 1000, dx),
-                   numpy.arange(-1000, 1000, dx)],
-                  shifts=numpy.array([[0,0,0]]),
-                  periodic=True)
+g = gridlock.Grid([
+    numpy.arange(-x_period/2, x_period/2, dx),
+    numpy.arange(-1000, 1000, dx),
+    numpy.arange(-1000, 1000, dx)],
+    shifts=numpy.array([[0,0,0]]),
+    periodic=True,
+    )
 gdata = g.allocate(1.445**2)
 
 g.draw_cuboid(gdata, [0,0,0], [200e8, 220, 220], foreground=3.47**2)
@@ -74,7 +77,8 @@ pyfftw_load_wisdom(WISDOM_FILEPATH)
 #                    epsilon=epsilon,
 #                    band=0)
 #
-#print("k={}, f={}, 1/f={}, k/f={}".format(k, f, 1/f, norm(reciprocal_lattice @ k) / f ))
+#kf = norm(reciprocal_lattice @ k) / f)
+#print(f'{k=}, {f=}, 1/f={1/f}, k/f={kf}')
 
 logger.info('Finding f at [0.25, 0, 0]')
 for k0x in [.25]:
@@ -82,7 +86,7 @@ for k0x in [.25]:
 
     kmag = norm(reciprocal_lattice @ k0)
     tolerance = (1000/1550) * 1e-4/1.5  # df = f * dn_eff / n
-    logger.info('tolerance {}'.format(tolerance))
+    logger.info(f'tolerance {tolerance}')
 
     n, v = bloch.eigsolve(4, k0, G_matrix=reciprocal_lattice, epsilon=epsilon, tolerance=tolerance**2)
     v2e = bloch.hmn_2_exyz(k0, G_matrix=reciprocal_lattice, epsilon=epsilon)
@@ -96,8 +100,11 @@ for k0x in [.25]:
         g2data[i+3] += numpy.imag(e[i])
 
     f = numpy.sqrt(numpy.real(numpy.abs(n))) # TODO
-    print('k0x = {:3g}\n eigval = {}\n f = {}\n'.format(k0x, n, f))
+    print(f'{k0x=:3g}')
+    print(f'eigval={n}')
+    print(f'{f=}')
     n_eff = norm(reciprocal_lattice @ k0) / f
-    print('kmag/f = n_eff =  {} \n wl = {}\n'.format(n_eff, 1/f ))
+    print(f'kmag/f = n_eff = {n_eff}')
+    print(f'wl={1/f}\n')
 
 pyfftw_save_wisdom(WISDOM_FILEPATH)

From 24da3f673b12ebad30895374bbd71c04f472eb9f Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 23 May 2023 13:07:26 -0700
Subject: [PATCH 307/437] eigness is positive

---
 meanas/fdfd/bloch.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 9dacdee..800a603 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -754,7 +754,7 @@ def eigsolve(
         Av = (scipy_op @ v.copy())[:, 0]
         eigness = norm(Av - (v.conj() @ Av) * v)
         f = numpy.sqrt(-numpy.real(n))
-        df = numpy.sqrt(-numpy.real(n + eigness))
+        df = numpy.sqrt(-numpy.real(n) + eigness)
         neff_err = kmag * (1 / df - 1 / f)
         logger.info(f'eigness {i}: {eigness}\n neff_err: {neff_err}')
 

From 5c3b3532a96b981573e1da0cc5f365210b36118e Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 16 Jan 2024 23:17:05 -0800
Subject: [PATCH 308/437] M should be same timestep as E

---
 meanas/fdtd/__init__.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py
index c1e7106..63be295 100644
--- a/meanas/fdtd/__init__.py
+++ b/meanas/fdtd/__init__.py
@@ -48,12 +48,12 @@ $$
       \\tilde{E}_{l, \\vec{r}} \\cdot \\hat{\\nabla} \\times \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\\\
    &= \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\cdot
             (-\\tilde{\\partial}_t \\mu_{\\vec{r} + \\frac{1}{2}} \\hat{H}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} -
-                \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}}) -
+                \\hat{M}_{l, \\vec{r} + \\frac{1}{2}}) -
       \\tilde{E}_{l, \\vec{r}} \\cdot (\\hat{\\partial}_t \\tilde{\\epsilon}_{\\vec{r}} \\tilde{E}_{l'+\\frac{1}{2}, \\vec{r}} +
                 \\tilde{J}_{l', \\vec{r}}) \\\\
    &= \\hat{H}_{l'} \\cdot (-\\mu / \\Delta_t)(\\hat{H}_{l + \\frac{1}{2}} - \\hat{H}_{l - \\frac{1}{2}}) -
       \\tilde{E}_l \\cdot (\\epsilon / \\Delta_t )(\\tilde{E}_{l'+\\frac{1}{2}} - \\tilde{E}_{l'-\\frac{1}{2}})
-      - \\hat{H}_{l'} \\cdot \\hat{M}_{l-1} - \\tilde{E}_l \\cdot \\tilde{J}_{l'} \\\\
+      - \\hat{H}_{l'} \\cdot \\hat{M}_{l} - \\tilde{E}_l \\cdot \\tilde{J}_{l'} \\\\
   \\end{aligned}
 $$
 

From c3b6fd94a60758b8c283c2959610cae5eb1c359b Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Wed, 17 Jan 2024 22:20:59 -0800
Subject: [PATCH 309/437] should be e dot j.conj()

---
 meanas/test/test_fdfd.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py
index 2f7e142..e2e8e7b 100644
--- a/meanas/test/test_fdfd.py
+++ b/meanas/test/test_fdfd.py
@@ -43,8 +43,12 @@ def test_poynting_planes(sim: 'FDResult') -> None:
               s[1, mask].sum(), -s[1, my].sum(),
               s[2, mask].sum(), -s[2, mz].sum()]
 
-    e_dot_j = sim.e * sim.j * sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :]
-    src_energy = -e_dot_j[:, mask].real / 2
+    e_dot_j = sim.e * sim.j.conj()
+    dv = (sim.dxes[0][0][:, None, None]
+        * sim.dxes[0][1][None, :, None]
+        * sim.dxes[0][2][None, None, :]
+        )
+    src_energy = -(e_dot_j.real * dv)[:, mask] / 2
 
     assert_close(sum(planes), src_energy.sum())
 

From 107c0fcd7e65c61230b628a21cd62c3c77c81fd9 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Wed, 17 Jan 2024 23:30:08 -0800
Subject: [PATCH 310/437] set fdfd sources in a way that catches incorrect j.e
 calculation

---
 meanas/test/test_fdfd.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py
index e2e8e7b..009c65b 100644
--- a/meanas/test/test_fdfd.py
+++ b/meanas/test/test_fdfd.py
@@ -26,6 +26,8 @@ def test_poynting_planes(sim: 'FDResult') -> None:
 #        for dxa in dxg:
 #            if not (dxa == sim.dxes[0][0][0]).all():
 #                pytest.skip('test_poynting_planes skips nonuniform dxes')
+
+    # pick only the second point
     points = numpy.where(mask)
     mask[points[0][0], points[1][0], points[2][0]] = 0
 
@@ -50,7 +52,7 @@ def test_poynting_planes(sim: 'FDResult') -> None:
         )
     src_energy = -(e_dot_j.real * dv)[:, mask] / 2
 
-    assert_close(sum(planes), src_energy.sum())
+    assert_close(sum(planes), src_energy.sum(), rtol=1e-6)      # TODO improve energy calculation accuracy?
 
 
 #####################################
@@ -92,8 +94,8 @@ def j_distribution(
     if request.param == 'center':
         j[center_mask] = j_mag
     elif request.param == 'diag':
-        j[numpy.roll(center_mask, [1, 1, 1], axis=(1, 2, 3))] = j_mag
-        j[numpy.roll(center_mask, [-1, -1, -1], axis=(1, 2, 3))] = -1j * j_mag
+        j[numpy.roll(center_mask, [1, 1, 1], axis=(1, 2, 3))] = (1 + 1j) * j_mag
+        j[numpy.roll(center_mask, [-1, -1, -1], axis=(1, 2, 3))] = (1 - 1j) * j_mag
     yield j
 
 

From 1b3d322fc6263d7c7b4d741fd9029b26aaf1d416 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Thu, 18 Jan 2024 00:54:03 -0800
Subject: [PATCH 311/437] some work on FDFD derivation

---
 meanas/fdfd/__init__.py   | 59 ++++++++++++++++++++++++++++++++++++++-
 meanas/fdmath/__init__.py |  2 +-
 2 files changed, 59 insertions(+), 2 deletions(-)

diff --git a/meanas/fdfd/__init__.py b/meanas/fdfd/__init__.py
index 3e9463e..624d576 100644
--- a/meanas/fdfd/__init__.py
+++ b/meanas/fdfd/__init__.py
@@ -21,13 +21,70 @@ From the "Frequency domain" section of `meanas.fdmath`, we have
 $$
  \\begin{aligned}
  \\tilde{E}_{l, \\vec{r}} &= \\tilde{E}_{\\vec{r}} e^{-\\imath \\omega l \\Delta_t} \\\\
+ \\tilde{H}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &= \\tilde{H}_{\\vec{r} + \\frac{1}{2}} e^{-\\imath \\omega (l - \\frac{1}{2}) \\Delta_t} \\\\
  \\tilde{J}_{l, \\vec{r}} &= \\tilde{J}_{\\vec{r}} e^{-\\imath \\omega (l - \\frac{1}{2}) \\Delta_t} \\\\
+ \\tilde{M}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &= \\tilde{M}_{\\vec{r} + \\frac{1}{2}} e^{-\\imath \\omega l \\Delta_t} \\\\
  \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}})
-    -\\Omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} &= \\imath \\Omega \\tilde{J}_{\\vec{r}} \\\\
+    -\\Omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} &= -\\imath \\Omega \\tilde{J}_{\\vec{r}} e^{\\imath \\omega \\Delta_t / 2} \\\\
  \\Omega &= 2 \\sin(\\omega \\Delta_t / 2) / \\Delta_t
  \\end{aligned}
 $$
 
+resulting in
+
+$$
+ \\begin{aligned}
+ \\tilde{\\partial}_t &\\Rightarrow -\\imath \\Omega e^{-\\imath \\omega \\Delta_t / 2}\\\\
+   \\hat{\\partial}_t &\\Rightarrow -\\imath \\Omega e^{ \\imath \\omega \\Delta_t / 2}\\\\
+ \\end{aligned}
+$$
+
+Maxwell's equations are then
+
+$$
+  \\begin{aligned}
+  \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}} &=
+         \\imath \\Omega e^{-\\imath \\omega \\Delta_t / 2} \\hat{B}_{\\vec{r} + \\frac{1}{2}}
+                                                          - \\hat{M}_{\\vec{r} + \\frac{1}{2}}  \\\\
+  \\hat{\\nabla} \\times \\hat{H}_{\\vec{r} + \\frac{1}{2}} &=
+        -\\imath \\Omega e^{ \\imath \\omega \\Delta_t / 2} \\tilde{D}_{\\vec{r}}
+                                                          + \\tilde{J}_{\\vec{r}} \\\\
+  \\tilde{\\nabla} \\cdot \\hat{B}_{\\vec{r} + \\frac{1}{2}} &= 0 \\\\
+  \\hat{\\nabla} \\cdot \\tilde{D}_{\\vec{r}} &= \\rho_{\\vec{r}}
+ \\end{aligned}
+$$
+
+With $\\Delta_t \\to 0$, this simplifies to
+
+$$
+ \\begin{aligned}
+ \\tilde{E}_{l, \\vec{r}} &\\to \\tilde{E}_{\\vec{r}} \\\\
+ \\tilde{H}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &\\to \\tilde{H}_{\\vec{r} + \\frac{1}{2}} \\\\
+ \\tilde{J}_{l, \\vec{r}} &\\to \\tilde{J}_{\\vec{r}} \\\\
+ \\tilde{M}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &\\to \\tilde{M}_{\\vec{r} + \\frac{1}{2}} \\\\
+ \\Omega &\\to \\omega \\\\
+ \\tilde{\\partial}_t &\\to -\\imath \\omega \\\\
+   \\hat{\\partial}_t &\\to -\\imath \\omega \\\\
+ \\end{aligned}
+$$
+
+and then
+
+$$
+  \\begin{aligned}
+  \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}} &=
+         \\imath \\omega \\hat{B}_{\\vec{r} + \\frac{1}{2}}
+                       - \\hat{M}_{\\vec{r} + \\frac{1}{2}}  \\\\
+  \\hat{\\nabla} \\times \\hat{H}_{\\vec{r} + \\frac{1}{2}} &=
+        -\\imath \\omega \\tilde{D}_{\\vec{r}}
+                       + \\tilde{J}_{\\vec{r}} \\\\
+ \\end{aligned}
+$$
+
+$$
+ \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}})
+    -\\omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} = -\\imath \\omega \\tilde{J}_{\\vec{r}} \\\\
+$$
 
 # TODO FDFD?
 # TODO PML
diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py
index cd62fcd..eb8b6de 100644
--- a/meanas/fdmath/__init__.py
+++ b/meanas/fdmath/__init__.py
@@ -426,7 +426,7 @@ This gives the frequency-domain wave equation,
 
 $$
  \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}})
-    -\\Omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} = \\imath \\Omega \\tilde{J}_{\\vec{r}}
+    -\\Omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} = -\\imath \\Omega \\tilde{J}_{\\vec{r}} e^{\\imath \\omega \\Delta_t / 2} \\\\
 $$
 
 

From 91d89550a1e440d9aef8d45a5052e6b5fc8c4435 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 18 Mar 2024 10:34:09 -0700
Subject: [PATCH 312/437] comment

---
 meanas/fdfd/functional.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py
index 745a536..74b4263 100644
--- a/meanas/fdfd/functional.py
+++ b/meanas/fdfd/functional.py
@@ -200,7 +200,7 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], c
 
     Note:
         If `E` and `H` are peak amplitudes as assumed elsewhere in this code,
-        the time-average of the poynting vector is ` = Re(S)/2 = Re(E x H) / 2`.
+        the time-average of the poynting vector is ` = Re(S)/2 = Re(E x H*) / 2`.
         The factor of `1/2` can be omitted if root-mean-square quantities are used
         instead.
 

From 950a5831ec35869a195d351391f7cfb73157b20d Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 18 Mar 2024 10:34:21 -0700
Subject: [PATCH 313/437] should also use dxbg

---
 meanas/fdfd/operators.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py
index f7c1dc7..ff0a95c 100644
--- a/meanas/fdfd/operators.py
+++ b/meanas/fdfd/operators.py
@@ -324,6 +324,7 @@ def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
     fx, fy, fz = [shift_circ(i, shape, 1) for i in range(3)]
 
     dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
+    dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
     Ex, Ey, Ez = [ei * da for ei, da in zip(numpy.split(e, 3), dxag)]
 
     block_diags = [[ None,     fx @ -Ez, fx @  Ey],
@@ -331,7 +332,7 @@ def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
                    [ fz @ -Ey, fz @  Ex, None]]
     block_matrix = sparse.bmat([[sparse.diags(x) if x is not None else None for x in row]
                                 for row in block_diags])
-    P = block_matrix @ sparse.diags(numpy.concatenate(dxag))
+    P = block_matrix @ sparse.diags(numpy.concatenate(dxbg))
     return P
 
 

From 7b4b2058bb127c7ac15c785e1f84e9481f8a71a3 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sat, 30 Mar 2024 18:06:31 -0700
Subject: [PATCH 314/437] bump minmum python to 3.11

---
 README.md      | 2 +-
 pyproject.toml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 709e13a..44709e6 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ linear systems, ideally with double precision.
 
 **Requirements:**
 
-* python >=3.8
+* python >=3.11
 * numpy
 * scipy
 
diff --git a/pyproject.toml b/pyproject.toml
index fc66831..8b875f3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,7 +33,7 @@ classifiers = [
     "License :: OSI Approved :: GNU Affero General Public License v3",
     "Topic :: Scientific/Engineering :: Physics",
     ]
-requires-python = ">=3.8"
+requires-python = ">=3.11"
 include = [
     "LICENSE.md"
     ]

From 52d297bb3171aff1a005e4d1f63f77e9f17688c4 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sat, 30 Mar 2024 18:06:41 -0700
Subject: [PATCH 315/437] add links to pypi and github

---
 README.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 44709e6..3dcdfe0 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,8 @@ solver will need the ability to solve complex symmetric (non-Hermitian)
 linear systems, ideally with double precision.
 
 - [Source repository](https://mpxd.net/code/jan/meanas)
-- PyPI *TBD*
+- [PyPI](https://pypi.org/project/meanas)
+- [Github mirror](https://github.com/anewusername/meanas)
 
 
 ## Installation

From b47dec03173cc58ed0ecd187877b9e3b32abcbbd Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sat, 30 Mar 2024 18:08:51 -0700
Subject: [PATCH 316/437] bump version to v0.9

---
 meanas/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/__init__.py b/meanas/__init__.py
index b35edce..354adc9 100644
--- a/meanas/__init__.py
+++ b/meanas/__init__.py
@@ -6,7 +6,7 @@ See the readme or `import meanas; help(meanas)` for more info.
 
 import pathlib
 
-__version__ = '0.8'
+__version__ = '0.9'
 __author__ = 'Jan Petykiewicz'
 
 

From 4c8a07bf200eae007a98a4c38f82c2a6935155c3 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 14 Jul 2024 22:08:30 -0700
Subject: [PATCH 317/437] Use raw strings to eliminate repeated backslashes

---
 meanas/fdfd/waveguide_2d.py | 250 ++++++++++++++++++------------------
 1 file changed, 125 insertions(+), 125 deletions(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index dce3573..84b7fad 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -1,4 +1,4 @@
-"""
+r"""
 Operators and helper functions for waveguides with unchanging cross-section.
 
 The propagation direction is chosen to be along the z axis, and all fields
@@ -12,166 +12,166 @@ As the z-dependence is known, all the functions in this file assume a 2D grid
 
 Consider Maxwell's equations in continuous space, in the frequency domain. Assuming
 a structure with some (x, y) cross-section extending uniformly into the z dimension,
-with a diagonal $\\epsilon$ tensor, we have
+with a diagonal $\epsilon$ tensor, we have
 
 $$
-\\begin{aligned}
-\\nabla \\times \\vec{E}(x, y, z) &= -\\imath \\omega \\mu \\vec{H} \\\\
-\\nabla \\times \\vec{H}(x, y, z) &=  \\imath \\omega \\epsilon \\vec{E} \\\\
-\\vec{E}(x,y,z) = (\\vec{E}_t(x, y) + E_z(x, y)\\vec{z}) e^{-\\gamma z} \\\\
-\\vec{H}(x,y,z) = (\\vec{H}_t(x, y) + H_z(x, y)\\vec{z}) e^{-\\gamma z} \\\\
-\\end{aligned}
+\begin{aligned}
+\nabla \times \vec{E}(x, y, z) &= -\imath \omega \mu \vec{H} \\
+\nabla \times \vec{H}(x, y, z) &=  \imath \omega \epsilon \vec{E} \\
+\vec{E}(x,y,z) &= (\vec{E}_t(x, y) + E_z(x, y)\vec{z}) e^{-\gamma z} \\
+\vec{H}(x,y,z) &= (\vec{H}_t(x, y) + H_z(x, y)\vec{z}) e^{-\gamma z} \\
+\end{aligned}
 $$
 
 Expanding the first two equations into vector components, we get
 
 $$
-\\begin{aligned}
--\\imath \\omega \\mu_{xx} H_x &= \\partial_y E_z - \\partial_z E_y \\\\
--\\imath \\omega \\mu_{yy} H_y &= \\partial_z E_x - \\partial_x E_z \\\\
--\\imath \\omega \\mu_{zz} H_z &= \\partial_x E_y - \\partial_y E_x \\\\
-\\imath \\omega \\epsilon_{xx} E_x &= \\partial_y H_z - \\partial_z H_y \\\\
-\\imath \\omega \\epsilon_{yy} E_y &= \\partial_z H_x - \\partial_x H_z \\\\
-\\imath \\omega \\epsilon_{zz} E_z &= \\partial_x H_y - \\partial_y H_x \\\\
-\\end{aligned}
+\begin{aligned}
+-\imath \omega \mu_{xx} H_x &= \partial_y E_z - \partial_z E_y \\
+-\imath \omega \mu_{yy} H_y &= \partial_z E_x - \partial_x E_z \\
+-\imath \omega \mu_{zz} H_z &= \partial_x E_y - \partial_y E_x \\
+\imath \omega \epsilon_{xx} E_x &= \partial_y H_z - \partial_z H_y \\
+\imath \omega \epsilon_{yy} E_y &= \partial_z H_x - \partial_x H_z \\
+\imath \omega \epsilon_{zz} E_z &= \partial_x H_y - \partial_y H_x \\
+\end{aligned}
 $$
 
-Substituting in our expressions for $\\vec{E}$, $\\vec{H}$ and discretizing:
+Substituting in our expressions for $\vec{E}$, $\vec{H}$ and discretizing:
 
 $$
-\\begin{aligned}
--\\imath \\omega \\mu_{xx} H_x &= \\tilde{\\partial}_y E_z + \\gamma E_y \\\\
--\\imath \\omega \\mu_{yy} H_y &= -\\gamma E_x - \\tilde{\\partial}_x E_z \\\\
--\\imath \\omega \\mu_{zz} H_z &= \\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x \\\\
-\\imath \\omega \\epsilon_{xx} E_x &= \\hat{\\partial}_y H_z + \\gamma H_y \\\\
-\\imath \\omega \\epsilon_{yy} E_y &= -\\gamma H_x - \\hat{\\partial}_x H_z \\\\
-\\imath \\omega \\epsilon_{zz} E_z &= \\hat{\\partial}_x H_y - \\hat{\\partial}_y H_x \\\\
-\\end{aligned}
+\begin{aligned}
+-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \gamma E_y \\
+-\imath \omega \mu_{yy} H_y &= -\gamma E_x - \tilde{\partial}_x E_z \\
+-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\
+\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \gamma H_y \\
+\imath \omega \epsilon_{yy} E_y &= -\gamma H_x - \hat{\partial}_x H_z \\
+\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
+\end{aligned}
 $$
 
 Rewrite the last three equations as
 $$
-\\begin{aligned}
-\\gamma H_y &=  \\imath \\omega \\epsilon_{xx} E_x - \\hat{\\partial}_y H_z \\\\
-\\gamma H_x &= -\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z \\\\
-\\imath \\omega E_z &= \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x H_y - \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y H_x \\\\
-\\end{aligned}
+\begin{aligned}
+\gamma H_y &=  \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
+\gamma H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
+\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
+\end{aligned}
 $$
 
-Now apply $\\gamma \\tilde{\\partial}_x$ to the last equation,
-then substitute in for $\\gamma H_x$ and $\\gamma H_y$:
+Now apply $\gamma \tilde{\partial}_x$ to the last equation,
+then substitute in for $\gamma H_x$ and $\gamma H_y$:
 
 $$
-\\begin{aligned}
-\\gamma \\tilde{\\partial}_x \\imath \\omega E_z &= \\gamma \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x H_y
-                                                - \\gamma \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y H_x \\\\
-        &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x ( \\imath \\omega \\epsilon_{xx} E_x - \\hat{\\partial}_y H_z)
-         - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (-\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z)  \\\\
-        &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x ( \\imath \\omega \\epsilon_{xx} E_x)
-         - \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (-\\imath \\omega \\epsilon_{yy} E_y)  \\\\
-\\gamma \\tilde{\\partial}_x E_z &= \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x)
-                                  + \\tilde{\\partial}_x \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\
-\\end{aligned}
+\begin{aligned}
+\gamma \tilde{\partial}_x \imath \omega E_z &= \gamma \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y
+                                             - \gamma \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
+        &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z)
+         - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z)  \\
+        &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x)
+         - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y)  \\
+\gamma \tilde{\partial}_x E_z &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+                               + \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
+\end{aligned}
 $$
 
-With a similar approach (but using $\\gamma \\tilde{\\partial}_y$ instead), we can get
+With a similar approach (but using $\gamma \tilde{\partial}_y$ instead), we can get
 
 $$
-\\begin{aligned}
-\\gamma \\tilde{\\partial}_y E_z &= \\tilde{\\partial}_y \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x)
-                                  + \\tilde{\\partial}_y \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y) \\\\
-\\end{aligned}
+\begin{aligned}
+\gamma \tilde{\partial}_y E_z &= \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+                               + \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
+\end{aligned}
 $$
 
-We can combine this equation for $\\gamma \\tilde{\\partial}_y E_z$ with
-the unused $\\imath \\omega \\mu_{xx} H_x$ and $\\imath \\omega \\mu_{yy} H_y$ equations to get
+We can combine this equation for $\gamma \tilde{\partial}_y E_z$ with
+the unused $\imath \omega \mu_{xx} H_x$ and $\imath \omega \mu_{yy} H_y$ equations to get
 
 $$
-\\begin{aligned}
--\\imath \\omega \\mu_{xx} \\gamma H_x &=  \\gamma^2 E_y + \\gamma \\tilde{\\partial}_y E_z \\\\
--\\imath \\omega \\mu_{xx} \\gamma H_x &=  \\gamma^2 E_y + \\tilde{\\partial}_y (
-                                      \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x)
-                                    + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y)
-                                    )\\\\
-\\end{aligned}
+\begin{aligned}
+-\imath \omega \mu_{xx} \gamma H_x &=  \gamma^2 E_y + \gamma \tilde{\partial}_y E_z \\
+-\imath \omega \mu_{xx} \gamma H_x &=  \gamma^2 E_y + \tilde{\partial}_y (
+                                      \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+                                    + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
+                                    )\\
+\end{aligned}
 $$
 
 and
 
 $$
-\\begin{aligned}
--\\imath \\omega \\mu_{yy} \\gamma H_y &= -\\gamma^2 E_x - \\gamma \\tilde{\\partial}_x E_z \\\\
--\\imath \\omega \\mu_{yy} \\gamma H_y &= -\\gamma^2 E_x - \\tilde{\\partial}_x (
-                                      \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x)
-                                    + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y)
-                                    )\\\\
-\\end{aligned}
+\begin{aligned}
+-\imath \omega \mu_{yy} \gamma H_y &= -\gamma^2 E_x - \gamma \tilde{\partial}_x E_z \\
+-\imath \omega \mu_{yy} \gamma H_y &= -\gamma^2 E_x - \tilde{\partial}_x (
+                                      \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+                                    + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
+                                    )\\
+\end{aligned}
 $$
 
-However, based on our rewritten equation for $\\gamma H_x$ and the so-far unused
-equation for $\\imath \\omega \\mu_{zz} H_z$ we can also write
+However, based on our rewritten equation for $\gamma H_x$ and the so-far unused
+equation for $\imath \omega \mu_{zz} H_z$ we can also write
 
 $$
-\\begin{aligned}
--\\imath \\omega \\mu_{xx} (\\gamma H_x) &= -\\imath \\omega \\mu_{xx} (-\\imath \\omega \\epsilon_{yy} E_y - \\hat{\\partial}_x H_z) \\\\
-                                         &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y
-                                            +\\imath \\omega \\mu_{xx} \\hat{\\partial}_x (
-                                                \\frac{1}{-\\imath \\omega \\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x)) \\\\
-                                         &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y
-                                            -\\mu_{xx} \\hat{\\partial}_x \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\
-\\end{aligned}
+\begin{aligned}
+-\imath \omega \mu_{xx} (\gamma H_x) &= -\imath \omega \mu_{xx} (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\
+                                     &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
+                                        +\imath \omega \mu_{xx} \hat{\partial}_x (
+                                           \frac{1}{-\imath \omega \mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x)) \\
+                                     &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
+                                        -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
+\end{aligned}
 $$
 
 and, similarly,
 
 $$
-\\begin{aligned}
--\\imath \\omega \\mu_{yy} (\\gamma H_y) &= \\omega^2 \\mu_{yy} \\epsilon_{xx} E_x
-                                           +\\mu_{yy} \\hat{\\partial}_y \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\
-\\end{aligned}
+\begin{aligned}
+-\imath \omega \mu_{yy} (\gamma H_y) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
+                                           +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
+\end{aligned}
 $$
 
 By combining both pairs of expressions, we get
 
 $$
-\\begin{aligned}
--\\gamma^2 E_x - \\tilde{\\partial}_x (
-    \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x)
-  + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y)
-    ) &= \\omega^2 \\mu_{yy} \\epsilon_{xx} E_x
-        +\\mu_{yy} \\hat{\\partial}_y \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\
-\\gamma^2 E_y + \\tilde{\\partial}_y (
-    \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_x (\\epsilon_{xx} E_x)
-  + \\frac{1}{\\epsilon_{zz}} \\hat{\\partial}_y (\\epsilon_{yy} E_y)
-    ) &= -\\omega^2 \\mu_{xx} \\epsilon_{yy} E_y
-         -\\mu_{xx} \\hat{\\partial}_x \\frac{1}{\\mu_{zz}} (\\tilde{\\partial}_x E_y - \\tilde{\\partial}_y E_x) \\\\
-\\end{aligned}
+\begin{aligned}
+-\gamma^2 E_x - \tilde{\partial}_x (
+    \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+  + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
+    ) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
+        +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
+\gamma^2 E_y + \tilde{\partial}_y (
+    \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+  + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
+    ) &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
+         -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
+\end{aligned}
 $$
 
 Using these, we can construct the eigenvalue problem
 
 $$
-\\beta^2 \\begin{bmatrix} E_x \\\\
-                          E_y \\end{bmatrix} =
-    (\\omega^2 \\begin{bmatrix} \\mu_{yy} \\epsilon_{xx} & 0 \\\\
-                                                       0 & \\mu_{xx} \\epsilon_{yy} \\end{bmatrix} +
-                 \\begin{bmatrix} -\\mu_{yy} \\hat{\\partial}_y \\\\
-                                   \\mu_{xx} \\hat{\\partial}_x \\end{bmatrix} \\mu_{zz}^{-1}
-                 \\begin{bmatrix} -\\tilde{\\partial}_y & \\tilde{\\partial}_x \\end{bmatrix} +
-      \\begin{bmatrix} \\tilde{\\partial}_x \\\\
-                       \\tilde{\\partial}_y \\end{bmatrix} \\epsilon_{zz}^{-1}
-                 \\begin{bmatrix} \\hat{\\partial}_x \\epsilon_{xx} & \\hat{\\partial}_y \\epsilon_{yy} \\end{bmatrix})
-    \\begin{bmatrix} E_x \\\\
-                     E_y \\end{bmatrix}
+\beta^2 \begin{bmatrix} E_x \\
+                        E_y \end{bmatrix} =
+    (\omega^2 \begin{bmatrix} \mu_{yy} \epsilon_{xx} & 0 \\
+                                                   0 & \mu_{xx} \epsilon_{yy} \end{bmatrix} +
+              \begin{bmatrix} -\mu_{yy} \hat{\partial}_y \\
+                               \mu_{xx} \hat{\partial}_x \end{bmatrix} \mu_{zz}^{-1}
+              \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} +
+      \begin{bmatrix} \tilde{\partial}_x \\
+                      \tilde{\partial}_y \end{bmatrix} \epsilon_{zz}^{-1}
+                 \begin{bmatrix} \hat{\partial}_x \epsilon_{xx} & \hat{\partial}_y \epsilon_{yy} \end{bmatrix})
+    \begin{bmatrix} E_x \\
+                    E_y \end{bmatrix}
 $$
 
-where $\\gamma = \\imath\\beta$. In the literature, $\\beta$ is usually used to denote
+where $\gamma = \imath\beta$. In the literature, $\beta$ is usually used to denote
 the lossless/real part of the propagation constant, but in `meanas` it is allowed to
 be complex.
 
 An equivalent eigenvalue problem can be formed using the $H_x$ and $H_y$ fields, if those are more convenient.
 
-Note that $E_z$ was never discretized, so $\\gamma$ and $\\beta$ will need adjustment
+Note that $E_z$ was never discretized, so $\gamma$ and $\beta$ will need adjustment
 to account for numerical dispersion if the result is introduced into a space with a discretized z-axis.
 
 
@@ -198,7 +198,7 @@ def operator_e(
         epsilon: vfdfield_t,
         mu: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
-    """
+    r"""
     Waveguide operator of the form
 
         omega**2 * mu * epsilon +
@@ -210,18 +210,18 @@ def operator_e(
     More precisely, the operator is
 
     $$
-    \\omega^2 \\begin{bmatrix} \\mu_{yy} \\epsilon_{xx} & 0 \\\\
-                                                       0 & \\mu_{xx} \\epsilon_{yy} \\end{bmatrix} +
-                 \\begin{bmatrix} -\\mu_{yy} \\hat{\\partial}_y \\\\
-                                   \\mu_{xx} \\hat{\\partial}_x \\end{bmatrix} \\mu_{zz}^{-1}
-                 \\begin{bmatrix} -\\tilde{\\partial}_y & \\tilde{\\partial}_x \\end{bmatrix} +
-      \\begin{bmatrix} \\tilde{\\partial}_x \\\\
-                       \\tilde{\\partial}_y \\end{bmatrix} \\epsilon_{zz}^{-1}
-                 \\begin{bmatrix} \\hat{\\partial}_x \\epsilon_{xx} & \\hat{\\partial}_y \\epsilon_{yy} \\end{bmatrix}
+    \omega^2 \begin{bmatrix} \mu_{yy} \epsilon_{xx} & 0 \\
+                                                       0 & \mu_{xx} \epsilon_{yy} \end{bmatrix} +
+                 \begin{bmatrix} -\mu_{yy} \hat{\partial}_y \\
+                                   \mu_{xx} \hat{\partial}_x \end{bmatrix} \mu_{zz}^{-1}
+                 \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} +
+      \begin{bmatrix} \tilde{\partial}_x \\
+                       \tilde{\partial}_y \end{bmatrix} \epsilon_{zz}^{-1}
+                 \begin{bmatrix} \hat{\partial}_x \epsilon_{xx} & \hat{\partial}_y \epsilon_{yy} \end{bmatrix}
     $$
 
-    $\\tilde{\\partial}_x$ and $\\hat{\\partial}_x$ are the forward and backward derivatives along x,
-    and each $\\epsilon_{xx}$, $\\mu_{yy}$, etc. is a diagonal matrix containing the vectorized material
+    $\tilde{\partial}_x$ and $\hat{\partial}_x$ are the forward and backward derivatives along x,
+    and each $\epsilon_{xx}$, $\mu_{yy}$, etc. is a diagonal matrix containing the vectorized material
     property distribution.
 
     This operator can be used to form an eigenvalue problem of the form
@@ -265,7 +265,7 @@ def operator_h(
         epsilon: vfdfield_t,
         mu: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
-    """
+    r"""
     Waveguide operator of the form
 
         omega**2 * epsilon * mu +
@@ -277,18 +277,18 @@ def operator_h(
     More precisely, the operator is
 
     $$
-    \\omega^2 \\begin{bmatrix} \\epsilon_{yy} \\mu_{xx} & 0 \\\\
-                                                      0 & \\epsilon_{xx} \\mu_{yy} \\end{bmatrix} +
-                 \\begin{bmatrix} -\\epsilon_{yy} \\tilde{\\partial}_y \\\\
-                                   \\epsilon_{xx} \\tilde{\\partial}_x \\end{bmatrix} \\epsilon_{zz}^{-1}
-                 \\begin{bmatrix} -\\hat{\\partial}_y & \\hat{\\partial}_x \\end{bmatrix} +
-      \\begin{bmatrix} \\hat{\\partial}_x \\\\
-                       \\hat{\\partial}_y \\end{bmatrix} \\mu_{zz}^{-1}
-                 \\begin{bmatrix} \\tilde{\\partial}_x \\mu_{xx} & \\tilde{\\partial}_y \\mu_{yy} \\end{bmatrix}
+    \omega^2 \begin{bmatrix} \epsilon_{yy} \mu_{xx} & 0 \\
+                                                  0 & \epsilon_{xx} \mu_{yy} \end{bmatrix} +
+                 \begin{bmatrix} -\epsilon_{yy} \tilde{\partial}_y \\
+                                  \epsilon_{xx} \tilde{\partial}_x \end{bmatrix} \epsilon_{zz}^{-1}
+                 \begin{bmatrix} -\hat{\partial}_y & \hat{\partial}_x \end{bmatrix} +
+      \begin{bmatrix} \hat{\partial}_x \\
+                      \hat{\partial}_y \end{bmatrix} \mu_{zz}^{-1}
+                 \begin{bmatrix} \tilde{\partial}_x \mu_{xx} & \tilde{\partial}_y \mu_{yy} \end{bmatrix}
     $$
 
-    $\\tilde{\\partial}_x$ and $\\hat{\\partial}_x$ are the forward and backward derivatives along x,
-    and each $\\epsilon_{xx}$, $\\mu_{yy}$, etc. is a diagonal matrix containing the vectorized material
+    $\tilde{\partial}_x$ and $\hat{\partial}_x$ are the forward and backward derivatives along x,
+    and each $\epsilon_{xx}$, $\mu_{yy}$, etc. is a diagonal matrix containing the vectorized material
     property distribution.
 
     This operator can be used to form an eigenvalue problem of the form

From 22565328abc75765c9aa4cabda71c94a15053b09 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 14 Jul 2024 22:08:52 -0700
Subject: [PATCH 318/437] use parens in place of backslashes

---
 meanas/fdfd/waveguide_2d.py | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 84b7fad..aea2345 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -253,9 +253,10 @@ def operator_e(
     mu_yx = sparse.diags(numpy.hstack((mu_parts[1], mu_parts[0])))
     mu_z_inv = sparse.diags(1 / mu_parts[2])
 
-    op = omega * omega * mu_yx @ eps_xy + \
-        mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx)) + \
-        sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy
+    op = (omega * omega * mu_yx @ eps_xy
+        + mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx))
+        + sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy
+        )
     return op
 
 
@@ -320,10 +321,10 @@ def operator_h(
     mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1])))
     mu_z_inv = sparse.diags(1 / mu_parts[2])
 
-    op = omega * omega * eps_yx @ mu_xy + \
-        eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx)) + \
-        sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy
-
+    op = (omega * omega * eps_yx @ mu_xy
+        + eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx))
+        + sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy
+        )
     return op
 
 

From 8c49710861181b1575cf88cc67aa607cd5d6a9a7 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 14 Jul 2024 22:09:16 -0700
Subject: [PATCH 319/437] black bg for tex svgs

---
 make_docs.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/make_docs.sh b/make_docs.sh
index 7ce9e39..6d99b5f 100755
--- a/make_docs.sh
+++ b/make_docs.sh
@@ -12,7 +12,7 @@ cd ~/projects/meanas
 rm -rf _doc_mathimg
 pdoc --pdf --force --template-dir pdoc_templates -o doc . > doc.md
 pandoc --metadata=title:"meanas" --from=markdown+abbreviations --to=html --output=doc.htex --gladtex -s --css pdoc_templates/pdoc.css doc.md
-gladtex -a -n -d _doc_mathimg -c white doc.htex
+gladtex -a -n -d _doc_mathimg -c white -b black doc.htex
 
 # Approach 3: html with gladtex
 #pdoc3 --html --force --template-dir pdoc_templates -o doc .

From 2d48858973036b5c5a328fde6f89b5d9608b6169 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 15 Jul 2024 16:10:51 -0700
Subject: [PATCH 320/437] drop duplicate import

---
 pdoc_templates/html_helpers.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/pdoc_templates/html_helpers.py b/pdoc_templates/html_helpers.py
index a0be764..5e58405 100644
--- a/pdoc_templates/html_helpers.py
+++ b/pdoc_templates/html_helpers.py
@@ -321,7 +321,6 @@ class _ToMarkdown:
         """Wrap URLs in Python-Markdown-compatible ."""
         return re.sub(r'(?)\s]+)(\s*)', r'\1<\2>\3', text)
 
-import subprocess
 
 class _MathPattern(InlineProcessor):
     NAME = 'pdoc-math'

From 77715da8b4e818e6c15f990355daacf6fab41dda Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 15 Jul 2024 16:32:31 -0700
Subject: [PATCH 321/437] Use raw strings to avoid double backslashes

---
 meanas/fdfd/__init__.py     |  88 ++++++------
 meanas/fdfd/functional.py   |   6 +-
 meanas/fdfd/operators.py    |  42 +++---
 meanas/fdfd/waveguide_2d.py |   2 +-
 meanas/fdmath/__init__.py   | 266 ++++++++++++++++++------------------
 meanas/fdmath/functional.py |   8 +-
 meanas/fdtd/__init__.py     | 148 ++++++++++----------
 meanas/fdtd/energy.py       |  22 +--
 8 files changed, 291 insertions(+), 291 deletions(-)

diff --git a/meanas/fdfd/__init__.py b/meanas/fdfd/__init__.py
index 624d576..1829cf9 100644
--- a/meanas/fdfd/__init__.py
+++ b/meanas/fdfd/__init__.py
@@ -1,4 +1,4 @@
-"""
+r"""
 Tools for finite difference frequency-domain (FDFD) simulations and calculations.
 
 These mostly involve picking a single frequency, then setting up and solving a
@@ -19,71 +19,71 @@ Submodules:
 From the "Frequency domain" section of `meanas.fdmath`, we have
 
 $$
- \\begin{aligned}
- \\tilde{E}_{l, \\vec{r}} &= \\tilde{E}_{\\vec{r}} e^{-\\imath \\omega l \\Delta_t} \\\\
- \\tilde{H}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &= \\tilde{H}_{\\vec{r} + \\frac{1}{2}} e^{-\\imath \\omega (l - \\frac{1}{2}) \\Delta_t} \\\\
- \\tilde{J}_{l, \\vec{r}} &= \\tilde{J}_{\\vec{r}} e^{-\\imath \\omega (l - \\frac{1}{2}) \\Delta_t} \\\\
- \\tilde{M}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &= \\tilde{M}_{\\vec{r} + \\frac{1}{2}} e^{-\\imath \\omega l \\Delta_t} \\\\
- \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}})
-    -\\Omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} &= -\\imath \\Omega \\tilde{J}_{\\vec{r}} e^{\\imath \\omega \\Delta_t / 2} \\\\
- \\Omega &= 2 \\sin(\\omega \\Delta_t / 2) / \\Delta_t
- \\end{aligned}
+ \begin{aligned}
+ \tilde{E}_{l, \vec{r}} &= \tilde{E}_{\vec{r}} e^{-\imath \omega l \Delta_t} \\
+ \tilde{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &= \tilde{H}_{\vec{r} + \frac{1}{2}} e^{-\imath \omega (l - \frac{1}{2}) \Delta_t} \\
+ \tilde{J}_{l, \vec{r}} &= \tilde{J}_{\vec{r}} e^{-\imath \omega (l - \frac{1}{2}) \Delta_t} \\
+ \tilde{M}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &= \tilde{M}_{\vec{r} + \frac{1}{2}} e^{-\imath \omega l \Delta_t} \\
+ \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{\vec{r}})
+    -\Omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} &= -\imath \Omega \tilde{J}_{\vec{r}} e^{\imath \omega \Delta_t / 2} \\
+ \Omega &= 2 \sin(\omega \Delta_t / 2) / \Delta_t
+ \end{aligned}
 $$
 
 resulting in
 
 $$
- \\begin{aligned}
- \\tilde{\\partial}_t &\\Rightarrow -\\imath \\Omega e^{-\\imath \\omega \\Delta_t / 2}\\\\
-   \\hat{\\partial}_t &\\Rightarrow -\\imath \\Omega e^{ \\imath \\omega \\Delta_t / 2}\\\\
- \\end{aligned}
+ \begin{aligned}
+ \tilde{\partial}_t &\Rightarrow -\imath \Omega e^{-\imath \omega \Delta_t / 2}\\
+   \hat{\partial}_t &\Rightarrow -\imath \Omega e^{ \imath \omega \Delta_t / 2}\\
+ \end{aligned}
 $$
 
 Maxwell's equations are then
 
 $$
-  \\begin{aligned}
-  \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}} &=
-         \\imath \\Omega e^{-\\imath \\omega \\Delta_t / 2} \\hat{B}_{\\vec{r} + \\frac{1}{2}}
-                                                          - \\hat{M}_{\\vec{r} + \\frac{1}{2}}  \\\\
-  \\hat{\\nabla} \\times \\hat{H}_{\\vec{r} + \\frac{1}{2}} &=
-        -\\imath \\Omega e^{ \\imath \\omega \\Delta_t / 2} \\tilde{D}_{\\vec{r}}
-                                                          + \\tilde{J}_{\\vec{r}} \\\\
-  \\tilde{\\nabla} \\cdot \\hat{B}_{\\vec{r} + \\frac{1}{2}} &= 0 \\\\
-  \\hat{\\nabla} \\cdot \\tilde{D}_{\\vec{r}} &= \\rho_{\\vec{r}}
- \\end{aligned}
+  \begin{aligned}
+  \tilde{\nabla} \times \tilde{E}_{\vec{r}} &=
+         \imath \Omega e^{-\imath \omega \Delta_t / 2} \hat{B}_{\vec{r} + \frac{1}{2}}
+                                                     - \hat{M}_{\vec{r} + \frac{1}{2}}  \\
+  \hat{\nabla} \times \hat{H}_{\vec{r} + \frac{1}{2}} &=
+        -\imath \Omega e^{ \imath \omega \Delta_t / 2} \tilde{D}_{\vec{r}}
+                                                     + \tilde{J}_{\vec{r}} \\
+  \tilde{\nabla} \cdot \hat{B}_{\vec{r} + \frac{1}{2}} &= 0 \\
+  \hat{\nabla} \cdot \tilde{D}_{\vec{r}} &= \rho_{\vec{r}}
+ \end{aligned}
 $$
 
-With $\\Delta_t \\to 0$, this simplifies to
+With $\Delta_t \to 0$, this simplifies to
 
 $$
- \\begin{aligned}
- \\tilde{E}_{l, \\vec{r}} &\\to \\tilde{E}_{\\vec{r}} \\\\
- \\tilde{H}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &\\to \\tilde{H}_{\\vec{r} + \\frac{1}{2}} \\\\
- \\tilde{J}_{l, \\vec{r}} &\\to \\tilde{J}_{\\vec{r}} \\\\
- \\tilde{M}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &\\to \\tilde{M}_{\\vec{r} + \\frac{1}{2}} \\\\
- \\Omega &\\to \\omega \\\\
- \\tilde{\\partial}_t &\\to -\\imath \\omega \\\\
-   \\hat{\\partial}_t &\\to -\\imath \\omega \\\\
- \\end{aligned}
+ \begin{aligned}
+ \tilde{E}_{l, \vec{r}} &\to \tilde{E}_{\vec{r}} \\
+ \tilde{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &\to \tilde{H}_{\vec{r} + \frac{1}{2}} \\
+ \tilde{J}_{l, \vec{r}} &\to \tilde{J}_{\vec{r}} \\
+ \tilde{M}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &\to \tilde{M}_{\vec{r} + \frac{1}{2}} \\
+ \Omega &\to \omega \\
+ \tilde{\partial}_t &\to -\imath \omega \\
+   \hat{\partial}_t &\to -\imath \omega \\
+ \end{aligned}
 $$
 
 and then
 
 $$
-  \\begin{aligned}
-  \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}} &=
-         \\imath \\omega \\hat{B}_{\\vec{r} + \\frac{1}{2}}
-                       - \\hat{M}_{\\vec{r} + \\frac{1}{2}}  \\\\
-  \\hat{\\nabla} \\times \\hat{H}_{\\vec{r} + \\frac{1}{2}} &=
-        -\\imath \\omega \\tilde{D}_{\\vec{r}}
-                       + \\tilde{J}_{\\vec{r}} \\\\
- \\end{aligned}
+  \begin{aligned}
+  \tilde{\nabla} \times \tilde{E}_{\vec{r}} &=
+         \imath \omega \hat{B}_{\vec{r} + \frac{1}{2}}
+                     - \hat{M}_{\vec{r} + \frac{1}{2}}  \\
+  \hat{\nabla} \times \hat{H}_{\vec{r} + \frac{1}{2}} &=
+        -\imath \omega \tilde{D}_{\vec{r}}
+                     + \tilde{J}_{\vec{r}} \\
+ \end{aligned}
 $$
 
 $$
- \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}})
-    -\\omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} = -\\imath \\omega \\tilde{J}_{\\vec{r}} \\\\
+ \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{\vec{r}})
+    -\omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath \omega \tilde{J}_{\vec{r}} \\
 $$
 
 # TODO FDFD?
diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py
index 74b4263..ba2bd70 100644
--- a/meanas/fdfd/functional.py
+++ b/meanas/fdfd/functional.py
@@ -189,10 +189,10 @@ def e_tfsf_source(
 
 
 def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], cfdfield_t]:
-    """
+    r"""
     Generates a function that takes the single-frequency `E` and `H` fields
-    and calculates the cross product `E` x `H` = $E \\times H$ as required
-    for the Poynting vector, $S = E \\times H$
+    and calculates the cross product `E` x `H` = $E \times H$ as required
+    for the Poynting vector, $S = E \times H$
 
     Note:
         This function also shifts the input `E` field by one cell as required
diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py
index ff0a95c..3a489a7 100644
--- a/meanas/fdfd/operators.py
+++ b/meanas/fdfd/operators.py
@@ -45,14 +45,14 @@ def e_full(
         pec: vfdfield_t | None = None,
         pmc: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
-    """
+    r"""
     Wave operator
-     $$ \\nabla \\times (\\frac{1}{\\mu} \\nabla \\times) - \\Omega^2 \\epsilon $$
+     $$ \nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon $$
 
         del x (1/mu * del x) - omega**2 * epsilon
 
      for use with the E-field, with wave equation
-     $$ (\\nabla \\times (\\frac{1}{\\mu} \\nabla \\times) - \\Omega^2 \\epsilon) E = -\\imath \\omega J $$
+     $$ (\nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon) E = -\imath \omega J $$
 
         (del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J
 
@@ -131,14 +131,14 @@ def h_full(
         pec: vfdfield_t | None = None,
         pmc: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
-    """
+    r"""
     Wave operator
-     $$ \\nabla \\times (\\frac{1}{\\epsilon} \\nabla \\times) - \\omega^2 \\mu $$
+     $$ \nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu $$
 
         del x (1/epsilon * del x) - omega**2 * mu
 
      for use with the H-field, with wave equation
-     $$ (\\nabla \\times (\\frac{1}{\\epsilon} \\nabla \\times) - \\omega^2 \\mu) E = \\imath \\omega M $$
+     $$ (\nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu) E = \imath \omega M $$
 
         (del x (1/epsilon * del x) - omega**2 * mu) E = i * omega * M
 
@@ -188,28 +188,28 @@ def eh_full(
         pec: vfdfield_t | None = None,
         pmc: vfdfield_t | None = None,
         ) -> sparse.spmatrix:
-    """
+    r"""
     Wave operator for `[E, H]` field representation. This operator implements Maxwell's
      equations without cancelling out either E or H. The operator is
-    $$  \\begin{bmatrix}
-        -\\imath \\omega \\epsilon  &  \\nabla \\times      \\\\
-        \\nabla \\times             &  \\imath \\omega \\mu
-        \\end{bmatrix} $$
+    $$  \begin{bmatrix}
+        -\imath \omega \epsilon  &  \nabla \times      \\
+        \nabla \times            &  \imath \omega \mu
+        \end{bmatrix} $$
 
         [[-i * omega * epsilon,  del x         ],
          [del x,                 i * omega * mu]]
 
     for use with a field vector of the form `cat(vec(E), vec(H))`:
-    $$  \\begin{bmatrix}
-        -\\imath \\omega \\epsilon  &  \\nabla \\times      \\\\
-        \\nabla \\times             &  \\imath \\omega \\mu
-        \\end{bmatrix}
-        \\begin{bmatrix} E \\\\
-                         H
-        \\end{bmatrix}
-        = \\begin{bmatrix} J \\\\
-                          -M
-          \\end{bmatrix} $$
+    $$  \begin{bmatrix}
+        -\imath \omega \epsilon  &  \nabla \times      \\
+        \nabla \times            &  \imath \omega \mu
+        \end{bmatrix}
+        \begin{bmatrix} E \\
+                        H
+        \end{bmatrix}
+        = \begin{bmatrix} J \\
+                         -M
+          \end{bmatrix} $$
 
     Args:
         omega: Angular frequency of the simulation
diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index aea2345..eaae21c 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -211,7 +211,7 @@ def operator_e(
 
     $$
     \omega^2 \begin{bmatrix} \mu_{yy} \epsilon_{xx} & 0 \\
-                                                       0 & \mu_{xx} \epsilon_{yy} \end{bmatrix} +
+                                                      0 & \mu_{xx} \epsilon_{yy} \end{bmatrix} +
                  \begin{bmatrix} -\mu_{yy} \hat{\partial}_y \\
                                    \mu_{xx} \hat{\partial}_x \end{bmatrix} \mu_{zz}^{-1}
                  \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} +
diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py
index eb8b6de..8a6b784 100644
--- a/meanas/fdmath/__init__.py
+++ b/meanas/fdmath/__init__.py
@@ -1,4 +1,4 @@
-"""
+r"""
 
 Basic discrete calculus for finite difference (fd) simulations.
 
@@ -43,11 +43,11 @@ Scalar derivatives and cell shifts
 ----------------------------------
 
 Define the discrete forward derivative as
- $$ [\\tilde{\\partial}_x f]_{m + \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m + 1} - f_m) $$
+ $$ [\tilde{\partial}_x f]_{m + \frac{1}{2}} = \frac{1}{\Delta_{x, m}} (f_{m + 1} - f_m) $$
  where $f$ is a function defined at discrete locations on the x-axis (labeled using $m$).
- The value at $m$ occupies a length $\\Delta_{x, m}$ along the x-axis. Note that $m$
+ The value at $m$ occupies a length $\Delta_{x, m}$ along the x-axis. Note that $m$
  is an index along the x-axis, _not_ necessarily an x-coordinate, since each length
- $\\Delta_{x, m}, \\Delta_{x, m+1}, ...$ is independently chosen.
+ $\Delta_{x, m}, \Delta_{x, m+1}, ...$ is independently chosen.
 
 If we treat `f` as a 1D array of values, with the `i`-th value `f[i]` taking up a length `dx[i]`
 along the x-axis, the forward derivative is
@@ -56,13 +56,13 @@ along the x-axis, the forward derivative is
 
 
 Likewise, discrete reverse derivative is
- $$ [\\hat{\\partial}_x f ]_{m - \\frac{1}{2}} = \\frac{1}{\\Delta_{x, m}} (f_{m} - f_{m - 1}) $$
+ $$ [\hat{\partial}_x f ]_{m - \frac{1}{2}} = \frac{1}{\Delta_{x, m}} (f_{m} - f_{m - 1}) $$
  or
 
     deriv_back(f)[i] = (f[i] - f[i - 1]) / dx[i]
 
 The derivatives' values are shifted by a half-cell relative to the original function, and
-will have different cell widths if all the `dx[i]` ( $\\Delta_{x, m}$ ) are not
+will have different cell widths if all the `dx[i]` ( $\Delta_{x, m}$ ) are not
 identical:
 
     [figure: derivatives and cell sizes]
@@ -88,19 +88,19 @@ identical:
 
 In the above figure,
  `f0 =` $f_0$, `f1 =` $f_1$
- `Df0 =` $[\\tilde{\\partial}f]_{0 + \\frac{1}{2}}$
- `Df1 =` $[\\tilde{\\partial}f]_{1 + \\frac{1}{2}}$
- `df0 =` $[\\hat{\\partial}f]_{0 - \\frac{1}{2}}$
+ `Df0 =` $[\tilde{\partial}f]_{0 + \frac{1}{2}}$
+ `Df1 =` $[\tilde{\partial}f]_{1 + \frac{1}{2}}$
+ `df0 =` $[\hat{\partial}f]_{0 - \frac{1}{2}}$
  etc.
 
-The fractional subscript $m + \\frac{1}{2}$ is used to indicate values defined
+The fractional subscript $m + \frac{1}{2}$ is used to indicate values defined
  at shifted locations relative to the original $m$, with corresponding lengths
- $$ \\Delta_{x, m + \\frac{1}{2}} = \\frac{1}{2} * (\\Delta_{x, m} + \\Delta_{x, m + 1}) $$
+ $$ \Delta_{x, m + \frac{1}{2}} = \frac{1}{2} * (\Delta_{x, m} + \Delta_{x, m + 1}) $$
 
-Just as $m$ is not itself an x-coordinate, neither is $m + \\frac{1}{2}$;
+Just as $m$ is not itself an x-coordinate, neither is $m + \frac{1}{2}$;
 carefully note the positions of the various cells in the above figure vs their labels.
 If the positions labeled with $m$ are considered the "base" or "original" grid,
-the positions labeled with $m + \\frac{1}{2}$ are said to lie on a "dual" or
+the positions labeled with $m + \frac{1}{2}$ are said to lie on a "dual" or
 "derived" grid.
 
 For the remainder of the `Discrete calculus` section, all figures will show
@@ -113,12 +113,12 @@ Gradients and fore-vectors
 --------------------------
 
 Expanding to three dimensions, we can define two gradients
-  $$ [\\tilde{\\nabla} f]_{m,n,p} = \\vec{x} [\\tilde{\\partial}_x f]_{m + \\frac{1}{2},n,p} +
-                                    \\vec{y} [\\tilde{\\partial}_y f]_{m,n + \\frac{1}{2},p} +
-                                    \\vec{z} [\\tilde{\\partial}_z f]_{m,n,p + \\frac{1}{2}}  $$
-  $$ [\\hat{\\nabla} f]_{m,n,p} = \\vec{x} [\\hat{\\partial}_x f]_{m + \\frac{1}{2},n,p} +
-                                  \\vec{y} [\\hat{\\partial}_y f]_{m,n + \\frac{1}{2},p} +
-                                  \\vec{z} [\\hat{\\partial}_z f]_{m,n,p + \\frac{1}{2}}  $$
+  $$ [\tilde{\nabla} f]_{m,n,p} = \vec{x} [\tilde{\partial}_x f]_{m + \frac{1}{2},n,p} +
+                                  \vec{y} [\tilde{\partial}_y f]_{m,n + \frac{1}{2},p} +
+                                  \vec{z} [\tilde{\partial}_z f]_{m,n,p + \frac{1}{2}}  $$
+  $$ [\hat{\nabla} f]_{m,n,p} = \vec{x} [\hat{\partial}_x f]_{m + \frac{1}{2},n,p} +
+                                \vec{y} [\hat{\partial}_y f]_{m,n + \frac{1}{2},p} +
+                                \vec{z} [\hat{\partial}_z f]_{m,n,p + \frac{1}{2}}  $$
 
  or
 
@@ -144,12 +144,12 @@ y in y, and z in z.
 
 We call the resulting object a "fore-vector" or "back-vector", depending
 on the direction of the shift. We write it as
-  $$ \\tilde{g}_{m,n,p} = \\vec{x} g^x_{m + \\frac{1}{2},n,p} +
-                          \\vec{y} g^y_{m,n + \\frac{1}{2},p} +
-                          \\vec{z} g^z_{m,n,p + \\frac{1}{2}} $$
-  $$ \\hat{g}_{m,n,p} = \\vec{x} g^x_{m - \\frac{1}{2},n,p} +
-                        \\vec{y} g^y_{m,n - \\frac{1}{2},p} +
-                        \\vec{z} g^z_{m,n,p - \\frac{1}{2}} $$
+  $$ \tilde{g}_{m,n,p} = \vec{x} g^x_{m + \frac{1}{2},n,p} +
+                         \vec{y} g^y_{m,n + \frac{1}{2},p} +
+                         \vec{z} g^z_{m,n,p + \frac{1}{2}} $$
+  $$ \hat{g}_{m,n,p} = \vec{x} g^x_{m - \frac{1}{2},n,p} +
+                       \vec{y} g^y_{m,n - \frac{1}{2},p} +
+                       \vec{z} g^z_{m,n,p - \frac{1}{2}} $$
 
 
     [figure: gradient / fore-vector]
@@ -172,15 +172,15 @@ Divergences
 
 There are also two divergences,
 
-  $$ d_{n,m,p} = [\\tilde{\\nabla} \\cdot \\hat{g}]_{n,m,p}
-               = [\\tilde{\\partial}_x g^x]_{m,n,p} +
-                 [\\tilde{\\partial}_y g^y]_{m,n,p} +
-                 [\\tilde{\\partial}_z g^z]_{m,n,p}   $$
+  $$ d_{n,m,p} = [\tilde{\nabla} \cdot \hat{g}]_{n,m,p}
+               = [\tilde{\partial}_x g^x]_{m,n,p} +
+                 [\tilde{\partial}_y g^y]_{m,n,p} +
+                 [\tilde{\partial}_z g^z]_{m,n,p}   $$
 
-  $$ d_{n,m,p} = [\\hat{\\nabla} \\cdot \\tilde{g}]_{n,m,p}
-               = [\\hat{\\partial}_x g^x]_{m,n,p} +
-                 [\\hat{\\partial}_y g^y]_{m,n,p} +
-                 [\\hat{\\partial}_z g^z]_{m,n,p}  $$
+  $$ d_{n,m,p} = [\hat{\nabla} \cdot \tilde{g}]_{n,m,p}
+               = [\hat{\partial}_x g^x]_{m,n,p} +
+                 [\hat{\partial}_y g^y]_{m,n,p} +
+                 [\hat{\partial}_z g^z]_{m,n,p}  $$
 
  or
 
@@ -203,7 +203,7 @@ where `g = [gx, gy, gz]` is a fore- or back-vector field.
 
 Since we applied the forward divergence to the back-vector (and vice-versa), the resulting scalar value
 is defined at the back-vector's (fore-vector's) location $(m,n,p)$ and not at the locations of its components
-$(m \\pm \\frac{1}{2},n,p)$ etc.
+$(m \pm \frac{1}{2},n,p)$ etc.
 
     [figure: divergence]
                                     ^^
@@ -227,23 +227,23 @@ Curls
 
 The two curls are then
 
-  $$ \\begin{aligned}
-     \\hat{h}_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &= \\\\
-     [\\tilde{\\nabla} \\times \\tilde{g}]_{m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2}} &=
-        \\vec{x} (\\tilde{\\partial}_y g^z_{m,n,p + \\frac{1}{2}} - \\tilde{\\partial}_z g^y_{m,n + \\frac{1}{2},p}) \\\\
-     &+ \\vec{y} (\\tilde{\\partial}_z g^x_{m + \\frac{1}{2},n,p} - \\tilde{\\partial}_x g^z_{m,n,p + \\frac{1}{2}}) \\\\
-     &+ \\vec{z} (\\tilde{\\partial}_x g^y_{m,n + \\frac{1}{2},p} - \\tilde{\\partial}_y g^z_{m + \\frac{1}{2},n,p})
-     \\end{aligned} $$
+  $$ \begin{aligned}
+     \hat{h}_{m + \frac{1}{2}, n + \frac{1}{2}, p + \frac{1}{2}} &= \\
+     [\tilde{\nabla} \times \tilde{g}]_{m + \frac{1}{2}, n + \frac{1}{2}, p + \frac{1}{2}} &=
+        \vec{x} (\tilde{\partial}_y g^z_{m,n,p + \frac{1}{2}} - \tilde{\partial}_z g^y_{m,n + \frac{1}{2},p}) \\
+     &+ \vec{y} (\tilde{\partial}_z g^x_{m + \frac{1}{2},n,p} - \tilde{\partial}_x g^z_{m,n,p + \frac{1}{2}}) \\
+     &+ \vec{z} (\tilde{\partial}_x g^y_{m,n + \frac{1}{2},p} - \tilde{\partial}_y g^z_{m + \frac{1}{2},n,p})
+     \end{aligned} $$
 
  and
 
-  $$ \\tilde{h}_{m - \\frac{1}{2}, n - \\frac{1}{2}, p - \\frac{1}{2}} =
-     [\\hat{\\nabla} \\times \\hat{g}]_{m - \\frac{1}{2}, n - \\frac{1}{2}, p - \\frac{1}{2}} $$
+  $$ \tilde{h}_{m - \frac{1}{2}, n - \frac{1}{2}, p - \frac{1}{2}} =
+     [\hat{\nabla} \times \hat{g}]_{m - \frac{1}{2}, n - \frac{1}{2}, p - \frac{1}{2}} $$
 
-  where $\\hat{g}$ and $\\tilde{g}$ are located at $(m,n,p)$
-  with components at $(m \\pm \\frac{1}{2},n,p)$ etc.,
-  while $\\hat{h}$ and $\\tilde{h}$ are located at $(m \\pm \\frac{1}{2}, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})$
-  with components at $(m, n \\pm \\frac{1}{2}, p \\pm \\frac{1}{2})$ etc.
+  where $\hat{g}$ and $\tilde{g}$ are located at $(m,n,p)$
+  with components at $(m \pm \frac{1}{2},n,p)$ etc.,
+  while $\hat{h}$ and $\tilde{h}$ are located at $(m \pm \frac{1}{2}, n \pm \frac{1}{2}, p \pm \frac{1}{2})$
+  with components at $(m, n \pm \frac{1}{2}, p \pm \frac{1}{2})$ etc.
 
 
     [code: curls]
@@ -287,27 +287,27 @@ Maxwell's Equations
 
 If we discretize both space (m,n,p) and time (l), Maxwell's equations become
 
- $$ \\begin{aligned}
-  \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &= -\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}}
-                                                                         - \\hat{M}_{l, \\vec{r} + \\frac{1}{2}}  \\\\
-  \\hat{\\nabla} \\times \\hat{H}_{l-\\frac{1}{2},\\vec{r} + \\frac{1}{2}} &= \\hat{\\partial}_t \\tilde{D}_{l, \\vec{r}}
-                                                                             + \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\
-  \\tilde{\\nabla} \\cdot \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}} &= 0 \\\\
-  \\hat{\\nabla} \\cdot \\tilde{D}_{l,\\vec{r}} &= \\rho_{l,\\vec{r}}
- \\end{aligned} $$
+ $$ \begin{aligned}
+  \tilde{\nabla} \times \tilde{E}_{l,\vec{r}} &= -\tilde{\partial}_t \hat{B}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}}
+                                                                   - \hat{M}_{l, \vec{r} + \frac{1}{2}}  \\
+  \hat{\nabla} \times \hat{H}_{l-\frac{1}{2},\vec{r} + \frac{1}{2}} &= \hat{\partial}_t \tilde{D}_{l, \vec{r}}
+                                                                   + \tilde{J}_{l-\frac{1}{2},\vec{r}} \\
+  \tilde{\nabla} \cdot \hat{B}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} &= 0 \\
+  \hat{\nabla} \cdot \tilde{D}_{l,\vec{r}} &= \rho_{l,\vec{r}}
+ \end{aligned} $$
 
  with
 
- $$ \\begin{aligned}
-  \\hat{B}_{\\vec{r}} &= \\mu_{\\vec{r} + \\frac{1}{2}} \\cdot \\hat{H}_{\\vec{r} + \\frac{1}{2}} \\\\
-  \\tilde{D}_{\\vec{r}} &= \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}}
- \\end{aligned} $$
+ $$ \begin{aligned}
+  \hat{B}_{\vec{r}} &= \mu_{\vec{r} + \frac{1}{2}} \cdot \hat{H}_{\vec{r} + \frac{1}{2}} \\
+  \tilde{D}_{\vec{r}} &= \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}}
+ \end{aligned} $$
 
-where the spatial subscripts are abbreviated as $\\vec{r} = (m, n, p)$ and
-$\\vec{r} + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2}, p + \\frac{1}{2})$,
-$\\tilde{E}$ and $\\hat{H}$ are the electric and magnetic fields,
-$\\tilde{J}$ and $\\hat{M}$ are the electric and magnetic current distributions,
-and $\\epsilon$ and $\\mu$ are the dielectric permittivity and magnetic permeability.
+where the spatial subscripts are abbreviated as $\vec{r} = (m, n, p)$ and
+$\vec{r} + \frac{1}{2} = (m + \frac{1}{2}, n + \frac{1}{2}, p + \frac{1}{2})$,
+$\tilde{E}$ and $\hat{H}$ are the electric and magnetic fields,
+$\tilde{J}$ and $\hat{M}$ are the electric and magnetic current distributions,
+and $\epsilon$ and $\mu$ are the dielectric permittivity and magnetic permeability.
 
 The above is Yee's algorithm, written in a form analogous to Maxwell's equations.
 The time derivatives can be expanded to form the update equations:
@@ -369,34 +369,34 @@ Each component forms its own grid, offset from the others:
 
 The divergence equations can be derived by taking the divergence of the curl equations
 and combining them with charge continuity,
- $$ \\hat{\\nabla} \\cdot \\tilde{J} + \\hat{\\partial}_t \\rho = 0 $$
+ $$ \hat{\nabla} \cdot \tilde{J} + \hat{\partial}_t \rho = 0 $$
  implying that the discrete Maxwell's equations do not produce spurious charges.
 
 
 Wave equation
 -------------
 
-Taking the backward curl of the $\\tilde{\\nabla} \\times \\tilde{E}$ equation and
-replacing the resulting $\\hat{\\nabla} \\times \\hat{H}$ term using its respective equation,
-and setting $\\hat{M}$ to zero, we can form the discrete wave equation:
+Taking the backward curl of the $\tilde{\nabla} \times \tilde{E}$ equation and
+replacing the resulting $\hat{\nabla} \times \hat{H}$ term using its respective equation,
+and setting $\hat{M}$ to zero, we can form the discrete wave equation:
 
 $$
-  \\begin{aligned}
-  \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &=
-      -\\tilde{\\partial}_t \\hat{B}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}}
-                          - \\hat{M}_{l-1, \\vec{r} + \\frac{1}{2}}  \\\\
-  \\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}} &=
-    -\\tilde{\\partial}_t \\hat{H}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}}  \\\\
-  \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &=
-    \\hat{\\nabla} \\times (-\\tilde{\\partial}_t \\hat{H}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}})  \\\\
-  \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &=
-    -\\tilde{\\partial}_t \\hat{\\nabla} \\times \\hat{H}_{l-\\frac{1}{2}, \\vec{r} + \\frac{1}{2}}  \\\\
-  \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}}) &=
-    -\\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_{\\vec{r}} \\tilde{E}_{l, \\vec{r}} + \\hat{\\partial}_t \\tilde{J}_{l-\\frac{1}{2},\\vec{r}} \\\\
-  \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l,\\vec{r}})
-            + \\tilde{\\partial}_t \\hat{\\partial}_t \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{l, \\vec{r}}
-            &= \\tilde{\\partial}_t \\tilde{J}_{l - \\frac{1}{2}, \\vec{r}}
-  \\end{aligned}
+  \begin{aligned}
+  \tilde{\nabla} \times \tilde{E}_{l,\vec{r}} &=
+     -\tilde{\partial}_t \hat{B}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}}
+                       - \hat{M}_{l-1, \vec{r} + \frac{1}{2}}  \\
+  \mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}} &=
+   -\tilde{\partial}_t \hat{H}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}}  \\
+  \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &=
+   \hat{\nabla} \times (-\tilde{\partial}_t \hat{H}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}})  \\
+  \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &=
+   -\tilde{\partial}_t \hat{\nabla} \times \hat{H}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}}  \\
+  \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &=
+   -\tilde{\partial}_t \hat{\partial}_t \epsilon_{\vec{r}} \tilde{E}_{l, \vec{r}} + \hat{\partial}_t \tilde{J}_{l-\frac{1}{2},\vec{r}} \\
+  \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}})
+           + \tilde{\partial}_t \hat{\partial}_t \epsilon_{\vec{r}} \cdot \tilde{E}_{l, \vec{r}}
+           &= \tilde{\partial}_t \tilde{J}_{l - \frac{1}{2}, \vec{r}}
+  \end{aligned}
 $$
 
 
@@ -406,27 +406,27 @@ Frequency domain
 We can substitute in a time-harmonic fields
 
 $$
- \\begin{aligned}
- \\tilde{E}_{l, \\vec{r}} &= \\tilde{E}_{\\vec{r}} e^{-\\imath \\omega l \\Delta_t} \\\\
- \\tilde{J}_{l, \\vec{r}} &= \\tilde{J}_{\\vec{r}} e^{-\\imath \\omega (l - \\frac{1}{2}) \\Delta_t}
- \\end{aligned}
+ \begin{aligned}
+ \tilde{E}_{l, \vec{r}} &= \tilde{E}_{\vec{r}} e^{-\imath \omega l \Delta_t} \\
+ \tilde{J}_{l, \vec{r}} &= \tilde{J}_{\vec{r}} e^{-\imath \omega (l - \frac{1}{2}) \Delta_t}
+ \end{aligned}
 $$
 
 resulting in
 
 $$
- \\begin{aligned}
- \\tilde{\\partial}_t &\\Rightarrow (e^{ \\imath \\omega \\Delta_t} - 1) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_t} \\sin(\\omega \\Delta_t / 2) e^{-\\imath \\omega \\Delta_t / 2} = -\\imath \\Omega e^{-\\imath \\omega \\Delta_t / 2}\\\\
-   \\hat{\\partial}_t &\\Rightarrow (1 - e^{-\\imath \\omega \\Delta_t}) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_t} \\sin(\\omega \\Delta_t / 2) e^{ \\imath \\omega \\Delta_t / 2} = -\\imath \\Omega e^{ \\imath \\omega \\Delta_t / 2}\\\\
- \\Omega &= 2 \\sin(\\omega \\Delta_t / 2) / \\Delta_t
- \\end{aligned}
+ \begin{aligned}
+ \tilde{\partial}_t &\Rightarrow (e^{ \imath \omega \Delta_t} - 1) / \Delta_t = \frac{-2 \imath}{\Delta_t} \sin(\omega \Delta_t / 2) e^{-\imath \omega \Delta_t / 2} = -\imath \Omega e^{-\imath \omega \Delta_t / 2}\\
+   \hat{\partial}_t &\Rightarrow (1 - e^{-\imath \omega \Delta_t}) / \Delta_t = \frac{-2 \imath}{\Delta_t} \sin(\omega \Delta_t / 2) e^{ \imath \omega \Delta_t / 2} = -\imath \Omega e^{ \imath \omega \Delta_t / 2}\\
+ \Omega &= 2 \sin(\omega \Delta_t / 2) / \Delta_t
+ \end{aligned}
 $$
 
 This gives the frequency-domain wave equation,
 
 $$
- \\hat{\\nabla} \\times (\\mu^{-1}_{\\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}})
-    -\\Omega^2 \\epsilon_{\\vec{r}} \\cdot \\tilde{E}_{\\vec{r}} = -\\imath \\Omega \\tilde{J}_{\\vec{r}} e^{\\imath \\omega \\Delta_t / 2} \\\\
+ \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{\vec{r}})
+    -\Omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath \Omega \tilde{J}_{\vec{r}} e^{\imath \omega \Delta_t / 2} \\
 $$
 
 
@@ -436,48 +436,48 @@ Plane waves and Dispersion relation
 With uniform material distribution and no sources
 
 $$
- \\begin{aligned}
- \\mu_{\\vec{r} + \\frac{1}{2}} &= \\mu \\\\
- \\epsilon_{\\vec{r}} &= \\epsilon \\\\
- \\tilde{J}_{\\vec{r}} &= 0 \\\\
- \\end{aligned}
+ \begin{aligned}
+ \mu_{\vec{r} + \frac{1}{2}} &= \mu \\
+ \epsilon_{\vec{r}} &= \epsilon \\
+ \tilde{J}_{\vec{r}} &= 0 \\
+ \end{aligned}
 $$
 
 the frequency domain wave equation simplifies to
 
-$$ \\hat{\\nabla} \\times \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}} - \\Omega^2 \\epsilon \\mu \\tilde{E}_{\\vec{r}} = 0 $$
+$$ \hat{\nabla} \times \tilde{\nabla} \times \tilde{E}_{\vec{r}} - \Omega^2 \epsilon \mu \tilde{E}_{\vec{r}} = 0 $$
 
-Since $\\hat{\\nabla} \\cdot \\tilde{E}_{\\vec{r}} = 0$, we can simplify
+Since $\hat{\nabla} \cdot \tilde{E}_{\vec{r}} = 0$, we can simplify
 
 $$
- \\begin{aligned}
- \\hat{\\nabla} \\times \\tilde{\\nabla} \\times \\tilde{E}_{\\vec{r}}
-   &= \\tilde{\\nabla}(\\hat{\\nabla} \\cdot \\tilde{E}_{\\vec{r}}) - \\hat{\\nabla} \\cdot \\tilde{\\nabla} \\tilde{E}_{\\vec{r}} \\\\
-   &= - \\hat{\\nabla} \\cdot \\tilde{\\nabla} \\tilde{E}_{\\vec{r}} \\\\
-   &= - \\tilde{\\nabla}^2 \\tilde{E}_{\\vec{r}}
- \\end{aligned}
+ \begin{aligned}
+ \hat{\nabla} \times \tilde{\nabla} \times \tilde{E}_{\vec{r}}
+  &= \tilde{\nabla}(\hat{\nabla} \cdot \tilde{E}_{\vec{r}}) - \hat{\nabla} \cdot \tilde{\nabla} \tilde{E}_{\vec{r}} \\
+  &= - \hat{\nabla} \cdot \tilde{\nabla} \tilde{E}_{\vec{r}} \\
+  &= - \tilde{\nabla}^2 \tilde{E}_{\vec{r}}
+ \end{aligned}
 $$
 
 and we get
 
-$$  \\tilde{\\nabla}^2 \\tilde{E}_{\\vec{r}} + \\Omega^2 \\epsilon \\mu \\tilde{E}_{\\vec{r}} = 0 $$
+$$  \tilde{\nabla}^2 \tilde{E}_{\vec{r}} + \Omega^2 \epsilon \mu \tilde{E}_{\vec{r}} = 0 $$
 
 We can convert this to three scalar-wave equations of the form
 
-$$ (\\tilde{\\nabla}^2 + K^2) \\phi_{\\vec{r}} = 0 $$
+$$ (\tilde{\nabla}^2 + K^2) \phi_{\vec{r}} = 0 $$
 
-with $K^2 = \\Omega^2 \\mu \\epsilon$. Now we let
+with $K^2 = \Omega^2 \mu \epsilon$. Now we let
 
-$$  \\phi_{\\vec{r}} = A e^{\\imath (k_x m \\Delta_x + k_y n \\Delta_y + k_z p \\Delta_z)}  $$
+$$  \phi_{\vec{r}} = A e^{\imath (k_x m \Delta_x + k_y n \Delta_y + k_z p \Delta_z)}  $$
 
 resulting in
 
 $$
- \\begin{aligned}
- \\tilde{\\partial}_x &\\Rightarrow (e^{ \\imath k_x \\Delta_x} - 1) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_x} \\sin(k_x \\Delta_x / 2) e^{ \\imath k_x \\Delta_x / 2} = \\imath K_x e^{ \\imath k_x \\Delta_x / 2}\\\\
-   \\hat{\\partial}_x &\\Rightarrow (1 - e^{-\\imath k_x \\Delta_x}) / \\Delta_t = \\frac{-2 \\imath}{\\Delta_x} \\sin(k_x \\Delta_x / 2) e^{-\\imath k_x \\Delta_x / 2} = \\imath K_x e^{-\\imath k_x \\Delta_x / 2}\\\\
- K_x &= 2 \\sin(k_x \\Delta_x / 2) / \\Delta_x \\\\
- \\end{aligned}
+ \begin{aligned}
+ \tilde{\partial}_x &\Rightarrow (e^{ \imath k_x \Delta_x} - 1) / \Delta_t = \frac{-2 \imath}{\Delta_x} \sin(k_x \Delta_x / 2) e^{ \imath k_x \Delta_x / 2} = \imath K_x e^{ \imath k_x \Delta_x / 2}\\
+   \hat{\partial}_x &\Rightarrow (1 - e^{-\imath k_x \Delta_x}) / \Delta_t = \frac{-2 \imath}{\Delta_x} \sin(k_x \Delta_x / 2) e^{-\imath k_x \Delta_x / 2} = \imath K_x e^{-\imath k_x \Delta_x / 2}\\
+ K_x &= 2 \sin(k_x \Delta_x / 2) / \Delta_x \\
+ \end{aligned}
 $$
 
 with similar expressions for the y and z dimnsions (and $K_y, K_z$).
@@ -485,20 +485,20 @@ with similar expressions for the y and z dimnsions (and $K_y, K_z$).
 This implies
 
 $$
-  \\tilde{\\nabla}^2 = -(K_x^2 + K_y^2 + K_z^2) \\phi_{\\vec{r}} \\\\
-  K_x^2 + K_y^2 + K_z^2 = \\Omega^2 \\mu \\epsilon = \\Omega^2 / c^2
+  \tilde{\nabla}^2 = -(K_x^2 + K_y^2 + K_z^2) \phi_{\vec{r}} \\
+  K_x^2 + K_y^2 + K_z^2 = \Omega^2 \mu \epsilon = \Omega^2 / c^2
 $$
 
-where $c = \\sqrt{\\mu \\epsilon}$.
+where $c = \sqrt{\mu \epsilon}$.
 
-Assuming real $(k_x, k_y, k_z), \\omega$ will be real only if
+Assuming real $(k_x, k_y, k_z), \omega$ will be real only if
 
-$$ c^2 \\Delta_t^2 = \\frac{\\Delta_t^2}{\\mu \\epsilon} < 1/(\\frac{1}{\\Delta_x^2} + \\frac{1}{\\Delta_y^2} + \\frac{1}{\\Delta_z^2}) $$
+$$ c^2 \Delta_t^2 = \frac{\Delta_t^2}{\mu \epsilon} < 1/(\frac{1}{\Delta_x^2} + \frac{1}{\Delta_y^2} + \frac{1}{\Delta_z^2}) $$
 
-If $\\Delta_x = \\Delta_y = \\Delta_z$, this simplifies to $c \\Delta_t < \\Delta_x / \\sqrt{3}$.
+If $\Delta_x = \Delta_y = \Delta_z$, this simplifies to $c \Delta_t < \Delta_x / \sqrt{3}$.
 This last form can be interpreted as enforcing causality; the distance that light
-travels in one timestep (i.e., $c \\Delta_t$) must be less than the diagonal
-of the smallest cell ( $\\Delta_x / \\sqrt{3}$ when on a uniform cubic grid).
+travels in one timestep (i.e., $c \Delta_t$) must be less than the diagonal
+of the smallest cell ( $\Delta_x / \sqrt{3}$ when on a uniform cubic grid).
 
 
 Grid description
@@ -515,8 +515,8 @@ to make the illustration simpler; we need at least two cells in the x dimension
 demonstrate how nonuniform `dx` affects the various components.
 
 Place the E fore-vectors at integer indices $r = (m, n, p)$ and the H back-vectors
-at fractional indices $r + \\frac{1}{2} = (m + \\frac{1}{2}, n + \\frac{1}{2},
-p + \\frac{1}{2})$. Remember that these are indices and not coordinates; they can
+at fractional indices $r + \frac{1}{2} = (m + \frac{1}{2}, n + \frac{1}{2},
+p + \frac{1}{2})$. Remember that these are indices and not coordinates; they can
 correspond to arbitrary (monotonically increasing) coordinates depending on the cell widths.
 
 Draw lines to denote the planes on which the H components and back-vectors are defined.
@@ -718,14 +718,14 @@ composed of the three diagonal tensor components:
 or
 
 $$
- \\epsilon = \\begin{bmatrix} \\epsilon_{xx} & 0 & 0 \\\\
-                              0 & \\epsilon_{yy} & 0 \\\\
-                              0 & 0 & \\epsilon_{zz} \\end{bmatrix}
+ \epsilon = \begin{bmatrix} \epsilon_{xx} & 0 & 0 \\
+                            0 & \epsilon_{yy} & 0 \\
+                            0 & 0 & \epsilon_{zz} \end{bmatrix}
 $$
 $$
- \\mu = \\begin{bmatrix} \\mu_{xx} & 0 & 0 \\\\
-                         0 & \\mu_{yy} & 0 \\\\
-                         0 & 0 & \\mu_{zz} \\end{bmatrix}
+ \mu = \begin{bmatrix} \mu_{xx} & 0 & 0 \\
+                         0 & \mu_{yy} & 0 \\
+                         0 & 0 & \mu_{zz} \end{bmatrix}
 $$
 
 where the off-diagonal terms (e.g. `epsilon_xy`) are assumed to be zero.
diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py
index e33aa93..3a10a00 100644
--- a/meanas/fdmath/functional.py
+++ b/meanas/fdmath/functional.py
@@ -62,7 +62,7 @@ def deriv_back(
 def curl_forward(
         dx_e: Sequence[NDArray[numpy.float_]] | None = None,
         ) -> fdfield_updater_t:
-    """
+    r"""
     Curl operator for use with the E field.
 
     Args:
@@ -71,7 +71,7 @@ def curl_forward(
 
     Returns:
         Function `f` for taking the discrete forward curl of a field,
-        `f(E)` -> curlE $= \\nabla_f \\times E$
+        `f(E)` -> curlE $= \nabla_f \times E$
     """
     Dx, Dy, Dz = deriv_forward(dx_e)
 
@@ -91,7 +91,7 @@ def curl_forward(
 def curl_back(
         dx_h: Sequence[NDArray[numpy.float_]] | None = None,
         ) -> fdfield_updater_t:
-    """
+    r"""
     Create a function which takes the backward curl of a field.
 
     Args:
@@ -100,7 +100,7 @@ def curl_back(
 
     Returns:
         Function `f` for taking the discrete backward curl of a field,
-        `f(H)` -> curlH $= \\nabla_b \\times H$
+        `f(H)` -> curlH $= \nabla_b \times H$
     """
     Dx, Dy, Dz = deriv_back(dx_h)
 
diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py
index 63be295..171c4f4 100644
--- a/meanas/fdtd/__init__.py
+++ b/meanas/fdtd/__init__.py
@@ -1,4 +1,4 @@
-"""
+r"""
 Utilities for running finite-difference time-domain (FDTD) simulations
 
 See the discussion of `Maxwell's Equations` in `meanas.fdmath` for basic
@@ -11,9 +11,9 @@ Timestep
 From the discussion of "Plane waves and the Dispersion relation" in `meanas.fdmath`,
 we have
 
-$$ c^2 \\Delta_t^2 = \\frac{\\Delta_t^2}{\\mu \\epsilon} < 1/(\\frac{1}{\\Delta_x^2} + \\frac{1}{\\Delta_y^2} + \\frac{1}{\\Delta_z^2}) $$
+$$ c^2 \Delta_t^2 = \frac{\Delta_t^2}{\mu \epsilon} < 1/(\frac{1}{\Delta_x^2} + \frac{1}{\Delta_y^2} + \frac{1}{\Delta_z^2}) $$
 
-or, if $\\Delta_x = \\Delta_y = \\Delta_z$, then $c \\Delta_t < \\frac{\\Delta_x}{\\sqrt{3}}$.
+or, if $\Delta_x = \Delta_y = \Delta_z$, then $c \Delta_t < \frac{\Delta_x}{\sqrt{3}}$.
 
 Based on this, we can set
 
@@ -27,81 +27,81 @@ Poynting Vector and Energy Conservation
 
 Let
 
-$$ \\begin{aligned}
-  \\tilde{S}_{l, l', \\vec{r}} &=& &\\tilde{E}_{l, \\vec{r}} \\otimes \\hat{H}_{l', \\vec{r} + \\frac{1}{2}}  \\\\
-  &=&  &\\vec{x} (\\tilde{E}^y_{l,m+1,n,p} \\hat{H}^z_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^z_{l,m+1,n,p} \\hat{H}^y_{l', \\vec{r} + \\frac{1}{2}}) \\\\
-  & &+ &\\vec{y} (\\tilde{E}^z_{l,m,n+1,p} \\hat{H}^x_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^x_{l,m,n+1,p} \\hat{H}^z_{l', \\vec{r} + \\frac{1}{2}}) \\\\
-  & &+ &\\vec{z} (\\tilde{E}^x_{l,m,n,p+1} \\hat{H}^y_{l',\\vec{r} + \\frac{1}{2}} - \\tilde{E}^y_{l,m,n,p+1} \\hat{H}^z_{l', \\vec{r} + \\frac{1}{2}})
-   \\end{aligned}
+$$ \begin{aligned}
+  \tilde{S}_{l, l', \vec{r}} &=& &\tilde{E}_{l, \vec{r}} \otimes \hat{H}_{l', \vec{r} + \frac{1}{2}}  \\
+  &=&  &\vec{x} (\tilde{E}^y_{l,m+1,n,p} \hat{H}^z_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^z_{l,m+1,n,p} \hat{H}^y_{l', \vec{r} + \frac{1}{2}}) \\
+  & &+ &\vec{y} (\tilde{E}^z_{l,m,n+1,p} \hat{H}^x_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^x_{l,m,n+1,p} \hat{H}^z_{l', \vec{r} + \frac{1}{2}}) \\
+  & &+ &\vec{z} (\tilde{E}^x_{l,m,n,p+1} \hat{H}^y_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^y_{l,m,n,p+1} \hat{H}^z_{l', \vec{r} + \frac{1}{2}})
+   \end{aligned}
 $$
 
-where $\\vec{r} = (m, n, p)$ and $\\otimes$ is a modified cross product
-in which the $\\tilde{E}$ terms are shifted as indicated.
+where $\vec{r} = (m, n, p)$ and $\otimes$ is a modified cross product
+in which the $\tilde{E}$ terms are shifted as indicated.
 
 By taking the divergence and rearranging terms, we can show that
 
 $$
-  \\begin{aligned}
-  \\hat{\\nabla} \\cdot \\tilde{S}_{l, l', \\vec{r}}
-   &= \\hat{\\nabla} \\cdot (\\tilde{E}_{l, \\vec{r}} \\otimes \\hat{H}_{l', \\vec{r} + \\frac{1}{2}})  \\\\
-   &= \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\cdot \\tilde{\\nabla} \\times \\tilde{E}_{l, \\vec{r}} -
-      \\tilde{E}_{l, \\vec{r}} \\cdot \\hat{\\nabla} \\times \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\\\
-   &= \\hat{H}_{l', \\vec{r} + \\frac{1}{2}} \\cdot
-            (-\\tilde{\\partial}_t \\mu_{\\vec{r} + \\frac{1}{2}} \\hat{H}_{l - \\frac{1}{2}, \\vec{r} + \\frac{1}{2}} -
-                \\hat{M}_{l, \\vec{r} + \\frac{1}{2}}) -
-      \\tilde{E}_{l, \\vec{r}} \\cdot (\\hat{\\partial}_t \\tilde{\\epsilon}_{\\vec{r}} \\tilde{E}_{l'+\\frac{1}{2}, \\vec{r}} +
-                \\tilde{J}_{l', \\vec{r}}) \\\\
-   &= \\hat{H}_{l'} \\cdot (-\\mu / \\Delta_t)(\\hat{H}_{l + \\frac{1}{2}} - \\hat{H}_{l - \\frac{1}{2}}) -
-      \\tilde{E}_l \\cdot (\\epsilon / \\Delta_t )(\\tilde{E}_{l'+\\frac{1}{2}} - \\tilde{E}_{l'-\\frac{1}{2}})
-      - \\hat{H}_{l'} \\cdot \\hat{M}_{l} - \\tilde{E}_l \\cdot \\tilde{J}_{l'} \\\\
-  \\end{aligned}
+  \begin{aligned}
+  \hat{\nabla} \cdot \tilde{S}_{l, l', \vec{r}}
+   &= \hat{\nabla} \cdot (\tilde{E}_{l, \vec{r}} \otimes \hat{H}_{l', \vec{r} + \frac{1}{2}})  \\
+   &= \hat{H}_{l', \vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l, \vec{r}} -
+      \tilde{E}_{l, \vec{r}} \cdot \hat{\nabla} \times \hat{H}_{l', \vec{r} + \frac{1}{2}} \\
+   &= \hat{H}_{l', \vec{r} + \frac{1}{2}} \cdot
+          (-\tilde{\partial}_t \mu_{\vec{r} + \frac{1}{2}} \hat{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} -
+              \hat{M}_{l, \vec{r} + \frac{1}{2}}) -
+      \tilde{E}_{l, \vec{r}} \cdot (\hat{\partial}_t \tilde{\epsilon}_{\vec{r}} \tilde{E}_{l'+\frac{1}{2}, \vec{r}} +
+              \tilde{J}_{l', \vec{r}}) \\
+   &= \hat{H}_{l'} \cdot (-\mu / \Delta_t)(\hat{H}_{l + \frac{1}{2}} - \hat{H}_{l - \frac{1}{2}}) -
+      \tilde{E}_l \cdot (\epsilon / \Delta_t )(\tilde{E}_{l'+\frac{1}{2}} - \tilde{E}_{l'-\frac{1}{2}})
+      - \hat{H}_{l'} \cdot \hat{M}_{l} - \tilde{E}_l \cdot \tilde{J}_{l'} \\
+  \end{aligned}
 $$
 
 where in the last line the spatial subscripts have been dropped to emphasize
 the time subscripts $l, l'$, i.e.
 
 $$
-  \\begin{aligned}
-  \\tilde{E}_l &= \\tilde{E}_{l, \\vec{r}} \\\\
-  \\hat{H}_l &= \\tilde{H}_{l, \\vec{r} + \\frac{1}{2}}  \\\\
-  \\tilde{\\epsilon} &= \\tilde{\\epsilon}_{\\vec{r}}  \\\\
-  \\end{aligned}
+  \begin{aligned}
+  \tilde{E}_l &= \tilde{E}_{l, \vec{r}} \\
+  \hat{H}_l &= \tilde{H}_{l, \vec{r} + \frac{1}{2}}  \\
+  \tilde{\epsilon} &= \tilde{\epsilon}_{\vec{r}}  \\
+  \end{aligned}
 $$
 
 etc.
-For $l' = l + \\frac{1}{2}$ we get
+For $l' = l + \frac{1}{2}$ we get
 
 $$
-  \\begin{aligned}
-  \\hat{\\nabla} \\cdot \\tilde{S}_{l, l + \\frac{1}{2}}
-   &= \\hat{H}_{l + \\frac{1}{2}} \\cdot
-            (-\\mu / \\Delta_t)(\\hat{H}_{l + \\frac{1}{2}} - \\hat{H}_{l - \\frac{1}{2}}) -
-      \\tilde{E}_l \\cdot (\\epsilon / \\Delta_t)(\\tilde{E}_{l+1} - \\tilde{E}_l)
-      - \\hat{H}_{l'} \\cdot \\hat{M}_l - \\tilde{E}_l \\cdot \\tilde{J}_{l + \\frac{1}{2}} \\\\
-   &= (-\\mu / \\Delta_t)(\\hat{H}^2_{l + \\frac{1}{2}} - \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}}) -
-      (\\epsilon / \\Delta_t)(\\tilde{E}_{l+1} \\cdot \\tilde{E}_l - \\tilde{E}^2_l)
-      - \\hat{H}_{l'} \\cdot \\hat{M}_l - \\tilde{E}_l \\cdot \\tilde{J}_{l + \\frac{1}{2}} \\\\
-   &= -(\\mu \\hat{H}^2_{l + \\frac{1}{2}}
-       +\\epsilon \\tilde{E}_{l+1} \\cdot \\tilde{E}_l) / \\Delta_t \\ \\
-      +(\\mu \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}}
-       +\\epsilon \\tilde{E}^2_l) / \\Delta_t \\ \\
-      - \\hat{H}_{l+\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\
-      - \\tilde{E}_l \\cdot \\tilde{J}_{l+\\frac{1}{2}} \\\\
-  \\end{aligned}
+  \begin{aligned}
+  \hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}}
+   &= \hat{H}_{l + \frac{1}{2}} \cdot
+            (-\mu / \Delta_t)(\hat{H}_{l + \frac{1}{2}} - \hat{H}_{l - \frac{1}{2}}) -
+      \tilde{E}_l \cdot (\epsilon / \Delta_t)(\tilde{E}_{l+1} - \tilde{E}_l)
+      - \hat{H}_{l'} \cdot \hat{M}_l - \tilde{E}_l \cdot \tilde{J}_{l + \frac{1}{2}} \\
+   &= (-\mu / \Delta_t)(\hat{H}^2_{l + \frac{1}{2}} - \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}}) -
+      (\epsilon / \Delta_t)(\tilde{E}_{l+1} \cdot \tilde{E}_l - \tilde{E}^2_l)
+      - \hat{H}_{l'} \cdot \hat{M}_l - \tilde{E}_l \cdot \tilde{J}_{l + \frac{1}{2}} \\
+   &= -(\mu \hat{H}^2_{l + \frac{1}{2}}
+       +\epsilon \tilde{E}_{l+1} \cdot \tilde{E}_l) / \Delta_t \\
+      +(\mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}}
+       +\epsilon \tilde{E}^2_l) / \Delta_t \\
+      - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\
+      - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\
+  \end{aligned}
 $$
 
-and for $l' = l - \\frac{1}{2}$,
+and for $l' = l - \frac{1}{2}$,
 
 $$
-  \\begin{aligned}
-  \\hat{\\nabla} \\cdot \\tilde{S}_{l, l - \\frac{1}{2}}
-   &=  (\\mu \\hat{H}^2_{l - \\frac{1}{2}}
-       +\\epsilon \\tilde{E}_{l-1} \\cdot \\tilde{E}_l) / \\Delta_t \\ \\
-      -(\\mu \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}}
-       +\\epsilon \\tilde{E}^2_l) / \\Delta_t \\ \\
-      - \\hat{H}_{l-\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\
-      - \\tilde{E}_l \\cdot \\tilde{J}_{l-\\frac{1}{2}} \\\\
-  \\end{aligned}
+  \begin{aligned}
+  \hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}}
+   &=  (\mu \hat{H}^2_{l - \frac{1}{2}}
+       +\epsilon \tilde{E}_{l-1} \cdot \tilde{E}_l) / \Delta_t \\
+      -(\mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}}
+       +\epsilon \tilde{E}^2_l) / \Delta_t \\
+      - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\
+      - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\
+  \end{aligned}
 $$
 
 These two results form the discrete time-domain analogue to Poynting's theorem.
@@ -109,25 +109,25 @@ They hint at the expressions for the energy, which can be calculated at the same
 time-index as either the E or H field:
 
 $$
- \\begin{aligned}
- U_l &= \\epsilon \\tilde{E}^2_l + \\mu \\hat{H}_{l + \\frac{1}{2}} \\cdot \\hat{H}_{l - \\frac{1}{2}} \\\\
- U_{l + \\frac{1}{2}} &= \\epsilon \\tilde{E}_l \\cdot \\tilde{E}_{l + 1} + \\mu \\hat{H}^2_{l + \\frac{1}{2}} \\\\
- \\end{aligned}
+ \begin{aligned}
+ U_l &= \epsilon \tilde{E}^2_l + \mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} \\
+ U_{l + \frac{1}{2}} &= \epsilon \tilde{E}_l \cdot \tilde{E}_{l + 1} + \mu \hat{H}^2_{l + \frac{1}{2}} \\
+ \end{aligned}
 $$
 
 Rewriting the Poynting theorem in terms of the energy expressions,
 
 $$
-  \\begin{aligned}
-  (U_{l+\\frac{1}{2}} - U_l) / \\Delta_t
-   &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l + \\frac{1}{2}} \\ \\
-      - \\hat{H}_{l+\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\
-      - \\tilde{E}_l \\cdot \\tilde{J}_{l+\\frac{1}{2}} \\\\
-  (U_l - U_{l-\\frac{1}{2}}) / \\Delta_t
-   &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l - \\frac{1}{2}} \\ \\
-      - \\hat{H}_{l-\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\
-      - \\tilde{E}_l \\cdot \\tilde{J}_{l-\\frac{1}{2}} \\\\
- \\end{aligned}
+  \begin{aligned}
+  (U_{l+\frac{1}{2}} - U_l) / \Delta_t
+   &= -\hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} \\
+      - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\
+      - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\
+  (U_l - U_{l-\frac{1}{2}}) / \Delta_t
+   &= -\hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} \\
+      - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\
+      - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\
+ \end{aligned}
 $$
 
 This result is exact and should practically hold to within numerical precision. No time-
@@ -147,10 +147,10 @@ of the time-domain fields.
 The Ricker wavelet (normalized second derivative of a Gaussian) is commonly used for the pulse
 shape. It can be written
 
-$$ f_r(t) = (1 - \\frac{1}{2} (\\omega (t - \\tau))^2) e^{-(\\frac{\\omega (t - \\tau)}{2})^2} $$
+$$ f_r(t) = (1 - \frac{1}{2} (\omega (t - \tau))^2) e^{-(\frac{\omega (t - \tau)}{2})^2} $$
 
-with $\\tau > \\frac{2 * \\pi}{\\omega}$ as a minimum delay to avoid a discontinuity at
-t=0 (assuming the source is off for t<0 this gives $\\sim 10^{-3}$ error at t=0).
+with $\tau > \frac{2 * \pi}{\omega}$ as a minimum delay to avoid a discontinuity at
+t=0 (assuming the source is off for t<0 this gives $\sim 10^{-3}$ error at t=0).
 
 
 
diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py
index 75938f3..43ea3a1 100644
--- a/meanas/fdtd/energy.py
+++ b/meanas/fdtd/energy.py
@@ -12,7 +12,7 @@ def poynting(
         h: fdfield_t,
         dxes: dx_lists_t | None = None,
         ) -> fdfield_t:
-    """
+    r"""
     Calculate the poynting vector `S` ($S$).
 
     This is the energy transfer rate (amount of energy `U` per `dt` transferred
@@ -44,16 +44,16 @@ def poynting(
 
     The full relationship is
     $$
-      \\begin{aligned}
-      (U_{l+\\frac{1}{2}} - U_l) / \\Delta_t
-       &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l + \\frac{1}{2}} \\ \\
-          - \\hat{H}_{l+\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\
-          - \\tilde{E}_l \\cdot \\tilde{J}_{l+\\frac{1}{2}} \\\\
-      (U_l - U_{l-\\frac{1}{2}}) / \\Delta_t
-       &= -\\hat{\\nabla} \\cdot \\tilde{S}_{l, l - \\frac{1}{2}} \\ \\
-          - \\hat{H}_{l-\\frac{1}{2}} \\cdot \\hat{M}_l \\ \\
-          - \\tilde{E}_l \\cdot \\tilde{J}_{l-\\frac{1}{2}} \\\\
-     \\end{aligned}
+      \begin{aligned}
+      (U_{l+\frac{1}{2}} - U_l) / \Delta_t
+       &= -\hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} \\
+          - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\
+          - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\
+      (U_l - U_{l-\frac{1}{2}}) / \Delta_t
+       &= -\hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} \\
+          - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\
+          - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\
+     \end{aligned}
     $$
 
     These equalities are exact and should practically hold to within numerical precision.

From ccfd4fbf04ac40eadc97527bbeef3b9254d27cbe Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 15 Jul 2024 16:32:48 -0700
Subject: [PATCH 322/437] use parentheses instead of backslash

---
 meanas/fdfd/waveguide_cyl.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 6b3a160..0d3be0b 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -73,8 +73,10 @@ def cylindrical_operator(
 
     omega2 = omega * omega
 
-    op = (omega2 * diag((Tx, Ty)) + pa) @ diag((a0, a1)) + \
+    op = (
+        (omega2 * diag((Tx, Ty)) + pa) @ diag((a0, a1))
         - (sparse.bmat(((None, Ty), (Tx, None))) + pb / omega2) @ diag((b0, b1))
+        )
     return op
 
 

From 639f88bba8057f24ea342442f890c309759990c6 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Wed, 17 Jul 2024 22:56:48 -0700
Subject: [PATCH 323/437] add sensitivity calculation

---
 meanas/fdfd/waveguide_2d.py | 107 +++++++++++++++++++++++++++++++++++-
 1 file changed, 106 insertions(+), 1 deletion(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index eaae21c..cfda1af 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -185,7 +185,7 @@ from numpy.linalg import norm
 import scipy.sparse as sparse       # type: ignore
 
 from ..fdmath.operators import deriv_forward, deriv_back, cross
-from ..fdmath import unvec, dx_lists_t, vfdfield_t, vcfdfield_t
+from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t
 from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
 
 
@@ -718,6 +718,111 @@ def e_err(
     return float(norm(op) / norm(e))
 
 
+def sensitivity(
+        e_norm: vcfdfield_t,
+        h_norm: vcfdfield_t,
+        wavenumber: complex,
+        omega: complex,
+        dxes: dx_lists_t,
+        epsilon: vfdfield_t,
+        mu: vfdfield_t | None = None,
+        ) -> vcfdfield_t:
+    r"""
+      Given a waveguide structure (`dxes`, `epsilon`, `mu`) and mode fields
+    (`e_norm`, `h_norm`, `wavenumber`, `omega`), calculates the sensitivity of the wavenumber
+    $\beta$ to changes in the dielectric structure $\epsilon$.
+
+    The output is a vector of the same size as `vec(epsilon)`, with each element specifying the
+    sensitivity of `wavenumber` to changes in the corresponding element in `vec(epsilon)`, i.e.
+
+    $$sens_{i} = \frac{\partial\beta}{\partial\epsilon_i}$$
+
+    An adjoint approach is used to calculate the sensitivity; the derivation is provided here:
+
+    Starting with the eigenvalue equation
+
+    $$\beta^2 E_{xy} = A_E E_{xy}$$
+
+    where $A_E$ is the waveguide operator from `operator_e()`, and $E_{xy} = \begin{bmatrix} E_x \\
+                                                                                             E_y \end{bmatrix}$,
+    we can differentiate with respect to one of the $\epsilon$ elements (i.e. at one Yee grid point), $\epsilon_i$:
+
+    $$
+    (2 \beta) \partial_{\epsilon_i}(\beta) E_{xy} + \beta^2 \partial_{\epsilon_i} E_{xy}
+        = \partial_{\epsilon_i}(A_E) E_{xy} + A_E \partial_{\epsilon_i} E_{xy}
+    $$
+
+    We then multiply by $H_{yx}^\star = \begin{bmatrix}H_y^\star \\ -H_x^\star \end{bmatrix}$ from the left:
+
+    $$
+    (2 \beta) \partial_{\epsilon_i}(\beta) H_{yx}^\star E_{xy} + \beta^2 H_{yx}^\star \partial_{\epsilon_i} E_{xy}
+        = H_{yx}^\star \partial_{\epsilon_i}(A_E) E_{xy} + H_{yx}^\star A_E \partial_{\epsilon_i} E_{xy}
+    $$
+
+    However, $H_{yx}^\star$ is actually a left-eigenvector of $A_E$. This can be verified by inspecting
+    the form of `operator_h` ($A_H$) and comparing its conjugate transpose to `operator_e` ($A_E$). Also, note
+    $H_{yx}^\star \cdot E_{xy} = H^\star \times E$ recalls the mode orthogonality relation. See doi:10.5194/ars-9-85-201
+    for a similar approach. Therefore,
+
+    $$
+    H_{yx}^\star A_E \partial_{\epsilon_i} E_{xy} = \beta^2 H_{yx}^\star \partial_{\epsilon_i} E_{xy}
+    $$
+
+    and we can simplify to
+
+    $$
+    \partial_{\epsilon_i}(\beta)
+        = \frac{1}{2 \beta} \frac{H_{yx}^\star \partial_{\epsilon_i}(A_E) E_{xy} }{H_{yx}^\star E_{xy}}
+    $$
+
+    This expression can be quickly calculated for all $i$ by writing out the various terms of
+    $\partial_{\epsilon_i} A_E$ and recognizing that the vector-matrix-vector products (i.e. scalars)
+    $sens_i = \vec{v}_{left} \partial_{\epsilon_i} (\epsilon_{xyz}) \vec{v}_{right}$, indexed by $i$, can be expressed as
+    elementwise multiplications $\vec{sens} = \vec{v}_{left} \star \vec{v}_{right}$
+
+
+    Args:
+        e_norm: Normalized, vectorized E_xyz field for the mode. E.g. as returned by `normalized_fields_e`.
+        h_norm: Normalized, vectorized H_xyz field for the mode. E.g. as returned by `normalized_fields_e`.
+        wavenumber: Propagation constant for the mode. The z-axis is assumed to be continuous (i.e. without numerical dispersion).
+        omega: The angular frequency of the system.
+        dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
+        epsilon: Vectorized dielectric constant grid
+        mu: Vectorized magnetic permeability grid (default 1 everywhere)
+
+    Returns:
+        Sparse matrix representation of the operator.
+    """
+    if mu is None:
+        mu = numpy.ones_like(epsilon)
+
+    Dfx, Dfy = deriv_forward(dxes[0])
+    Dbx, Dby = deriv_back(dxes[1])
+
+
+    eps_x, eps_y, eps_z = numpy.split(epsilon, 3)
+    eps_xy = sparse.diags(numpy.hstack((eps_x, eps_y)))
+    eps_z_inv = sparse.diags(1 / eps_z)
+
+    mu_x, mu_y, mu_z = numpy.split(mu, 3)
+    mu_yx = sparse.diags(numpy.hstack((mu_y, mu_x)))
+    mu_z_inv = sparse.diags(1 / mu_z)
+
+    da_exxhyy = vec(dxes[1][0][:, None] * dxes[0][1][None, :])
+    da_eyyhxx = vec(dxes[1][1][None, :] * dxes[0][0][:, None])
+    ev_xy = numpy.concatenate(numpy.split(e_norm, 3)[:2]) * numpy.concatenate([da_exxhyy, da_eyyhxx])
+    hx, hy, hz = numpy.split(h_norm, 3)
+    hv_yx_conj = numpy.conj(numpy.concatenate([hy, -hx]))
+
+    sens_xy1 = (hv_yx_conj @ (omega * omega * mu_yx)) * ev_xy
+    sens_xy2 = (hv_yx_conj @ sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby))) * ev_xy
+    sens_z =   (hv_yx_conj @ sparse.vstack((Dfx, Dfy)) @ (-eps_z_inv * eps_z_inv)) * (sparse.hstack((Dbx, Dby)) @ eps_xy @ ev_xy)
+    norm = hv_yx_conj @ ev_xy
+
+    sens_tot = numpy.concatenate([sens_xy1 + sens_xy2, sens_z]) / (2 * wavenumber * norm)
+    return sens_tot
+
+
 def solve_modes(
         mode_numbers: list[int],
         omega: complex,

From 95e3f71b40494e7134f6425104c421ab5e6911b1 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Wed, 17 Jul 2024 23:15:34 -0700
Subject: [PATCH 324/437] use f-strings in place of .format()

---
 examples/fdtd.py            |  3 ++-
 meanas/fdfd/bloch.py        |  2 +-
 meanas/fdfd/solvers.py      |  3 ++-
 meanas/fdfd/waveguide_2d.py |  2 +-
 meanas/fdmath/operators.py  | 13 ++++++-------
 meanas/fdtd/boundaries.py   |  4 ++--
 meanas/fdtd/pml.py          |  4 ++--
 meanas/test/test_fdtd.py    |  2 +-
 8 files changed, 17 insertions(+), 16 deletions(-)

diff --git a/examples/fdtd.py b/examples/fdtd.py
index 8dc0d98..8378b34 100644
--- a/examples/fdtd.py
+++ b/examples/fdtd.py
@@ -157,7 +157,8 @@ def main():
         e[1][tuple(grid.shape//2)] += field_source(t)
         update_H(e, h)
 
-        print('iteration {}: average {} iterations per sec'.format(t, (t+1)/(time.perf_counter()-start)))
+        avg_rate = (t + 1)/(time.perf_counter() - start))
+        print(f'iteration {t}: average {avg_rate} iterations per sec')
         sys.stdout.flush()
 
         if t % 20 == 0:
diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 800a603..55abbee 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -781,7 +781,7 @@ def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_to
                                           x_min, x_max, isave, dsave)
         for i in range(int(1e6)):
             if task != 'F':
-                logging.info('search converged in {} iterations'.format(i))
+                logging.info(f'search converged in {i} iterations')
                 break
             fx = f(x, dfx)
             x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task,
diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py
index 19cb418..8ac157c 100644
--- a/meanas/fdfd/solvers.py
+++ b/meanas/fdfd/solvers.py
@@ -43,7 +43,8 @@ def _scipy_qmr(
         nonlocal ii
         ii += 1
         if ii % 100 == 0:
-            logger.info('Solver residual at iteration {} : {}'.format(ii, norm(A @ xk - b)))
+            cur_norm = norm(A @ xk - b)
+            logger.info(f'Solver residual at iteration {ii} : {cur_norm}')
 
     if 'callback' in kwargs:
         def augmented_callback(xk: ArrayLike) -> None:
diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index cfda1af..df4df73 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -420,7 +420,7 @@ def _normalized_fields(
     Sz_a = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0]
     Sz_b = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1]
     Sz_tavg = numpy.real(Sz_a.sum() - Sz_b.sum()) * 0.5       # 0.5 since E, H are assumed to be peak (not RMS) amplitudes
-    assert Sz_tavg > 0, 'Found a mode propagating in the wrong direction! Sz_tavg={}'.format(Sz_tavg)
+    assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
 
     energy = epsilon * e.conj() * e
 
diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py
index 95101c5..9d5988d 100644
--- a/meanas/fdmath/operators.py
+++ b/meanas/fdmath/operators.py
@@ -29,9 +29,9 @@ def shift_circ(
         Sparse matrix for performing the circular shift.
     """
     if len(shape) not in (2, 3):
-        raise Exception('Invalid shape: {}'.format(shape))
+        raise Exception(f'Invalid shape: {shape}')
     if axis not in range(len(shape)):
-        raise Exception('Invalid direction: {}, shape is {}'.format(axis, shape))
+        raise Exception(f'Invalid direction: {axis}, shape is {shape}')
 
     shifts = [abs(shift_distance) if a == axis else 0 for a in range(3)]
     shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts)]
@@ -69,12 +69,11 @@ def shift_with_mirror(
         Sparse matrix for performing the shift-with-mirror.
     """
     if len(shape) not in (2, 3):
-        raise Exception('Invalid shape: {}'.format(shape))
+        raise Exception(f'Invalid shape: {shape}')
     if axis not in range(len(shape)):
-        raise Exception('Invalid direction: {}, shape is {}'.format(axis, shape))
+        raise Exception(f'Invalid direction: {axis}, shape is {shape}')
     if shift_distance >= shape[axis]:
-        raise Exception('Shift ({}) is too large for axis {} of size {}'.format(
-                        shift_distance, axis, shape[axis]))
+        raise Exception(f'Shift ({shift_distance}) is too large for axis {axis} of size {shape[axis]}')
 
     def mirrored_range(n: int, s: int) -> NDArray[numpy.int_]:
         v = numpy.arange(n) + s
@@ -198,7 +197,7 @@ def avg_forward(axis: int, shape: Sequence[int]) -> sparse.spmatrix:
         Sparse matrix for forward average operation.
     """
     if len(shape) not in (2, 3):
-        raise Exception('Invalid shape: {}'.format(shape))
+        raise Exception(f'Invalid shape: {shape}')
 
     n = numpy.prod(shape)
     return 0.5 * (sparse.eye(n) + shift_circ(axis, shape))
diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py
index 652d957..e82deef 100644
--- a/meanas/fdtd/boundaries.py
+++ b/meanas/fdtd/boundaries.py
@@ -15,7 +15,7 @@ def conducting_boundary(
         ) -> tuple[fdfield_updater_t, fdfield_updater_t]:
     dirs = [0, 1, 2]
     if direction not in dirs:
-        raise Exception('Invalid direction: {}'.format(direction))
+        raise Exception(f'Invalid direction: {direction}')
     dirs.remove(direction)
     u, v = dirs
 
@@ -64,4 +64,4 @@ def conducting_boundary(
 
         return ep, hp
 
-    raise Exception('Bad polarity: {}'.format(polarity))
+    raise Exception(f'Bad polarity: {polarity}')
diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py
index b11b3b5..65d71e6 100644
--- a/meanas/fdtd/pml.py
+++ b/meanas/fdtd/pml.py
@@ -33,10 +33,10 @@ def cpml_params(
         ) -> dict[str, Any]:
 
     if axis not in range(3):
-        raise Exception('Invalid axis: {}'.format(axis))
+        raise Exception(f'Invalid axis: {axis}')
 
     if polarity not in (-1, 1):
-        raise Exception('Invalid polarity: {}'.format(polarity))
+        raise Exception(f'Invalid polarity: {polarity}')
 
     if thickness <= 2:
         raise Exception('It would be wise to have a pml with 4+ cells of thickness')
diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py
index 701275e..0a92c73 100644
--- a/meanas/test/test_fdtd.py
+++ b/meanas/test/test_fdtd.py
@@ -101,7 +101,7 @@ def test_poynting_divergence(sim: 'TDResult') -> None:
 def test_poynting_planes(sim: 'TDResult') -> None:
     mask = (sim.js[0] != 0).any(axis=0)
     if mask.sum() > 1:
-        pytest.skip('test_poynting_planes can only test single point sources, got {}'.format(mask.sum()))
+        pytest.skip(f'test_poynting_planes can only test single point sources, got {mask.sum()}')
 
     args: dict[str, Any] = {
         'dxes': sim.dxes,

From dc3e733e7f0181e9592f8dbc10f16cf8599d1681 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Wed, 17 Jul 2024 23:15:57 -0700
Subject: [PATCH 325/437] flake8 fixes

---
 meanas/fdfd/bloch.py        | 14 +++++++-------
 meanas/fdfd/waveguide_2d.py | 12 ++++++------
 meanas/fdtd/boundaries.py   | 10 +++++++---
 3 files changed, 20 insertions(+), 16 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 55abbee..0d0ac1a 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -684,11 +684,11 @@ def eigsolve(
                 Qi = Qi_func(theta)
                 c2 = numpy.cos(2 * theta)
                 s2 = numpy.sin(2 * theta)
-                F = -0.5*s2 *  (ZtAZ - DtAD) + c2 * symZtAD
+                F = -0.5 * s2 * (ZtAZ - DtAD) + c2 * symZtAD
                 trace_deriv = _rtrace_AtB(Qi, F)
 
                 G = Qi @ F.conj().T @ Qi.conj().T
-                H = -0.5*s2 * (ZtZ - DtD) + c2 * symZtD
+                H = -0.5 * s2 * (ZtZ - DtD) + c2 * symZtD
                 trace_deriv -= _rtrace_AtB(G, H)
 
                 trace_deriv *= 2
@@ -696,12 +696,12 @@ def eigsolve(
 
             U_sZtD = U @ symZtD
 
-            dE = 2.0 * (_rtrace_AtB(U, symZtAD) -
-                        _rtrace_AtB(ZtAZU, U_sZtD))
+            dE = 2.0 * (_rtrace_AtB(U, symZtAD)
+                        - _rtrace_AtB(ZtAZU, U_sZtD))
 
-            d2E = 2 * (_rtrace_AtB(U, DtAD) -
-                       _rtrace_AtB(ZtAZU, U @ (DtD - 4 * symZtD @ U_sZtD)) -
-                   4 * _rtrace_AtB(U, symZtAD @ U_sZtD))
+            d2E = 2 * (_rtrace_AtB(U, DtAD)
+                       - _rtrace_AtB(ZtAZU, U @ (DtD - 4 * symZtD @ U_sZtD))
+                       - 4 * _rtrace_AtB(U, symZtAD @ U_sZtD))
 
             # Newton-Raphson to find a root of the first derivative:
             theta = -dE / d2E
diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index df4df73..399574d 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -253,7 +253,8 @@ def operator_e(
     mu_yx = sparse.diags(numpy.hstack((mu_parts[1], mu_parts[0])))
     mu_z_inv = sparse.diags(1 / mu_parts[2])
 
-    op = (omega * omega * mu_yx @ eps_xy
+    op = (
+        omega * omega * mu_yx @ eps_xy
         + mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx))
         + sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy
         )
@@ -321,7 +322,8 @@ def operator_h(
     mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1])))
     mu_z_inv = sparse.diags(1 / mu_parts[2])
 
-    op = (omega * omega * eps_yx @ mu_xy
+    op = (
+        omega * omega * eps_yx @ mu_xy
         + eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx))
         + sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy
         )
@@ -799,14 +801,12 @@ def sensitivity(
     Dfx, Dfy = deriv_forward(dxes[0])
     Dbx, Dby = deriv_back(dxes[1])
 
-
     eps_x, eps_y, eps_z = numpy.split(epsilon, 3)
     eps_xy = sparse.diags(numpy.hstack((eps_x, eps_y)))
     eps_z_inv = sparse.diags(1 / eps_z)
 
-    mu_x, mu_y, mu_z = numpy.split(mu, 3)
+    mu_x, mu_y, _mu_z = numpy.split(mu, 3)
     mu_yx = sparse.diags(numpy.hstack((mu_y, mu_x)))
-    mu_z_inv = sparse.diags(1 / mu_z)
 
     da_exxhyy = vec(dxes[1][0][:, None] * dxes[0][1][None, :])
     da_eyyhxx = vec(dxes[1][1][None, :] * dxes[0][0][:, None])
@@ -816,7 +816,7 @@ def sensitivity(
 
     sens_xy1 = (hv_yx_conj @ (omega * omega * mu_yx)) * ev_xy
     sens_xy2 = (hv_yx_conj @ sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby))) * ev_xy
-    sens_z =   (hv_yx_conj @ sparse.vstack((Dfx, Dfy)) @ (-eps_z_inv * eps_z_inv)) * (sparse.hstack((Dbx, Dby)) @ eps_xy @ ev_xy)
+    sens_z   = (hv_yx_conj @ sparse.vstack((Dfx, Dfy)) @ (-eps_z_inv * eps_z_inv)) * (sparse.hstack((Dbx, Dby)) @ eps_xy @ ev_xy)
     norm = hv_yx_conj @ ev_xy
 
     sens_tot = numpy.concatenate([sens_xy1 + sens_xy2, sens_z]) / (2 * wavenumber * norm)
diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py
index e82deef..131d741 100644
--- a/meanas/fdtd/boundaries.py
+++ b/meanas/fdtd/boundaries.py
@@ -19,9 +19,13 @@ def conducting_boundary(
     dirs.remove(direction)
     u, v = dirs
 
+    boundary_slice: list[Any]
+    shifted1_slice: list[Any]
+    shifted2_slice: list[Any]
+
     if polarity < 0:
-        boundary_slice = [slice(None)] * 3      # type: list[Any]
-        shifted1_slice = [slice(None)] * 3      # type: list[Any]
+        boundary_slice = [slice(None)] * 3
+        shifted1_slice = [slice(None)] * 3
         boundary_slice[direction] = 0
         shifted1_slice[direction] = 1
 
@@ -42,7 +46,7 @@ def conducting_boundary(
     if polarity > 0:
         boundary_slice = [slice(None)] * 3
         shifted1_slice = [slice(None)] * 3
-        shifted2_slice = [slice(None)] * 3      # type: list[Any]
+        shifted2_slice = [slice(None)] * 3
         boundary_slice[direction] = -1
         shifted1_slice[direction] = -2
         shifted2_slice[direction] = -3

From 2712d96f2ad16ce6f3f0319937eb6bde86c4b699 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Thu, 18 Jul 2024 01:03:42 -0700
Subject: [PATCH 326/437] add notes on references

---
 meanas/fdfd/waveguide_cyl.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 0d3be0b..d476caa 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -25,6 +25,9 @@ def cylindrical_operator(
     """
     Cylindrical coordinate waveguide operator of the form
 
+    (NOTE: See 10.1364/OL.33.001848)
+    TODO: consider 10.1364/OE.20.021583
+
     TODO
 
     for use with a field vector of the form `[E_r, E_y]`.

From 2f00baf0c62fd4a174bc0e4e126c0305f4c4976f Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Thu, 18 Jul 2024 01:03:51 -0700
Subject: [PATCH 327/437] fixup cylindrical wg example

---
 examples/tcyl.py | 40 +++++++++++++++++++++-------------------
 1 file changed, 21 insertions(+), 19 deletions(-)

diff --git a/examples/tcyl.py b/examples/tcyl.py
index 590a260..e6dc15f 100644
--- a/examples/tcyl.py
+++ b/examples/tcyl.py
@@ -3,7 +3,7 @@ import numpy
 from numpy.linalg import norm
 
 from meanas.fdmath import vec, unvec
-from meanas.fdfd import waveguide_mode, functional, scpml
+from meanas.fdfd import waveguide_cyl, functional, scpml
 from meanas.fdfd.solvers import generic as generic_solver
 
 import gridlock
@@ -37,29 +37,34 @@ def test1(solver=generic_solver):
     xyz_max = numpy.array([800, y_max, z_max]) + (pml_thickness + 2) * dx
 
     # Coordinates of the edges of the cells.
-    half_edge_coords = [numpy.arange(dx/2, m + dx/2, step=dx) for m in xyz_max]
+    half_edge_coords = [numpy.arange(dx / 2, m + dx / 2, step=dx) for m in xyz_max]
     edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
     edge_coords[0] = numpy.array([-dx, dx])
 
     # #### Create the grid and draw the device ####
     grid = gridlock.Grid(edge_coords)
     epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
-    grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], eps=n_wg**2)
+    grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], foreground=n_wg**2)
 
     dxes = [grid.dxyz, grid.autoshifted_dxyz()]
     for a in (1, 2):
         for p in (-1, 1):
-            dxes = scmpl.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p,
-                                            thickness=pml_thickness)
+            dxes = scpml.stretch_with_scpml(
+                dxes,
+                omega=omega,
+                axis=a,
+                polarity=p,
+                thickness=pml_thickness,
+                )
 
     wg_args = {
         'omega': omega,
         'dxes': [(d[1], d[2]) for d in dxes],
-        'epsilon': vec(g.transpose([1, 2, 0]) for g in epsilon),
+        'epsilon': vec(epsilon.transpose([0, 2, 3, 1])),
         'r0': r0,
     }
 
-    wg_results = waveguide_mode.solve_waveguide_mode_cylindrical(mode_number=0, **wg_args)
+    wg_results = waveguide_cyl.solve_mode(mode_number=0, **wg_args)
 
     E = wg_results['E']
 
@@ -70,20 +75,17 @@ def test1(solver=generic_solver):
     '''
     Plot results
     '''
-    def pcolor(v):
+    def pcolor(fig, ax, v, title):
         vmax = numpy.max(numpy.abs(v))
-        pyplot.pcolor(v.T, cmap='seismic', vmin=-vmax, vmax=vmax)
-        pyplot.axis('equal')
-        pyplot.colorbar()
+        mappable = ax.pcolormesh(v.T, cmap='seismic', vmin=-vmax, vmax=vmax)
+        ax.set_aspect('equal', adjustable='box')
+        ax.set_title(title)
+        ax.figure.colorbar(mappable)
 
-    pyplot.figure()
-    pyplot.subplot(2, 2, 1)
-    pcolor(numpy.real(E[0][:, :]))
-    pyplot.subplot(2, 2, 2)
-    pcolor(numpy.real(E[1][:, :]))
-    pyplot.subplot(2, 2, 3)
-    pcolor(numpy.real(E[2][:, :]))
-    pyplot.subplot(2, 2, 4)
+    fig, axes = pyplot.subplots(2, 2)
+    pcolor(fig, axes[0][0], numpy.real(E[0]), 'Ex')
+    pcolor(fig, axes[0][1], numpy.real(E[1]), 'Ey')
+    pcolor(fig, axes[1][0], numpy.real(E[2]), 'Ez')
     pyplot.show()
 
 

From 99c22d572f6c9b1be440abae2cc41abaf38b7283 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 28 Jul 2024 23:21:59 -0700
Subject: [PATCH 328/437] bump numpy version

---
 pyproject.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index 8b875f3..990bde2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,7 +39,7 @@ include = [
     ]
 dynamic = ["version"]
 dependencies = [
-    "numpy~=1.21",
+    "numpy~=1.26",
     "scipy",
     ]
 

From 6f3ae5a64fd107808bb84c89aea5d73cfe935e3d Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 28 Jul 2024 23:22:21 -0700
Subject: [PATCH 329/437] explicitly re-export some names

---
 meanas/fdfd/__init__.py   |  9 ++++++++-
 meanas/fdmath/__init__.py | 24 ++++++++++++++++++++----
 meanas/fdtd/__init__.py   | 24 +++++++++++++++++++-----
 3 files changed, 47 insertions(+), 10 deletions(-)

diff --git a/meanas/fdfd/__init__.py b/meanas/fdfd/__init__.py
index 1829cf9..ba57fc4 100644
--- a/meanas/fdfd/__init__.py
+++ b/meanas/fdfd/__init__.py
@@ -91,5 +91,12 @@ $$
 
 
 """
-from . import solvers, operators, functional, scpml, waveguide_2d, waveguide_3d
+from . import (
+    solvers as solvers,
+    operators as operators,
+    functional as functional,
+    scpml as scpml,
+    waveguide_2d as waveguide_2d,
+    waveguide_3d as waveguide_3d,
+    )
 # from . import farfield, bloch TODO
diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py
index 8a6b784..b1d8354 100644
--- a/meanas/fdmath/__init__.py
+++ b/meanas/fdmath/__init__.py
@@ -741,8 +741,24 @@ the true values can be multiplied back in after the simulation is complete if no
 normalized results are needed.
 """
 
-from .types import fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t, dx_lists_t, dx_lists_mut
-from .types import fdfield_updater_t, cfdfield_updater_t
-from .vectorization import vec, unvec
-from . import operators, functional, types, vectorization
+from .types import (
+    fdfield_t as fdfield_t,
+    vfdfield_t as vfdfield_t,
+    cfdfield_t as cfdfield_t,
+    vcfdfield_t as vcfdfield_t,
+    dx_lists_t as dx_lists_t,
+    dx_lists_mut as dx_lists_mut,
+    fdfield_updater_t as fdfield_updater_t,
+    cfdfield_updater_t as cfdfield_updater_t,
+    )
+from .vectorization import (
+    vec as vec,
+    unvec as unvec,
+    )
+from . import (
+    operators as operators,
+    functional as functional,
+    types as types,
+    vectorization as vectorization,
+    )
 
diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py
index 171c4f4..33b1995 100644
--- a/meanas/fdtd/__init__.py
+++ b/meanas/fdtd/__init__.py
@@ -159,8 +159,22 @@ Boundary conditions
 # TODO notes about boundaries / PMLs
 """
 
-from .base import maxwell_e, maxwell_h
-from .pml import cpml_params, updates_with_cpml
-from .energy import (poynting, poynting_divergence, energy_hstep, energy_estep,
-                     delta_energy_h2e, delta_energy_j)
-from .boundaries import conducting_boundary
+from .base import (
+    maxwell_e as maxwell_e,
+    maxwell_h as maxwell_h,
+    )
+from .pml import (
+    cpml_params as cpml_params,
+    updates_with_cpml as updates_with_cpml,
+    )
+from .energy import (
+    poynting as poynting,
+    poynting_divergence as poynting_divergence,
+    energy_hstep as energy_hstep,
+    energy_estep as energy_estep,
+    delta_energy_h2e as delta_energy_h2e,
+    delta_energy_j as delta_energy_j,
+    )
+from .boundaries import (
+    conducting_boundary as conducting_boundary,
+    )

From b16b35d84a2ab8b7ece797f738daec58061d15b3 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 28 Jul 2024 23:23:11 -0700
Subject: [PATCH 330/437] use new numpy.random.Generator approach

---
 meanas/eigensolvers.py | 3 ++-
 meanas/fdfd/bloch.py   | 5 +++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py
index ac64f5c..032f921 100644
--- a/meanas/eigensolvers.py
+++ b/meanas/eigensolvers.py
@@ -25,8 +25,9 @@ def power_iteration(
     Returns:
         (Largest-magnitude eigenvalue, Corresponding eigenvector estimate)
     """
+    rng = numpy.random.default_rng()
     if guess_vector is None:
-        v = numpy.random.rand(operator.shape[0]) + 1j * numpy.random.rand(operator.shape[0])
+        v = rng.random(operator.shape[0]) + 1j * rng.random(operator.shape[0])
     else:
         v = guess_vector
 
diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 0d0ac1a..e5754a1 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -561,9 +561,10 @@ def eigsolve(
     prev_theta = 0.5
     D = numpy.zeros(shape=y_shape, dtype=complex)
 
+    rng = numpy.random.default_rng()
     Z: NDArray[numpy.complex128]
     if y0 is None:
-        Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape)
+        Z = rng.random(y_shape) + 1j * rng.random(y_shape)
     else:
         Z = numpy.array(y0, copy=False).T
 
@@ -573,7 +574,7 @@ def eigsolve(
         try:
             U = numpy.linalg.inv(ZtZ)
         except numpy.linalg.LinAlgError:
-            Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape)
+            Z = rng.random(y_shape) + 1j * rng.random(y_shape)
             continue
 
         trace_U = real(trace(U))

From 36bea6a5931c96e85b29c99446629d2bf892a240 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 28 Jul 2024 23:23:21 -0700
Subject: [PATCH 331/437] drop unused import

---
 meanas/fdfd/bloch.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index e5754a1..f1f18ed 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -114,7 +114,6 @@ logger = logging.getLogger(__name__)
 try:
     import pyfftw.interfaces.numpy_fft  # type: ignore
     import pyfftw.interfaces            # type: ignore
-    import multiprocessing
     logger.info('Using pyfftw')
 
     pyfftw.interfaces.cache.enable()

From ee51c7db496ec6db9cb82a724f7018926348d1b1 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sun, 28 Jul 2024 23:23:47 -0700
Subject: [PATCH 332/437] improve type annotations

---
 meanas/fdfd/waveguide_3d.py  |  7 ++++---
 meanas/fdfd/waveguide_cyl.py |  2 +-
 meanas/fdmath/functional.py  | 13 +++++++------
 meanas/fdmath/operators.py   |  9 +++++----
 meanas/fdmath/types.py       | 14 +++++++-------
 5 files changed, 24 insertions(+), 21 deletions(-)

diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py
index 7f994d3..2f499fa 100644
--- a/meanas/fdfd/waveguide_3d.py
+++ b/meanas/fdfd/waveguide_3d.py
@@ -7,6 +7,7 @@ its parameters into 2D equivalents and expands the results back into 3D.
 from typing import Sequence, Any
 import numpy
 from numpy.typing import NDArray
+from numpy import complexfloating
 
 from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, cfdfield_t
 from . import operators, waveguide_2d
@@ -21,7 +22,7 @@ def solve_mode(
         slices: Sequence[slice],
         epsilon: fdfield_t,
         mu: fdfield_t | None = None,
-        ) -> dict[str, complex | NDArray[numpy.float_]]:
+        ) -> dict[str, complex | NDArray[complexfloating]]:
     """
     Given a 3D grid, selects a slice from the grid and attempts to
      solve for an eigenmode propagating through that slice.
@@ -40,8 +41,8 @@ def solve_mode(
     Returns:
         ```
         {
-            'E': list[NDArray[numpy.float_]],
-            'H': list[NDArray[numpy.float_]],
+            'E': NDArray[complexfloating],
+            'H': NDArray[complexfloating],
             'wavenumber': complex,
         }
         ```
diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index d476caa..596c6be 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -11,7 +11,7 @@ As the z-dependence is known, all the functions in this file assume a 2D grid
 import numpy
 import scipy.sparse as sparse       # type: ignore
 
-from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t, cfdfield_t
+from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, cfdfield_t
 from ..fdmath.operators import deriv_forward, deriv_back
 from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
 
diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py
index 3a10a00..91d8d29 100644
--- a/meanas/fdmath/functional.py
+++ b/meanas/fdmath/functional.py
@@ -7,12 +7,13 @@ from typing import Sequence, Callable
 
 import numpy
 from numpy.typing import NDArray
+from numpy import floating
 
 from .types import fdfield_t, fdfield_updater_t
 
 
 def deriv_forward(
-        dx_e: Sequence[NDArray[numpy.float_]] | None = None,
+        dx_e: Sequence[NDArray[floating]] | None = None,
         ) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
     """
     Utility operators for taking discretized derivatives (backward variant).
@@ -36,7 +37,7 @@ def deriv_forward(
 
 
 def deriv_back(
-        dx_h: Sequence[NDArray[numpy.float_]] | None = None,
+        dx_h: Sequence[NDArray[floating]] | None = None,
         ) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
     """
     Utility operators for taking discretized derivatives (forward variant).
@@ -60,7 +61,7 @@ def deriv_back(
 
 
 def curl_forward(
-        dx_e: Sequence[NDArray[numpy.float_]] | None = None,
+        dx_e: Sequence[NDArray[floating]] | None = None,
         ) -> fdfield_updater_t:
     r"""
     Curl operator for use with the E field.
@@ -89,7 +90,7 @@ def curl_forward(
 
 
 def curl_back(
-        dx_h: Sequence[NDArray[numpy.float_]] | None = None,
+        dx_h: Sequence[NDArray[floating]] | None = None,
         ) -> fdfield_updater_t:
     r"""
     Create a function which takes the backward curl of a field.
@@ -118,7 +119,7 @@ def curl_back(
 
 
 def curl_forward_parts(
-        dx_e: Sequence[NDArray[numpy.float_]] | None = None,
+        dx_e: Sequence[NDArray[floating]] | None = None,
         ) -> Callable:
     Dx, Dy, Dz = deriv_forward(dx_e)
 
@@ -131,7 +132,7 @@ def curl_forward_parts(
 
 
 def curl_back_parts(
-        dx_h: Sequence[NDArray[numpy.float_]] | None = None,
+        dx_h: Sequence[NDArray[floating]] | None = None,
         ) -> Callable:
     Dx, Dy, Dz = deriv_back(dx_h)
 
diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py
index 9d5988d..c085808 100644
--- a/meanas/fdmath/operators.py
+++ b/meanas/fdmath/operators.py
@@ -6,6 +6,7 @@ Basic discrete calculus etc.
 from typing import Sequence
 import numpy
 from numpy.typing import NDArray
+from numpy import floating
 import scipy.sparse as sparse   # type: ignore
 
 from .types import vfdfield_t
@@ -96,7 +97,7 @@ def shift_with_mirror(
 
 
 def deriv_forward(
-        dx_e: Sequence[NDArray[numpy.float_]],
+        dx_e: Sequence[NDArray[floating]],
         ) -> list[sparse.spmatrix]:
     """
     Utility operators for taking discretized derivatives (forward variant).
@@ -123,7 +124,7 @@ def deriv_forward(
 
 
 def deriv_back(
-        dx_h: Sequence[NDArray[numpy.float_]],
+        dx_h: Sequence[NDArray[floating]],
         ) -> list[sparse.spmatrix]:
     """
     Utility operators for taking discretized derivatives (backward variant).
@@ -218,7 +219,7 @@ def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix:
 
 
 def curl_forward(
-        dx_e: Sequence[NDArray[numpy.float_]],
+        dx_e: Sequence[NDArray[floating]],
         ) -> sparse.spmatrix:
     """
     Curl operator for use with the E field.
@@ -234,7 +235,7 @@ def curl_forward(
 
 
 def curl_back(
-        dx_h: Sequence[NDArray[numpy.float_]],
+        dx_h: Sequence[NDArray[floating]],
         ) -> sparse.spmatrix:
     """
     Curl operator for use with the H field.
diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py
index aae9594..b78e93f 100644
--- a/meanas/fdmath/types.py
+++ b/meanas/fdmath/types.py
@@ -2,25 +2,25 @@
 Types shared across multiple submodules
 """
 from typing import Sequence, Callable, MutableSequence
-import numpy
 from numpy.typing import NDArray
+from numpy import floating, complexfloating
 
 
 # Field types
-fdfield_t = NDArray[numpy.float_]
+fdfield_t = NDArray[floating]
 """Vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)"""
 
-vfdfield_t = NDArray[numpy.float_]
+vfdfield_t = NDArray[floating]
 """Linearized vector field (single vector of length 3*X*Y*Z)"""
 
-cfdfield_t = NDArray[numpy.complex_]
+cfdfield_t = NDArray[complexfloating]
 """Complex vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)"""
 
-vcfdfield_t = NDArray[numpy.complex_]
+vcfdfield_t = NDArray[complexfloating]
 """Linearized complex vector field (single vector of length 3*X*Y*Z)"""
 
 
-dx_lists_t = Sequence[Sequence[NDArray[numpy.float_]]]
+dx_lists_t = Sequence[Sequence[NDArray[floating]]]
 """
  'dxes' datastructure which contains grid cell width information in the following format:
 
@@ -31,7 +31,7 @@ dx_lists_t = Sequence[Sequence[NDArray[numpy.float_]]]
    and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc.
 """
 
-dx_lists_mut = MutableSequence[MutableSequence[NDArray[numpy.float_]]]
+dx_lists_mut = MutableSequence[MutableSequence[NDArray[floating]]]
 """Mutable version of `dx_lists_t`"""
 
 

From 10f26c12b4452cf02360a678cedd27e5138f229d Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:22:54 -0700
Subject: [PATCH 333/437] add ruff and mypy configs

---
 pyproject.toml | 45 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)

diff --git a/pyproject.toml b/pyproject.toml
index 990bde2..741ae48 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,3 +51,48 @@ path = "meanas/__init__.py"
 dev = ["pytest", "pdoc", "gridlock"]
 examples = ["gridlock"]
 test = ["pytest"]
+
+
+[tool.ruff]
+exclude = [
+    ".git",
+    "dist",
+    ]
+line-length = 245
+indent-width = 4
+lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+lint.select = [
+    "NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG",
+    "C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT",
+    "ARG", "PL", "R", "TRY",
+    "G010", "G101", "G201", "G202",
+    "Q002", "Q003", "Q004",
+    ]
+lint.ignore = [
+    #"ANN001",   # No annotation
+    "ANN002",   # *args
+    "ANN003",   # **kwargs
+    "ANN401",   # Any
+    "ANN101",   # self: Self
+    "SIM108",   # single-line if / else assignment
+    "RET504",   # x=y+z; return x
+    "PIE790",   # unnecessary pass
+    "ISC003",   # non-implicit string concatenation
+    "C408",     # dict(x=y) instead of {'x': y}
+    "PLR09",    # Too many xxx
+    "PLR2004",  # magic number
+    "PLC0414",  # import x as x
+    "TRY003",   # Long exception message
+    "TRY002",   # Exception()
+    ]
+
+
+[[tool.mypy.overrides]]
+module = [
+    "scipy",
+    "scipy.optimize",
+    "scipy.linalg",
+    "scipy.sparse",
+    "scipy.sparse.linalg",
+    ]
+ignore_missing_imports = true

From ca94ad1b25c49f5433eb37fe08e19b032dcf7292 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:23:08 -0700
Subject: [PATCH 334/437] use path.open()

---
 meanas/__init__.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/meanas/__init__.py b/meanas/__init__.py
index 354adc9..0757a5c 100644
--- a/meanas/__init__.py
+++ b/meanas/__init__.py
@@ -11,7 +11,8 @@ __author__ = 'Jan Petykiewicz'
 
 
 try:
-    with open(pathlib.Path(__file__).parent / 'README.md', 'r') as f:
+    readme_path = pathlib.Path(__file__).parent / 'README.md'
+    with readme_path.open('r') as f:
         __doc__ = f.read()
 except Exception:
     pass

From d5fca741d1514cbe8d66add3cadc3a71bda184f5 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:27:59 -0700
Subject: [PATCH 335/437] remove type:ignore from scipy imports (done at
 pyproject.toml level)

---
 meanas/eigensolvers.py       | 4 ++--
 meanas/fdfd/bloch.py         | 8 ++++----
 meanas/fdfd/operators.py     | 2 +-
 meanas/fdfd/solvers.py       | 2 +-
 meanas/fdfd/waveguide_2d.py  | 2 +-
 meanas/fdfd/waveguide_cyl.py | 2 +-
 meanas/fdmath/operators.py   | 2 +-
 7 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py
index 032f921..7a3a8a7 100644
--- a/meanas/eigensolvers.py
+++ b/meanas/eigensolvers.py
@@ -5,8 +5,8 @@ from typing import Callable
 import numpy
 from numpy.typing import NDArray, ArrayLike
 from numpy.linalg import norm
-from scipy import sparse              # type: ignore
-import scipy.sparse.linalg as spalg   # type: ignore
+from scipy import sparse
+import scipy.sparse.linalg as spalg
 
 
 def power_iteration(
diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index f1f18ed..12660e7 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -100,10 +100,10 @@ import numpy
 from numpy import pi, real, trace
 from numpy.fft import fftfreq
 from numpy.typing import NDArray, ArrayLike
-import scipy                            # type: ignore
-import scipy.optimize                   # type: ignore
-from scipy.linalg import norm           # type: ignore
-import scipy.sparse.linalg as spalg     # type: ignore
+import scipy
+import scipy.optimize
+from scipy.linalg import norm
+import scipy.sparse.linalg as spalg
 
 from ..fdmath import fdfield_t, cfdfield_t
 
diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py
index 3a489a7..32e3af0 100644
--- a/meanas/fdfd/operators.py
+++ b/meanas/fdfd/operators.py
@@ -28,7 +28,7 @@ The following operators are included:
 """
 
 import numpy
-import scipy.sparse as sparse       # type: ignore
+from scipy import sparse
 
 from ..fdmath import vec, dx_lists_t, vfdfield_t, vcfdfield_t
 from ..fdmath.operators import shift_with_mirror, shift_circ, curl_forward, curl_back
diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py
index 8ac157c..0487a06 100644
--- a/meanas/fdfd/solvers.py
+++ b/meanas/fdfd/solvers.py
@@ -8,7 +8,7 @@ import logging
 import numpy
 from numpy.typing import ArrayLike, NDArray
 from numpy.linalg import norm
-import scipy.sparse.linalg          # type: ignore
+import scipy.sparse.linalg
 
 from ..fdmath import dx_lists_t, vfdfield_t, vcfdfield_t
 from . import operators
diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 399574d..32e65bc 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -182,7 +182,7 @@ from typing import Any
 import numpy
 from numpy.typing import NDArray, ArrayLike
 from numpy.linalg import norm
-import scipy.sparse as sparse       # type: ignore
+from scipy import sparse
 
 from ..fdmath.operators import deriv_forward, deriv_back, cross
 from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t
diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 596c6be..65778ba 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -9,7 +9,7 @@ As the z-dependence is known, all the functions in this file assume a 2D grid
 # TODO update module docs
 
 import numpy
-import scipy.sparse as sparse       # type: ignore
+from scipy import sparse
 
 from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, cfdfield_t
 from ..fdmath.operators import deriv_forward, deriv_back
diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py
index c085808..79ddfee 100644
--- a/meanas/fdmath/operators.py
+++ b/meanas/fdmath/operators.py
@@ -7,7 +7,7 @@ from typing import Sequence
 import numpy
 from numpy.typing import NDArray
 from numpy import floating
-import scipy.sparse as sparse   # type: ignore
+from scipy import sparse
 
 from .types import vfdfield_t
 

From 43f038d761693e34345a64bf7eb59932d7bcdfc0 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:29:39 -0700
Subject: [PATCH 336/437] modernize type annotations

---
 meanas/eigensolvers.py         |  2 +-
 meanas/fdfd/bloch.py           |  3 ++-
 meanas/fdfd/farfield.py        |  3 ++-
 meanas/fdfd/functional.py      |  2 +-
 meanas/fdfd/scpml.py           |  2 +-
 meanas/fdfd/solvers.py         | 11 ++++++-----
 meanas/fdfd/waveguide_3d.py    |  3 ++-
 meanas/fdmath/functional.py    |  3 ++-
 meanas/fdmath/operators.py     |  2 +-
 meanas/fdmath/types.py         |  2 +-
 meanas/fdmath/vectorization.py |  3 ++-
 meanas/fdtd/pml.py             |  3 ++-
 12 files changed, 23 insertions(+), 16 deletions(-)

diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py
index 7a3a8a7..e8630aa 100644
--- a/meanas/eigensolvers.py
+++ b/meanas/eigensolvers.py
@@ -1,7 +1,7 @@
 """
 Solvers for eigenvalue / eigenvector problems
 """
-from typing import Callable
+from collections.abc import Callable
 import numpy
 from numpy.typing import NDArray, ArrayLike
 from numpy.linalg import norm
diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 12660e7..5ea5e7b 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -94,7 +94,8 @@ This module contains functions for generating and solving the
 
 """
 
-from typing import Callable, Any, cast, Sequence
+from typing import Any, cast
+from collections.abc import Callable, Sequence
 import logging
 import numpy
 from numpy import pi, real, trace
diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py
index 5c1caf0..4829d86 100644
--- a/meanas/fdfd/farfield.py
+++ b/meanas/fdfd/farfield.py
@@ -1,7 +1,8 @@
 """
 Functions for performing near-to-farfield transformation (and the reverse).
 """
-from typing import Any, Sequence, cast
+from typing import Any, cast
+from collections.abc import Sequence
 import numpy
 from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift
 from numpy import pi
diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py
index ba2bd70..8b21923 100644
--- a/meanas/fdfd/functional.py
+++ b/meanas/fdfd/functional.py
@@ -5,7 +5,7 @@ Functional versions of many FDFD operators. These can be useful for performing
 The functions generated here expect `cfdfield_t` inputs with shape (3, X, Y, Z),
 e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z)
 """
-from typing import Callable
+from collections.abc import Callable
 import numpy
 
 from ..fdmath import dx_lists_t, fdfield_t, cfdfield_t, cfdfield_updater_t
diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py
index bc056e1..f0a8843 100644
--- a/meanas/fdfd/scpml.py
+++ b/meanas/fdfd/scpml.py
@@ -2,7 +2,7 @@
 Functions for creating stretched coordinate perfectly matched layer (PML) absorbers.
 """
 
-from typing import Sequence, Callable
+from collections.abc import Sequence, Callable
 
 import numpy
 from numpy.typing import NDArray
diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py
index 0487a06..517ecab 100644
--- a/meanas/fdfd/solvers.py
+++ b/meanas/fdfd/solvers.py
@@ -2,7 +2,8 @@
 Solvers and solver interface for FDFD problems.
 """
 
-from typing import Callable, Dict, Any, Optional
+from typing import Any
+from collections.abc import Callable
 import logging
 
 import numpy
@@ -68,12 +69,12 @@ def generic(
         dxes: dx_lists_t,
         J: vcfdfield_t,
         epsilon: vfdfield_t,
-        mu: Optional[vfdfield_t] = None,
-        pec: Optional[vfdfield_t] = None,
-        pmc: Optional[vfdfield_t] = None,
+        mu: vfdfield_t | None = None,
+        pec: vfdfield_t | None = None,
+        pmc: vfdfield_t | None = None,
         adjoint: bool = False,
         matrix_solver: Callable[..., ArrayLike] = _scipy_qmr,
-        matrix_solver_opts: Optional[Dict[str, Any]] = None,
+        matrix_solver_opts: dict[str, Any] | None = None,
         ) -> vcfdfield_t:
     """
     Conjugate gradient FDFD solver using CSR sparse matrices.
diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py
index 2f499fa..3cffa94 100644
--- a/meanas/fdfd/waveguide_3d.py
+++ b/meanas/fdfd/waveguide_3d.py
@@ -4,7 +4,8 @@ Tools for working with waveguide modes in 3D domains.
 This module relies heavily on `waveguide_2d` and mostly just transforms
 its parameters into 2D equivalents and expands the results back into 3D.
 """
-from typing import Sequence, Any
+from typing import Any
+from collections.abc import Sequence
 import numpy
 from numpy.typing import NDArray
 from numpy import complexfloating
diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py
index 91d8d29..0e90f2b 100644
--- a/meanas/fdmath/functional.py
+++ b/meanas/fdmath/functional.py
@@ -3,7 +3,8 @@ Math functions for finite difference simulations
 
 Basic discrete calculus etc.
 """
-from typing import Sequence, Callable
+from typing import TypeVar
+from collections.abc import Sequence, Callable
 
 import numpy
 from numpy.typing import NDArray
diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py
index 79ddfee..fe9847b 100644
--- a/meanas/fdmath/operators.py
+++ b/meanas/fdmath/operators.py
@@ -3,7 +3,7 @@ Matrix operators for finite difference simulations
 
 Basic discrete calculus etc.
 """
-from typing import Sequence
+from collections.abc import Sequence
 import numpy
 from numpy.typing import NDArray
 from numpy import floating
diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py
index b78e93f..bc678ea 100644
--- a/meanas/fdmath/types.py
+++ b/meanas/fdmath/types.py
@@ -1,7 +1,7 @@
 """
 Types shared across multiple submodules
 """
-from typing import Sequence, Callable, MutableSequence
+from collections.abc import Sequence, Callable, MutableSequence
 from numpy.typing import NDArray
 from numpy import floating, complexfloating
 
diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py
index 0a9f8ad..fef3c5e 100644
--- a/meanas/fdmath/vectorization.py
+++ b/meanas/fdmath/vectorization.py
@@ -4,7 +4,8 @@ and a 1D array representation of that field `[f_x0, f_x1, f_x2,... f_y0,... f_z0
 Vectorized versions of the field use row-major (ie., C-style) ordering.
 """
 
-from typing import overload, Sequence
+from typing import overload
+from collections.abc import Sequence
 import numpy
 from numpy.typing import ArrayLike
 
diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py
index 65d71e6..9c3aec5 100644
--- a/meanas/fdtd/pml.py
+++ b/meanas/fdtd/pml.py
@@ -7,7 +7,8 @@ PML implementations
 """
 # TODO retest pmls!
 
-from typing import Callable, Sequence, Any
+from typing import Any
+from collections.abc import Callable, Sequence
 from copy import deepcopy
 import numpy
 from numpy.typing import NDArray, DTypeLike

From e19968bb9f8eb24801c649362eac5bddeaee073e Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:30:00 -0700
Subject: [PATCH 337/437] linter-related test updates

---
 meanas/test/conftest.py      | 31 ++++++++++++++++---------------
 meanas/test/test_fdfd.py     | 22 +++++++++++-----------
 meanas/test/test_fdfd_pml.py | 34 +++++++++++++++++-----------------
 meanas/test/test_fdtd.py     | 20 ++++++++++----------
 meanas/test/utils.py         | 23 ++++++++++++-----------
 5 files changed, 66 insertions(+), 64 deletions(-)

diff --git a/meanas/test/conftest.py b/meanas/test/conftest.py
index 5dcdbff..9ce179c 100644
--- a/meanas/test/conftest.py
+++ b/meanas/test/conftest.py
@@ -3,7 +3,8 @@
 Test fixtures
 
 """
-from typing import Iterable, Any
+# ruff: noqa: ARG001
+from typing import Any
 import numpy
 from numpy.typing import NDArray
 import pytest       # type: ignore
@@ -20,18 +21,18 @@ FixtureRequest = Any
                         (5, 5, 5),
                         # (7, 7, 7),
                        ])
-def shape(request: FixtureRequest) -> Iterable[tuple[int, ...]]:
-    yield (3, *request.param)
+def shape(request: FixtureRequest) -> tuple[int, ...]:
+    return (3, *request.param)
 
 
 @pytest.fixture(scope='module', params=[1.0, 1.5])
-def epsilon_bg(request: FixtureRequest) -> Iterable[float]:
-    yield request.param
+def epsilon_bg(request: FixtureRequest) -> float:
+    return request.param
 
 
 @pytest.fixture(scope='module', params=[1.0, 2.5])
-def epsilon_fg(request: FixtureRequest) -> Iterable[float]:
-    yield request.param
+def epsilon_fg(request: FixtureRequest) -> float:
+    return request.param
 
 
 @pytest.fixture(scope='module', params=['center', '000', 'random'])
@@ -40,7 +41,7 @@ def epsilon(
         shape: tuple[int, ...],
         epsilon_bg: float,
         epsilon_fg: float,
-        ) -> Iterable[NDArray[numpy.float64]]:
+        ) -> NDArray[numpy.float64]:
     is3d = (numpy.array(shape) == 1).sum() == 0
     if is3d:
         if request.param == '000':
@@ -60,17 +61,17 @@ def epsilon(
                                   high=max(epsilon_bg, epsilon_fg),
                                   size=shape)
 
-    yield epsilon
+    return epsilon
 
 
 @pytest.fixture(scope='module', params=[1.0])  # 1.5
-def j_mag(request: FixtureRequest) -> Iterable[float]:
-    yield request.param
+def j_mag(request: FixtureRequest) -> float:
+    return request.param
 
 
 @pytest.fixture(scope='module', params=[1.0, 1.5])
-def dx(request: FixtureRequest) -> Iterable[float]:
-    yield request.param
+def dx(request: FixtureRequest) -> float:
+    return request.param
 
 
 @pytest.fixture(scope='module', params=['uniform', 'centerbig'])
@@ -78,7 +79,7 @@ def dxes(
         request: FixtureRequest,
         shape: tuple[int, ...],
         dx: float,
-        ) -> Iterable[list[list[NDArray[numpy.float64]]]]:
+        ) -> list[list[NDArray[numpy.float64]]]:
     if request.param == 'uniform':
         dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
     elif request.param == 'centerbig':
@@ -90,5 +91,5 @@ def dxes(
         dxe = [PRNG.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]]
         dxh = [(d + numpy.roll(d, -1)) / 2 for d in dxe]
         dxes = [dxe, dxh]
-    yield dxes
+    return dxes
 
diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py
index 009c65b..5df8e4f 100644
--- a/meanas/test/test_fdfd.py
+++ b/meanas/test/test_fdfd.py
@@ -1,4 +1,4 @@
-from typing import Iterable
+# ruff: noqa: ARG001
 import dataclasses
 import pytest       # type: ignore
 import numpy
@@ -61,24 +61,24 @@ def test_poynting_planes(sim: 'FDResult') -> None:
 # Also see conftest.py
 
 @pytest.fixture(params=[1 / 1500])
-def omega(request: FixtureRequest) -> Iterable[float]:
-    yield request.param
+def omega(request: FixtureRequest) -> float:
+    return request.param
 
 
 @pytest.fixture(params=[None])
-def pec(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
-    yield request.param
+def pec(request: FixtureRequest) -> NDArray[numpy.float64] | None:
+    return request.param
 
 
 @pytest.fixture(params=[None])
-def pmc(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
-    yield request.param
+def pmc(request: FixtureRequest) -> NDArray[numpy.float64] | None:
+    return request.param
 
 
 #@pytest.fixture(scope='module',
 #                params=[(25, 5, 5)])
-#def shape(request):
-#    yield (3, *request.param)
+#def shape(request: FixtureRequest):
+#    return (3, *request.param)
 
 
 @pytest.fixture(params=['diag'])        # 'center'
@@ -86,7 +86,7 @@ def j_distribution(
         request: FixtureRequest,
         shape: tuple[int, ...],
         j_mag: float,
-        ) -> Iterable[NDArray[numpy.float64]]:
+        ) -> NDArray[numpy.float64]:
     j = numpy.zeros(shape, dtype=complex)
     center_mask = numpy.zeros(shape, dtype=bool)
     center_mask[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = True
@@ -96,7 +96,7 @@ def j_distribution(
     elif request.param == 'diag':
         j[numpy.roll(center_mask, [1, 1, 1], axis=(1, 2, 3))] = (1 + 1j) * j_mag
         j[numpy.roll(center_mask, [-1, -1, -1], axis=(1, 2, 3))] = (1 - 1j) * j_mag
-    yield j
+    return j
 
 
 @dataclasses.dataclass()
diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py
index d752491..a443ef8 100644
--- a/meanas/test/test_fdfd_pml.py
+++ b/meanas/test/test_fdfd_pml.py
@@ -1,4 +1,4 @@
-from typing import Iterable
+# ruff: noqa: ARG001
 import pytest       # type: ignore
 import numpy
 from numpy.typing import NDArray
@@ -44,30 +44,30 @@ def test_pml(sim: FDResult, src_polarity: int) -> None:
 # Also see conftest.py
 
 @pytest.fixture(params=[1 / 1500])
-def omega(request: FixtureRequest) -> Iterable[float]:
-    yield request.param
+def omega(request: FixtureRequest) -> float:
+    return request.param
 
 
 @pytest.fixture(params=[None])
-def pec(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
-    yield request.param
+def pec(request: FixtureRequest) -> NDArray[numpy.float64] | None:
+    return request.param
 
 
 @pytest.fixture(params=[None])
-def pmc(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]:
-    yield request.param
+def pmc(request: FixtureRequest) -> NDArray[numpy.float64] | None:
+    return request.param
 
 
 @pytest.fixture(params=[(30, 1, 1),
                         (1, 30, 1),
                         (1, 1, 30)])
-def shape(request: FixtureRequest) -> Iterable[tuple[int, ...]]:
-    yield (3, *request.param)
+def shape(request: FixtureRequest) -> tuple[int, int, int]:
+    return (3, *request.param)
 
 
 @pytest.fixture(params=[+1, -1])
-def src_polarity(request: FixtureRequest) -> Iterable[int]:
-    yield request.param
+def src_polarity(request: FixtureRequest) -> int:
+    return request.param
 
 
 @pytest.fixture()
@@ -78,7 +78,7 @@ def j_distribution(
         dxes: dx_lists_mut,
         omega: float,
         src_polarity: int,
-        ) -> Iterable[NDArray[numpy.complex128]]:
+        ) -> NDArray[numpy.complex128]:
     j = numpy.zeros(shape, dtype=complex)
 
     dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0]    # Propagation axis
@@ -106,7 +106,7 @@ def j_distribution(
 
     j = fdfd.waveguide_3d.compute_source(E=e, wavenumber=wavenumber_corrected, omega=omega, dxes=dxes,
                                          axis=dim, polarity=src_polarity, slices=slices, epsilon=epsilon)
-    yield j
+    return j
 
 
 @pytest.fixture()
@@ -115,9 +115,9 @@ def epsilon(
         shape: tuple[int, ...],
         epsilon_bg: float,
         epsilon_fg: float,
-        ) -> Iterable[NDArray[numpy.float64]]:
+        ) -> NDArray[numpy.float64]:
     epsilon = numpy.full(shape, epsilon_fg, dtype=float)
-    yield epsilon
+    return epsilon
 
 
 @pytest.fixture(params=['uniform'])
@@ -127,7 +127,7 @@ def dxes(
         dx: float,
         omega: float,
         epsilon_fg: float,
-        ) -> Iterable[list[list[NDArray[numpy.float64]]]]:
+        ) -> list[list[NDArray[numpy.float64]]]:
     if request.param == 'uniform':
         dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
     dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0]    # Propagation axis
@@ -141,7 +141,7 @@ def dxes(
                 epsilon_effective=epsilon_fg,
                 thickness=10,
                 )
-    yield dxes
+    return dxes
 
 
 @pytest.fixture()
diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py
index 0a92c73..25ee891 100644
--- a/meanas/test/test_fdtd.py
+++ b/meanas/test/test_fdtd.py
@@ -1,4 +1,5 @@
-from typing import Iterable, Any
+# ruff: noqa: ARG001
+from typing import Any
 import dataclasses
 import pytest       # type: ignore
 import numpy
@@ -150,8 +151,8 @@ def test_poynting_planes(sim: 'TDResult') -> None:
 
 
 @pytest.fixture(params=[0.3])
-def dt(request: FixtureRequest) -> Iterable[float]:
-    yield request.param
+def dt(request: FixtureRequest) -> float:
+    return request.param
 
 
 @dataclasses.dataclass()
@@ -168,8 +169,8 @@ class TDResult:
 
 
 @pytest.fixture(params=[(0, 4, 8)])  # (0,)
-def j_steps(request: FixtureRequest) -> Iterable[tuple[int, ...]]:
-    yield request.param
+def j_steps(request: FixtureRequest) -> tuple[int, ...]:
+    return request.param
 
 
 @pytest.fixture(params=['center', 'random'])
@@ -177,7 +178,7 @@ def j_distribution(
         request: FixtureRequest,
         shape: tuple[int, ...],
         j_mag: float,
-        ) -> Iterable[NDArray[numpy.float64]]:
+        ) -> NDArray[numpy.float64]:
     j = numpy.zeros(shape)
     if request.param == 'center':
         j[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = j_mag
@@ -185,7 +186,7 @@ def j_distribution(
         j[:, 0, 0, 0] = j_mag
     elif request.param == 'random':
         j[:] = PRNG.uniform(low=-j_mag, high=j_mag, size=shape)
-    yield j
+    return j
 
 
 @pytest.fixture()
@@ -199,9 +200,8 @@ def sim(
         j_steps: tuple[int, ...],
         ) -> TDResult:
     is3d = (numpy.array(shape) == 1).sum() == 0
-    if is3d:
-        if dt != 0.3:
-            pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)')
+    if is3d and dt != 0.3:
+        pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)')
 
     sim = TDResult(
         shape=shape,
diff --git a/meanas/test/utils.py b/meanas/test/utils.py
index 00ed3f1..f6f9230 100644
--- a/meanas/test/utils.py
+++ b/meanas/test/utils.py
@@ -1,5 +1,3 @@
-from typing import Any
-
 import numpy
 from numpy.typing import NDArray
 
@@ -10,22 +8,25 @@ PRNG = numpy.random.RandomState(12345)
 def assert_fields_close(
         x: NDArray,
         y: NDArray,
-        *args: Any,
-        **kwargs: Any,
-        ) -> None:
-    numpy.testing.assert_allclose(
-        x, y, verbose=False,            # type: ignore
-        err_msg='Fields did not match:\n{}\n{}'.format(numpy.moveaxis(x, -1, 0),
-                                                       numpy.moveaxis(y, -1, 0)),
         *args,
         **kwargs,
+        ) -> None:
+    x_disp = numpy.moveaxis(x, -1, 0)
+    y_disp = numpy.moveaxis(y, -1, 0)
+    numpy.testing.assert_allclose(
+        x,           # type: ignore
+        y,           # type: ignore
+        *args,
+        verbose=False,
+        err_msg=f'Fields did not match:\n{x_disp}\n{y_disp}',
+        **kwargs,
         )
 
 def assert_close(
         x: NDArray,
         y: NDArray,
-        *args: Any,
-        **kwargs: Any,
+        *args,
+        **kwargs,
         ) -> None:
     numpy.testing.assert_allclose(x, y, *args, **kwargs)
 

From 43bb0ba379643b762769be0f18c8ea42e81f704f Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:31:16 -0700
Subject: [PATCH 338/437] use generators where applicable

---
 meanas/fdfd/bloch.py     | 8 ++++----
 meanas/fdfd/operators.py | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 5ea5e7b..427e1a5 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -232,7 +232,7 @@ def maxwell_operator(
             Raveled conv(1/mu_k, ik x conv(1/eps_k, ik x h_mn)), returned
             and overwritten in-place of `h`.
         """
-        hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
+        hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
 
         #{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis
 
@@ -303,7 +303,7 @@ def hmn_2_exyz(
     k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
     def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
-        hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
+        hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
         d_xyz = (n * hin_m
                - m * hin_n) * k_mag         # noqa: E128
 
@@ -341,7 +341,7 @@ def hmn_2_hxyz(
     _k_mag, m, n = generate_kmn(k0, G_matrix, shape)
 
     def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
-        hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
+        hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
         h_xyz = (m * hin_m
                + n * hin_n)     # noqa: E128
         return numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)])
@@ -394,7 +394,7 @@ def inverse_maxwell_operator_approx(
         Returns:
             Raveled ik x conv(eps_k, ik x conv(mu_k, h_mn))
         """
-        hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)]
+        hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
 
         #{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis
 
diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py
index 32e3af0..afa5fbd 100644
--- a/meanas/fdfd/operators.py
+++ b/meanas/fdfd/operators.py
@@ -321,11 +321,11 @@ def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
     """
     shape = [len(dx) for dx in dxes[0]]
 
-    fx, fy, fz = [shift_circ(i, shape, 1) for i in range(3)]
+    fx, fy, fz = (shift_circ(i, shape, 1) for i in range(3))
 
     dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
     dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
-    Ex, Ey, Ez = [ei * da for ei, da in zip(numpy.split(e, 3), dxag)]
+    Ex, Ey, Ez = (ei * da for ei, da in zip(numpy.split(e, 3), dxag, strict=True))
 
     block_diags = [[ None,     fx @ -Ez, fx @  Ey],
                    [ fy @  Ez, None,     fy @ -Ex],
@@ -349,11 +349,11 @@ def poynting_h_cross(h: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
     """
     shape = [len(dx) for dx in dxes[0]]
 
-    fx, fy, fz = [shift_circ(i, shape, 1) for i in range(3)]
+    fx, fy, fz = (shift_circ(i, shape, 1) for i in range(3))
 
     dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
     dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
-    Hx, Hy, Hz = [sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg)]
+    Hx, Hy, Hz = (sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg, strict=True))
 
     P = (sparse.bmat(
         [[ None,    -Hz @ fx,   Hy @ fx],

From 3f8802cb5fb635c05e6f0b474b333f16b6961246 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:31:44 -0700
Subject: [PATCH 339/437] use strict zip

---
 meanas/fdmath/operators.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py
index fe9847b..b5cd8fc 100644
--- a/meanas/fdmath/operators.py
+++ b/meanas/fdmath/operators.py
@@ -35,7 +35,7 @@ def shift_circ(
         raise Exception(f'Invalid direction: {axis}, shape is {shape}')
 
     shifts = [abs(shift_distance) if a == axis else 0 for a in range(3)]
-    shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts)]
+    shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts, strict=True)]
     ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
 
     n = numpy.prod(shape)
@@ -83,7 +83,7 @@ def shift_with_mirror(
         return v
 
     shifts = [shift_distance if a == axis else 0 for a in range(3)]
-    shifted_diags = [mirrored_range(n, s) for n, s in zip(shape, shifts)]
+    shifted_diags = [mirrored_range(n, s) for n, s in zip(shape, shifts, strict=True)]
     ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
 
     n = numpy.prod(shape)

From 95e923d7b71f46f2e7b36b616c1b29e219275811 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:32:03 -0700
Subject: [PATCH 340/437] improve error handling

---
 meanas/fdfd/bloch.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 427e1a5..cd2ae16 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -657,7 +657,7 @@ def eigsolve(
             Q = c * c * ZtZ + s * s * DtD + 2 * s * c * symZtD
             try:
                 Qi = numpy.linalg.inv(Q)
-            except numpy.linalg.LinAlgError:
+            except numpy.linalg.LinAlgError as err:
                 logger.info('taylor Qi')
                 # if c or s small, taylor expand
                 if c < 1e-4 * s and c != 0:
@@ -667,7 +667,7 @@ def eigsolve(
                     ZtZi = numpy.linalg.inv(ZtZ)
                     Qi = ZtZi / (c * c) - 2 * s / (c * c * c) * (ZtZi @ (ZtZi @ symZtD).conj().T)
                 else:
-                    raise Exception('Inexplicable singularity in trace_func')
+                    raise Exception('Inexplicable singularity in trace_func') from err
             Qi_memo[0] = theta
             Qi_memo[1] = cast(float, Qi)
             return cast(float, Qi)

From 1021768e30f6cf50243a9971bde4bffe1d1baac7 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:32:20 -0700
Subject: [PATCH 341/437] simplify indentation

---
 meanas/fdfd/functional.py | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py
index 8b21923..f4a250f 100644
--- a/meanas/fdfd/functional.py
+++ b/meanas/fdfd/functional.py
@@ -47,8 +47,7 @@ def e_full(
 
     if mu is None:
         return op_1
-    else:
-        return op_mu
+    return op_mu
 
 
 def eh_full(
@@ -84,8 +83,7 @@ def eh_full(
 
     if mu is None:
         return op_1
-    else:
-        return op_mu
+    return op_mu
 
 
 def e2h(
@@ -116,8 +114,7 @@ def e2h(
 
     if mu is None:
         return e2h_1_1
-    else:
-        return e2h_mu
+    return e2h_mu
 
 
 def m2j(
@@ -151,8 +148,7 @@ def m2j(
 
     if mu is None:
         return m2j_1
-    else:
-        return m2j_mu
+    return m2j_mu
 
 
 def e_tfsf_source(

From 5dd9994e761c4026254b97614341b3e810246edd Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:32:52 -0700
Subject: [PATCH 342/437] improve some type annotations

---
 meanas/fdmath/functional.py | 13 ++++++++-----
 meanas/fdtd/pml.py          |  2 +-
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py
index 0e90f2b..1b5811d 100644
--- a/meanas/fdmath/functional.py
+++ b/meanas/fdmath/functional.py
@@ -8,7 +8,7 @@ from collections.abc import Sequence, Callable
 
 import numpy
 from numpy.typing import NDArray
-from numpy import floating
+from numpy import floating, complexfloating
 
 from .types import fdfield_t, fdfield_updater_t
 
@@ -61,9 +61,12 @@ def deriv_back(
     return derivs
 
 
+TT = TypeVar('TT', bound='NDArray[floating | complexfloating]')
+
+
 def curl_forward(
         dx_e: Sequence[NDArray[floating]] | None = None,
-        ) -> fdfield_updater_t:
+        ) -> Callable[[TT], TT]:
     r"""
     Curl operator for use with the E field.
 
@@ -77,7 +80,7 @@ def curl_forward(
     """
     Dx, Dy, Dz = deriv_forward(dx_e)
 
-    def ce_fun(e: fdfield_t) -> fdfield_t:
+    def ce_fun(e: TT) -> TT:
         output = numpy.empty_like(e)
         output[0] = Dy(e[2])
         output[1] = Dz(e[0])
@@ -92,7 +95,7 @@ def curl_forward(
 
 def curl_back(
         dx_h: Sequence[NDArray[floating]] | None = None,
-        ) -> fdfield_updater_t:
+        ) -> Callable[[TT], TT]:
     r"""
     Create a function which takes the backward curl of a field.
 
@@ -106,7 +109,7 @@ def curl_back(
     """
     Dx, Dy, Dz = deriv_back(dx_h)
 
-    def ch_fun(h: fdfield_t) -> fdfield_t:
+    def ch_fun(h: TT) -> TT:
         output = numpy.empty_like(h)
         output[0] = Dy(h[2])
         output[1] = Dz(h[0])
diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py
index 9c3aec5..7678808 100644
--- a/meanas/fdtd/pml.py
+++ b/meanas/fdtd/pml.py
@@ -185,7 +185,7 @@ def updates_with_cpml(
     def update_H(
             e: fdfield_t,
             h: fdfield_t,
-            mu: fdfield_t = numpy.ones(3),
+            mu: fdfield_t | tuple[int, int, int] = (1, 1, 1),
             ) -> None:
         dyEx = Dfy(e[0])
         dzEx = Dfz(e[0])

From c53a3c4d8486904d03db7ff76fe0fa62e820696d Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:33:43 -0700
Subject: [PATCH 343/437] unused var

---
 meanas/fdtd/pml.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py
index 7678808..8098da0 100644
--- a/meanas/fdtd/pml.py
+++ b/meanas/fdtd/pml.py
@@ -112,7 +112,7 @@ def updates_with_cpml(
     params_H: list[list[tuple[Any, Any, Any, Any]]] = deepcopy(params_E)
 
     for axis in range(3):
-        for pp, polarity in enumerate((-1, 1)):
+        for pp, _polarity in enumerate((-1, 1)):
             cpml_param = cpml_params[axis][pp]
             if cpml_param is None:
                 psi_E[axis][pp] = (None, None)

From 63e7cb949ff43753df5af6f7398b4bff109f79fd Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:33:58 -0700
Subject: [PATCH 344/437] explicitly specify closed variables

---
 meanas/fdfd/bloch.py | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index cd2ae16..516dcf6 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -647,8 +647,7 @@ def eigsolve(
 
         Qi_memo: list[float | None] = [None, None]
 
-        def Qi_func(theta: float) -> float:
-            nonlocal Qi_memo
+        def Qi_func(theta: float, Qi_memo=Qi_memo, ZtZ=ZtZ, DtD=DtD, symZtD=symZtD) -> float:   # noqa: ANN001
             if Qi_memo[0] == theta:
                 return cast(float, Qi_memo[1])
 
@@ -672,7 +671,7 @@ def eigsolve(
             Qi_memo[1] = cast(float, Qi)
             return cast(float, Qi)
 
-        def trace_func(theta: float) -> float:
+        def trace_func(theta: float, ZtAZ=ZtAZ, DtAD=DtAD, symZtAD=symZtAD) -> float:           # noqa: ANN001
             c = numpy.cos(theta)
             s = numpy.sin(theta)
             Qi = Qi_func(theta)
@@ -681,7 +680,7 @@ def eigsolve(
             return numpy.abs(trace)
 
         if False:
-            def trace_deriv(theta):
+            def trace_deriv(theta, sgn: int = sgn, ZtAZ=ZtAZ, DtAD=DtAD, symZtD=symZtD, symZtAD=symZtAD, ZtZ=ZtZ, DtD=DtD):     # noqa: ANN001
                 Qi = Qi_func(theta)
                 c2 = numpy.cos(2 * theta)
                 s2 = numpy.sin(2 * theta)

From 739e96df3d9fb83b4cb62c26867a68fadfbf69eb Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 00:34:17 -0700
Subject: [PATCH 345/437] avoid a copy

---
 meanas/fdfd/bloch.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 516dcf6..2f4a002 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -308,7 +308,7 @@ def hmn_2_exyz(
                - m * hin_n) * k_mag         # noqa: E128
 
         # divide by epsilon
-        return numpy.array([ei for ei in numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0)])         # TODO avoid copy
+        return numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0)
 
     return operator
 

From 36431cd0e462a98ebd4cef93049d944ba922bef1 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Mon, 29 Jul 2024 02:25:16 -0700
Subject: [PATCH 346/437] enable numpy 2.0 and recent scipy

---
 meanas/fdfd/bloch.py         | 6 +++---
 meanas/test/test_fdfd.py     | 2 +-
 meanas/test/test_fdfd_pml.py | 2 +-
 pyproject.toml               | 4 ++--
 4 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 2f4a002..2e1da30 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -155,7 +155,7 @@ def generate_kmn(
             All are given in the xyz basis (e.g. `|k|[0,0,0] = norm(G_matrix @ k0)`).
     """
     k0 = numpy.array(k0)
-    G_matrix = numpy.array(G_matrix, copy=False)
+    G_matrix = numpy.asarray(G_matrix)
 
     Gi_grids = numpy.array(numpy.meshgrid(*(fftfreq(n, 1 / n) for n in shape[:3]), indexing='ij'))
     Gi = numpy.moveaxis(Gi_grids, 0, -1)
@@ -538,7 +538,7 @@ def eigsolve(
         `(eigenvalues, eigenvectors)` where `eigenvalues[i]` corresponds to the
         vector `eigenvectors[i, :]`
     """
-    k0 = numpy.array(k0, copy=False)
+    k0 = numpy.asarray(k0)
 
     h_size = 2 * epsilon[0].size
 
@@ -566,7 +566,7 @@ def eigsolve(
     if y0 is None:
         Z = rng.random(y_shape) + 1j * rng.random(y_shape)
     else:
-        Z = numpy.array(y0, copy=False).T
+        Z = numpy.asarray(y0).T
 
     while True:
         Z *= num_modes / norm(Z)
diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py
index 5df8e4f..5f2cf11 100644
--- a/meanas/test/test_fdfd.py
+++ b/meanas/test/test_fdfd.py
@@ -145,7 +145,7 @@ def sim(
         omega=omega,
         dxes=dxes,
         epsilon=eps_vec,
-        matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11},
+        matrix_solver_opts={'atol': 1e-15, 'rtol': 1e-11},
         )
     e = unvec(e_vec, shape[1:])
 
diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py
index a443ef8..832053d 100644
--- a/meanas/test/test_fdfd_pml.py
+++ b/meanas/test/test_fdfd_pml.py
@@ -162,7 +162,7 @@ def sim(
         omega=omega,
         dxes=dxes,
         epsilon=eps_vec,
-        matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11},
+        matrix_solver_opts={'atol': 1e-15, 'rtol': 1e-11},
         )
     e = unvec(e_vec, shape[1:])
 
diff --git a/pyproject.toml b/pyproject.toml
index 741ae48..a6d31bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,8 +39,8 @@ include = [
     ]
 dynamic = ["version"]
 dependencies = [
-    "numpy~=1.26",
-    "scipy",
+    "numpy>=1.26",
+    "scipy~=1.14",
     ]
 
 

From e459b5e61f50176bad009947ba418382ee637240 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 7 Jan 2025 00:04:01 -0800
Subject: [PATCH 347/437] clean up comments and some types

---
 meanas/fdfd/operators.py    |  2 +-
 meanas/fdfd/solvers.py      | 13 ++++++-------
 meanas/fdfd/waveguide_2d.py | 16 ++++++++--------
 meanas/fdfd/waveguide_3d.py | 13 +++++++------
 meanas/fdmath/types.py      |  4 ++--
 5 files changed, 24 insertions(+), 24 deletions(-)

diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py
index afa5fbd..8c16ef7 100644
--- a/meanas/fdfd/operators.py
+++ b/meanas/fdfd/operators.py
@@ -40,7 +40,7 @@ __author__ = 'Jan Petykiewicz'
 def e_full(
         omega: complex,
         dxes: dx_lists_t,
-        epsilon: vfdfield_t,
+        epsilon: vfdfield_t | vcfdfield_t,
         mu: vfdfield_t | None = None,
         pec: vfdfield_t | None = None,
         pmc: vfdfield_t | None = None,
diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py
index 517ecab..5b48493 100644
--- a/meanas/fdfd/solvers.py
+++ b/meanas/fdfd/solvers.py
@@ -35,9 +35,9 @@ def _scipy_qmr(
         Guess for solution (returned even if didn't converge)
     """
 
-    '''
-    Report on our progress
-    '''
+    #
+    #Report on our progress
+    #
     ii = 0
 
     def log_residual(xk: ArrayLike) -> None:
@@ -56,10 +56,9 @@ def _scipy_qmr(
     else:
         kwargs['callback'] = log_residual
 
-    '''
-    Run the actual solve
-    '''
-
+    #
+    # Run the actual solve
+    #
     x, _ = scipy.sparse.linalg.qmr(A, b, **kwargs)
     return x
 
diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 32e65bc..05215c4 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -845,13 +845,13 @@ def solve_modes(
             ability to find the correct mode. Default 2.
 
     Returns:
-        e_xys: list of vfdfield_t specifying fields
+        e_xys: NDArray of vfdfield_t specifying fields. First dimension is mode number.
         wavenumbers: list of wavenumbers
     """
 
-    '''
-    Solve for the largest-magnitude eigenvalue of the real operator
-    '''
+    #
+    # Solve for the largest-magnitude eigenvalue of the real operator
+    #
     dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
     mu_real = None if mu is None else numpy.real(mu)
     A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), mu_real)
@@ -859,10 +859,10 @@ def solve_modes(
     eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
     e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)]
 
-    '''
-    Now solve for the eigenvector of the full operator, using the real operator's
-     eigenvector as an initial guess for Rayleigh quotient iteration.
-    '''
+    #
+    # Now solve for the eigenvector of the full operator, using the real operator's
+    #  eigenvector as an initial guess for Rayleigh quotient iteration.
+    #
     A = operator_e(omega, dxes, epsilon, mu)
     for nn in range(len(mode_numbers)):
         eigvals[nn], e_xys[:, nn] = rayleigh_quotient_iteration(A, e_xys[:, nn])
diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py
index 3cffa94..8bb0513 100644
--- a/meanas/fdfd/waveguide_3d.py
+++ b/meanas/fdfd/waveguide_3d.py
@@ -53,9 +53,9 @@ def solve_mode(
 
     slices = tuple(slices)
 
-    '''
-    Solve the 2D problem in the specified plane
-    '''
+    #
+    # Solve the 2D problem in the specified plane
+    #
     # Define rotation to set z as propagation direction
     order = numpy.roll(range(3), 2 - axis)
     reverse_order = numpy.roll(range(3), axis - 2)
@@ -73,9 +73,10 @@ def solve_mode(
     }
     e_xy, wavenumber_2d = waveguide_2d.solve_mode(mode_number, **args_2d)
 
-    '''
-    Apply corrections and expand to 3D
-    '''
+    #
+    # Apply corrections and expand to 3D
+    #
+
     # Correct wavenumber to account for numerical dispersion.
     wavenumber = 2 / dx_prop * numpy.arcsin(wavenumber_2d * dx_prop / 2)
 
diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py
index bc678ea..d44b30a 100644
--- a/meanas/fdmath/types.py
+++ b/meanas/fdmath/types.py
@@ -20,7 +20,7 @@ vcfdfield_t = NDArray[complexfloating]
 """Linearized complex vector field (single vector of length 3*X*Y*Z)"""
 
 
-dx_lists_t = Sequence[Sequence[NDArray[floating]]]
+dx_lists_t = Sequence[Sequence[NDArray[floating | complexfloating]]]
 """
  'dxes' datastructure which contains grid cell width information in the following format:
 
@@ -31,7 +31,7 @@ dx_lists_t = Sequence[Sequence[NDArray[floating]]]
    and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc.
 """
 
-dx_lists_mut = MutableSequence[MutableSequence[NDArray[floating]]]
+dx_lists_mut = MutableSequence[MutableSequence[NDArray[floating | complexfloating]]]
 """Mutable version of `dx_lists_t`"""
 
 

From 47415a0beb01c3dcadae58a64c65eee2322ec890 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 7 Jan 2025 00:04:53 -0800
Subject: [PATCH 348/437] Return list-of-vectors from waveguide mode solve

---
 meanas/fdfd/waveguide_2d.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 05215c4..8ea4846 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -179,6 +179,7 @@ to account for numerical dispersion if the result is introduced into a space wit
 # TODO update module docs
 
 from typing import Any
+from collections.abc import Sequence
 import numpy
 from numpy.typing import NDArray, ArrayLike
 from numpy.linalg import norm
@@ -871,7 +872,7 @@ def solve_modes(
     wavenumbers = numpy.sqrt(eigvals)
     wavenumbers *= numpy.sign(numpy.real(wavenumbers))
 
-    return e_xys, wavenumbers
+    return e_xys.T, wavenumbers
 
 
 def solve_mode(
@@ -892,4 +893,4 @@ def solve_mode(
     """
     kwargs['mode_numbers'] = [mode_number]
     e_xys, wavenumbers = solve_modes(*args, **kwargs)
-    return e_xys[:, 0], wavenumbers[0]
+    return e_xys[0], wavenumbers[0]

From 4f2433320deacd441991653c647fabea7f7a8e92 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 7 Jan 2025 00:05:19 -0800
Subject: [PATCH 349/437] fix zip(strict=True) for 2D problems

---
 meanas/fdmath/operators.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py
index b5cd8fc..5d50670 100644
--- a/meanas/fdmath/operators.py
+++ b/meanas/fdmath/operators.py
@@ -34,7 +34,7 @@ def shift_circ(
     if axis not in range(len(shape)):
         raise Exception(f'Invalid direction: {axis}, shape is {shape}')
 
-    shifts = [abs(shift_distance) if a == axis else 0 for a in range(3)]
+    shifts = [abs(shift_distance) if a == axis else 0 for a in range(len(shape))]
     shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts, strict=True)]
     ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
 
@@ -82,7 +82,7 @@ def shift_with_mirror(
         v = numpy.where(v < 0, - 1 - v, v)
         return v
 
-    shifts = [shift_distance if a == axis else 0 for a in range(3)]
+    shifts = [shift_distance if a == axis else 0 for a in range(len(shape))]
     shifted_diags = [mirrored_range(n, s) for n, s in zip(shape, shifts, strict=True)]
     ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
 

From e54735d9c6de57d032aca8bf9be52c8672e60a2c Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 7 Jan 2025 00:10:15 -0800
Subject: [PATCH 350/437] Fix cylindrical waveguide module

- Properly account for rmin vs r0
- Change return values to match waveguide_2d
- Change operator definition to look more like waveguide_2d

remaining TODO:
- Fix docs
- Further consolidate operators vs waveguide_2d
- Figure out E/H field conversions
---
 meanas/fdfd/waveguide_cyl.py | 129 ++++++++++++++++++++---------------
 1 file changed, 74 insertions(+), 55 deletions(-)

diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 65778ba..f9e2570 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -8,10 +8,14 @@ As the z-dependence is known, all the functions in this file assume a 2D grid
 """
 # TODO update module docs
 
+from typing import Any
+from collections.abc import Sequence
+
 import numpy
+from numpy.typing import NDArray, ArrayLike
 from scipy import sparse
 
-from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, cfdfield_t
+from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t
 from ..fdmath.operators import deriv_forward, deriv_back
 from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
 
@@ -21,6 +25,7 @@ def cylindrical_operator(
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
         r0: float,
+        rmin: float,
         ) -> sparse.spmatrix:
     """
     Cylindrical coordinate waveguide operator of the form
@@ -42,8 +47,8 @@ def cylindrical_operator(
         omega: The angular frequency of the system
         dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
         epsilon: Vectorized dielectric constant grid
-        r0: Radius of curvature for the simulation. This should be the minimum value of
-            r within the simulation domain.
+        r0: Radius of curvature at x=0
+        rmin: Radius at the left edge of the simulation domain
 
     Returns:
         Sparse matrix representation of the operator
@@ -52,44 +57,52 @@ def cylindrical_operator(
     Dfx, Dfy = deriv_forward(dxes[0])
     Dbx, Dby = deriv_back(dxes[1])
 
-    rx = r0 + numpy.cumsum(dxes[0][0])
-    ry = r0 + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0])
-    tx = rx / r0
-    ty = ry / r0
+    ra = rmin + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0])   # Radius at Ex points
+    rb = rmin + numpy.cumsum(dxes[0][0])                      # Radius at Ey points
+    ta = ra / r0
+    tb = rb / r0
 
-    Tx = sparse.diags(vec(tx[:, None].repeat(dxes[0][1].size, axis=1)))
-    Ty = sparse.diags(vec(ty[:, None].repeat(dxes[1][1].size, axis=1)))
+    Ta = sparse.diags(vec(ta[:, None].repeat(dxes[0][1].size, axis=1)))
+    Tb = sparse.diags(vec(tb[:, None].repeat(dxes[1][1].size, axis=1)))
 
     eps_parts = numpy.split(epsilon, 3)
     eps_x = sparse.diags(eps_parts[0])
     eps_y = sparse.diags(eps_parts[1])
     eps_z_inv = sparse.diags(1 / eps_parts[2])
 
-    pa = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dbx, Dby))
-    pb = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dby, Dbx))
-    a0 = Ty @ eps_x + omega**-2 * Dby @ Ty @ Dfy
-    a1 = Tx @ eps_y + omega**-2 * Dbx @ Ty @ Dfx
-    b0 = Dbx @ Ty @ Dfy
-    b1 = Dby @ Ty @ Dfx
-
+    omega2 = omega * omega
     diag = sparse.block_diag
 
-    omega2 = omega * omega
+    sq0 = omega2 * diag((Tb @ Tb @ eps_x,
+                         Ta @ Ta @ eps_y))
+    lin0 = sparse.vstack((-Tb @ Dby, Ta @ Dbx)) @ Tb @ sparse.hstack((-Dfy, Dfx))
+    lin1 = sparse.vstack((Dfx, Dfy)) @ Ta @ eps_z_inv @ sparse.hstack((Dbx @ Tb @ eps_x,
+                                                                       Dby @ Ta @ eps_y))
+    # op = (
+    #     # E
+    #     omega * omega * mu_yx @ eps_xy
+    #     + mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx))
+    #     + sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy
 
-    op = (
-        (omega2 * diag((Tx, Ty)) + pa) @ diag((a0, a1))
-        - (sparse.bmat(((None, Ty), (Tx, None))) + pb / omega2) @ diag((b0, b1))
-        )
+    #     # H
+    #     omega * omega * eps_yx @ mu_xy
+    #     + eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx))
+    #     + sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy
+    #     )
+
+    op = sq0 + lin0 + lin1
     return op
 
 
-def solve_mode(
-        mode_number: int,
+def solve_modes(
+        mode_numbers: Sequence[int],
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
         r0: float,
-        ) -> dict[str, complex | cfdfield_t]:
+        rmin: float,
+        mode_margin: int = 2,
+        ) -> tuple[vcfdfield_t, NDArray[numpy.complex64]]:
     """
     TODO: fixup
     Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
@@ -105,44 +118,50 @@ def solve_mode(
             r within the simulation domain.
 
     Returns:
-        ```
-        {
-            'E': list[NDArray[numpy.complex_]],
-            'H': list[NDArray[numpy.complex_]],
-            'wavenumber': complex,
-        }
-        ```
+        e_xys: NDArray of vfdfield_t specifying fields. First dimension is mode number.
+        wavenumbers: list of wavenumbers
     """
 
-    '''
-    Solve for the largest-magnitude eigenvalue of the real operator
-    '''
+    #
+    # Solve for the largest-magnitude eigenvalue of the real operator
+    #
     dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
 
-    A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0)
-    eigvals, eigvecs = signed_eigensolve(A_r, mode_number + 3)
-    e_xy = eigvecs[:, -(mode_number + 1)]
+    A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0=r0, rmin=rmin)
+    eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
+    e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)].T
 
-    '''
-    Now solve for the eigenvector of the full operator, using the real operator's
-     eigenvector as an initial guess for Rayleigh quotient iteration.
-    '''
-    A = cylindrical_operator(omega, dxes, epsilon, r0)
-    eigval, e_xy = rayleigh_quotient_iteration(A, e_xy)
+    #
+    # Now solve for the eigenvector of the full operator, using the real operator's
+    #  eigenvector as an initial guess for Rayleigh quotient iteration.
+    #
+    A = cylindrical_operator(omega, dxes, epsilon, r0=r0, rmin=rmin)
+    for nn in range(len(mode_numbers)):
+        eigvals[nn], e_xys[nn, :] = rayleigh_quotient_iteration(A, e_xys[nn, :])
 
     # Calculate the wave-vector (force the real part to be positive)
-    wavenumber = numpy.sqrt(eigval)
-    wavenumber *= numpy.sign(numpy.real(wavenumber))
+    wavenumbers = numpy.sqrt(eigvals)
+    wavenumbers *= numpy.sign(numpy.real(wavenumbers))
 
-    # TODO: Perform correction on wavenumber to account for numerical dispersion.
+    return e_xys, wavenumbers
 
-    shape = [d.size for d in dxes[0]]
-    e_xy = numpy.hstack((e_xy, numpy.zeros(shape[0] * shape[1])))
-    fields = {
-        'wavenumber': wavenumber,
-        'E': unvec(e_xy, shape),
-        # 'E': unvec(e, shape),
-        # 'H': unvec(h, shape),
-    }
 
-    return fields
+def solve_mode(
+        mode_number: int,
+        *args: Any,
+        **kwargs: Any,
+        ) -> tuple[vcfdfield_t, complex]:
+    """
+    Wrapper around `solve_modes()` that solves for a single mode.
+
+    Args:
+       mode_number: 0-indexed mode number to solve for
+       *args: passed to `solve_modes()`
+       **kwargs: passed to `solve_modes()`
+
+    Returns:
+        (e_xy, wavenumber)
+    """
+    kwargs['mode_numbers'] = [mode_number]
+    e_xys, wavenumbers = solve_modes(*args, **kwargs)
+    return e_xys[0], wavenumbers[0]

From c543868c0b016ecd522537b68a25744978376f21 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 21:51:32 -0800
Subject: [PATCH 351/437] check for sign=0 case

---
 meanas/fdfd/waveguide_2d.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 8ea4846..f2306c1 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -435,6 +435,7 @@ def _normalized_fields(
     sign = numpy.sign(E_weighted[:,
                                  :max(shape[0] // 2, 1),
                                  :max(shape[1] // 2, 1)].real.sum())
+    assert sign != 0
 
     norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle)
 

From b3c2fd391b2c2567cb1a5f6fb3520a1a923350b1 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 21:57:54 -0800
Subject: [PATCH 352/437] [waveguide_2d] Return modes sorted by wavenumber
 (descending)

---
 meanas/fdfd/waveguide_2d.py | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index f2306c1..8bc57a1 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -859,7 +859,9 @@ def solve_modes(
     A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), mu_real)
 
     eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
-    e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)]
+    keep_inds = -(numpy.array(mode_numbers) + 1)
+    e_xys = eigvecs[:, keep_inds].T
+    eigvals = eigvals[keep_inds]
 
     #
     # Now solve for the eigenvector of the full operator, using the real operator's
@@ -867,13 +869,17 @@ def solve_modes(
     #
     A = operator_e(omega, dxes, epsilon, mu)
     for nn in range(len(mode_numbers)):
-        eigvals[nn], e_xys[:, nn] = rayleigh_quotient_iteration(A, e_xys[:, nn])
+        eigvals[nn], e_xys[nn, :] = rayleigh_quotient_iteration(A, e_xys[nn, :])
 
     # Calculate the wave-vector (force the real part to be positive)
     wavenumbers = numpy.sqrt(eigvals)
     wavenumbers *= numpy.sign(numpy.real(wavenumbers))
 
-    return e_xys.T, wavenumbers
+    order = wavenumbers.argsort()[::-1]
+    e_xys = e_xys[order]
+    wavenumbers = wavenumbers[order]
+
+    return e_xys, wavenumbers
 
 
 def solve_mode(

From 50f92e1cc853967fe7b56b043b770ce31a40080b Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 21:58:46 -0800
Subject: [PATCH 353/437] [vectorization] add nvdim arg allowing unvec() on 2D
 fields

---
 meanas/fdmath/vectorization.py | 29 ++++++++++++++++++-----------
 1 file changed, 18 insertions(+), 11 deletions(-)

diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py
index fef3c5e..3871801 100644
--- a/meanas/fdmath/vectorization.py
+++ b/meanas/fdmath/vectorization.py
@@ -28,14 +28,16 @@ def vec(f: cfdfield_t) -> vcfdfield_t:
 def vec(f: ArrayLike) -> vfdfield_t | vcfdfield_t:
     pass
 
-def vec(f: fdfield_t | cfdfield_t | ArrayLike | None) -> vfdfield_t | vcfdfield_t | None:
+def vec(
+        f: fdfield_t | cfdfield_t | ArrayLike | None,
+        ) -> vfdfield_t | vcfdfield_t | None:
     """
-    Create a 1D ndarray from a 3D vector field which spans a 1-3D region.
+    Create a 1D ndarray from a vector field which spans a 1-3D region.
 
     Returns `None` if called with `f=None`.
 
     Args:
-        f: A vector field, `[f_x, f_y, f_z]` where each `f_` component is a 1- to
+        f: A vector field, e.g. `[f_x, f_y, f_z]` where each `f_` component is a 1- to
            3-D ndarray (`f_*` should all be the same size). Doesn't fail with `f=None`.
 
     Returns:
@@ -47,33 +49,38 @@ def vec(f: fdfield_t | cfdfield_t | ArrayLike | None) -> vfdfield_t | vcfdfield_
 
 
 @overload
-def unvec(v: None, shape: Sequence[int]) -> None:
+def unvec(v: None, shape: Sequence[int], nvdim: int) -> None:
     pass
 
 @overload
-def unvec(v: vfdfield_t, shape: Sequence[int]) -> fdfield_t:
+def unvec(v: vfdfield_t, shape: Sequence[int], nvdim: int) -> fdfield_t:
     pass
 
 @overload
-def unvec(v: vcfdfield_t, shape: Sequence[int]) -> cfdfield_t:
+def unvec(v: vcfdfield_t, shape: Sequence[int], nvdim: int) -> cfdfield_t:
     pass
 
-def unvec(v: vfdfield_t | vcfdfield_t | None, shape: Sequence[int]) -> fdfield_t | cfdfield_t | None:
+def unvec(
+        v: vfdfield_t | vcfdfield_t | None,
+        shape: Sequence[int],
+        nvdim: int = 3,
+        ) -> fdfield_t | cfdfield_t | None:
     """
-    Perform the inverse of vec(): take a 1D ndarray and output a 3D field
-     of form `[f_x, f_y, f_z]` where each of `f_*` is a len(shape)-dimensional
+    Perform the inverse of vec(): take a 1D ndarray and output an `nvdim`-component field
+     of form e.g. `[f_x, f_y, f_z]` (`nvdim=3`) where each of `f_*` is a len(shape)-dimensional
      ndarray.
 
     Returns `None` if called with `v=None`.
 
     Args:
-        v: 1D ndarray representing a 3D vector field of shape shape (or None)
+        v: 1D ndarray representing a vector field of shape shape (or None)
         shape: shape of the vector field
+        nvdim: Number of components in each vector
 
     Returns:
         `[f_x, f_y, f_z]` where each `f_` is a `len(shape)` dimensional ndarray (or `None`)
     """
     if v is None:
         return None
-    return v.reshape((3, *shape), order='C')
+    return v.reshape((nvdim, *shape), order='C')
 

From 4e3a163522e3b8d0b23f20294055b8250bc25bd8 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 21:59:12 -0800
Subject: [PATCH 354/437] indentation & style

---
 examples/fdfd.py | 56 ++++++++++++++++++++++++++----------------------
 1 file changed, 30 insertions(+), 26 deletions(-)

diff --git a/examples/fdfd.py b/examples/fdfd.py
index 4612ba0..e28a2d2 100644
--- a/examples/fdfd.py
+++ b/examples/fdfd.py
@@ -46,20 +46,24 @@ def test0(solver=generic_solver):
     # #### Create the grid, mask, and draw the device ####
     grid = gridlock.Grid(edge_coords)
     epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
-    grid.draw_cylinder(epsilon,
-                       surface_normal=2,
-                       center=center,
-                       radius=max(radii),
-                       thickness=th,
-                       eps=n_ring**2,
-                       num_points=24)
-    grid.draw_cylinder(epsilon,
-                       surface_normal=2,
-                       center=center,
-                       radius=min(radii),
-                       thickness=th*1.1,
-                       eps=n_air ** 2,
-                       num_points=24)
+    grid.draw_cylinder(
+        epsilon,
+        surface_normal=2,
+        center=center,
+        radius=max(radii),
+        thickness=th,
+        foreground=n_ring**2,
+        num_points=24,
+        )
+    grid.draw_cylinder(
+        epsilon,
+        surface_normal=2,
+        center=center,
+        radius=min(radii),
+        thickness=th*1.1,
+        foreground=n_air ** 2,
+        num_points=24,
+        )
 
     dxes = [grid.dxyz, grid.autoshifted_dxyz()]
     for a in (0, 1, 2):
@@ -71,9 +75,9 @@ def test0(solver=generic_solver):
     J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1
 
 
-    '''
-    Solve!
-    '''
+    #
+    # Solve!
+    #
     sim_args = {
         'omega': omega,
         'dxes': dxes,
@@ -87,9 +91,9 @@ def test0(solver=generic_solver):
 
     E = unvec(x, grid.shape)
 
-    '''
-    Plot results
-    '''
+    #
+    # Plot results
+    #
     pyplot.figure()
     pyplot.pcolor(numpy.real(E[1][:, :, grid.shape[2]//2]), cmap='seismic')
     pyplot.axis('equal')
@@ -169,9 +173,9 @@ def test1(solver=generic_solver):
 #    pcolor((numpy.abs(J3).sum(axis=2).sum(axis=0) > 0).astype(float).T)
     pyplot.show(block=True)
 
-    '''
-    Solve!
-    '''
+    #
+    # Solve!
+    #
     sim_args = {
         'omega': omega,
         'dxes': dxes,
@@ -188,9 +192,9 @@ def test1(solver=generic_solver):
 
     E = unvec(x, grid.shape)
 
-    '''
-    Plot results
-    '''
+    #
+    # Plot results
+    #
     center = grid.pos2ind([0, 0, 0], None).astype(int)
     pyplot.figure()
     pyplot.subplot(2, 2, 1)

From 76701f593cc34c306cdbf9db69010c3e24685175 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 21:59:37 -0800
Subject: [PATCH 355/437] Check overlap only on forward-propagating part of
 mode

---
 examples/fdfd.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/fdfd.py b/examples/fdfd.py
index e28a2d2..16c2f20 100644
--- a/examples/fdfd.py
+++ b/examples/fdfd.py
@@ -236,7 +236,7 @@ def test1(solver=generic_solver):
     pyplot.grid(alpha=0.6)
     pyplot.title('Overlap with mode')
     pyplot.show()
-    print('Average overlap with mode:', sum(q)/len(q))
+    print('Average overlap with mode:', sum(q[8:32])/len(q[8:32]))
 
 
 def module_available(name):

From 659566750fa59def39218489dcfca70574d2a5bd Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 21:59:46 -0800
Subject: [PATCH 356/437] update for new gridlock syntax

---
 examples/fdfd.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/fdfd.py b/examples/fdfd.py
index 16c2f20..e768ba7 100644
--- a/examples/fdfd.py
+++ b/examples/fdfd.py
@@ -126,7 +126,7 @@ def test1(solver=generic_solver):
     # #### Create the grid and draw the device ####
     grid = gridlock.Grid(edge_coords)
     epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
-    grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], eps=n_wg**2)
+    grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], foreground=n_wg**2)
 
     dxes = [grid.dxyz, grid.autoshifted_dxyz()]
     for a in (0, 1, 2):

From 829007c6721683741a330965ae7d4d70f1276154 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 22:00:08 -0800
Subject: [PATCH 357/437] Only keep the real part of the energy

---
 meanas/fdfd/waveguide_2d.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 8bc57a1..00d33a0 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -425,7 +425,7 @@ def _normalized_fields(
     Sz_tavg = numpy.real(Sz_a.sum() - Sz_b.sum()) * 0.5       # 0.5 since E, H are assumed to be peak (not RMS) amplitudes
     assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
 
-    energy = epsilon * e.conj() * e
+    energy = numpy.real(epsilon * e.conj() * e)
 
     norm_amplitude = 1 / numpy.sqrt(Sz_tavg)
     norm_angle = -numpy.angle(e[energy.argmax()])       # Will randomly add a negative sign when mode is symmetric

From 7987dc796f7f6d22dd380ad8a75bee52f1ae2af9 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 22:00:21 -0800
Subject: [PATCH 358/437] mode numbers may be any sequence

---
 meanas/fdfd/waveguide_2d.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 00d33a0..cb9c12c 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -826,7 +826,7 @@ def sensitivity(
 
 
 def solve_modes(
-        mode_numbers: list[int],
+        mode_numbers: Sequence[int],
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,

From 155f30068f66e1982fa2a2b0bf1668ed46425cd9 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 22:01:10 -0800
Subject: [PATCH 359/437] add inner_product() and use it for energy calculation

---
 meanas/fdfd/waveguide_2d.py | 41 +++++++++++++++++++++++++++++++------
 1 file changed, 35 insertions(+), 6 deletions(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index cb9c12c..22248f1 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -414,15 +414,10 @@ def _normalized_fields(
     shape = [s.size for s in dxes[0]]
     dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
 
-    E = unvec(e, shape)
-    H = unvec(h, shape)
-
     # Find time-averaged Sz and normalize to it
     # H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting
     phase = numpy.exp(-1j * -prop_phase / 2)
-    Sz_a = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0]
-    Sz_b = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1]
-    Sz_tavg = numpy.real(Sz_a.sum() - Sz_b.sum()) * 0.5       # 0.5 since E, H are assumed to be peak (not RMS) amplitudes
+    Sz_tavg = inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real
     assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
 
     energy = numpy.real(epsilon * e.conj() * e)
@@ -901,3 +896,37 @@ def solve_mode(
     kwargs['mode_numbers'] = [mode_number]
     e_xys, wavenumbers = solve_modes(*args, **kwargs)
     return e_xys[0], wavenumbers[0]
+
+
+def inner_product(    # TODO documentation
+        e1: vcfdfield_t,
+        h2: vcfdfield_t,
+        dxes: dx_lists_t,
+        prop_phase: float = 0,
+        conj_h: bool = False,
+        trapezoid: bool = False,
+        ) -> tuple[vcfdfield_t, vcfdfield_t]:
+
+    shape = [s.size for s in dxes[0]]
+
+    # H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting
+    phase = numpy.exp(-1j * -prop_phase / 2)
+
+    E1 = unvec(e1, shape)
+    H2 = unvec(h2, shape) * phase
+
+    if conj_h:
+        H2 = numpy.conj(H2)
+
+    # Find time-averaged Sz and normalize to it
+    dxes_real = [[numpy.real(dxyz) for dxyz in dxeh] for dxeh in dxes]
+    if integrate:
+        Sz_a = numpy.trapezoid(numpy.trapezoid(E1[0] * H2[1], numpy.cumsum(dxes_real[0][1])), numpy.cumsum(dxes_real[1][0]))
+        Sz_b = numpy.trapezoid(numpy.trapezoid(E1[1] * H2[0], numpy.cumsum(dxes_real[0][0])), numpy.cumsum(dxes_real[1][1]))
+    else:
+        Sz_a = E1[0] * H2[1] * dxes_real[1][0][:, None] * dxes_real[0][1][None, :]
+        Sz_b = E1[1] * H2[0] * dxes_real[0][0][:, None] * dxes_real[1][1][None, :]
+    Sz = 0.5 * (Sz_a.sum() - Sz_b.sum())
+    return Sz
+
+

From 006833acf23b126423f1c34d90eafc265886f9ab Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 22:01:29 -0800
Subject: [PATCH 360/437] add logger

---
 meanas/fdfd/waveguide_cyl.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index f9e2570..8f130f8 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -10,6 +10,7 @@ As the z-dependence is known, all the functions in this file assume a 2D grid
 
 from typing import Any
 from collections.abc import Sequence
+import logging
 
 import numpy
 from numpy.typing import NDArray, ArrayLike
@@ -20,6 +21,9 @@ from ..fdmath.operators import deriv_forward, deriv_back
 from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
 
 
+logger = logging.getLogger(__name__)
+
+
 def cylindrical_operator(
         omega: complex,
         dxes: dx_lists_t,

From 6a56921c129a1cf5ef8499336ffb6dbe01cf66aa Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 22:02:19 -0800
Subject: [PATCH 361/437] Return angular wavenumbers, and remove r0 arg
 (leaving only rmin)

---
 meanas/fdfd/waveguide_cyl.py | 44 +++++++++++++++++++-----------------
 1 file changed, 23 insertions(+), 21 deletions(-)

diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 8f130f8..ef2c250 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -28,7 +28,6 @@ def cylindrical_operator(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        r0: float,
         rmin: float,
         ) -> sparse.spmatrix:
     """
@@ -51,8 +50,7 @@ def cylindrical_operator(
         omega: The angular frequency of the system
         dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
         epsilon: Vectorized dielectric constant grid
-        r0: Radius of curvature at x=0
-        rmin: Radius at the left edge of the simulation domain
+        rmin: Radius at the left edge of the simulation domain (minimum 'x')
 
     Returns:
         Sparse matrix representation of the operator
@@ -61,13 +59,7 @@ def cylindrical_operator(
     Dfx, Dfy = deriv_forward(dxes[0])
     Dbx, Dby = deriv_back(dxes[1])
 
-    ra = rmin + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0])   # Radius at Ex points
-    rb = rmin + numpy.cumsum(dxes[0][0])                      # Radius at Ey points
-    ta = ra / r0
-    tb = rb / r0
-
-    Ta = sparse.diags(vec(ta[:, None].repeat(dxes[0][1].size, axis=1)))
-    Tb = sparse.diags(vec(tb[:, None].repeat(dxes[1][1].size, axis=1)))
+    Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
 
     eps_parts = numpy.split(epsilon, 3)
     eps_x = sparse.diags(eps_parts[0])
@@ -103,10 +95,9 @@ def solve_modes(
         omega: complex,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
-        r0: float,
         rmin: float,
         mode_margin: int = 2,
-        ) -> tuple[vcfdfield_t, NDArray[numpy.complex64]]:
+        ) -> tuple[vcfdfield_t, NDArray[numpy.complex128]]:
     """
     TODO: fixup
     Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
@@ -118,12 +109,12 @@ def solve_modes(
         dxes: Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types.
               The first coordinate is assumed to be r, the second is y.
         epsilon: Dielectric constant
-        r0: Radius of curvature for the simulation. This should be the minimum value of
-            r within the simulation domain.
+        rmin: Radius of curvature for the simulation. This should be the minimum value of
+               r within the simulation domain.
 
     Returns:
         e_xys: NDArray of vfdfield_t specifying fields. First dimension is mode number.
-        wavenumbers: list of wavenumbers
+        angular_wavenumbers: list of wavenumbers in 1/rad units.
     """
 
     #
@@ -131,15 +122,17 @@ def solve_modes(
     #
     dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
 
-    A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0=r0, rmin=rmin)
+    A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), rmin=rmin)
     eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
-    e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)].T
+    keep_inds = -(numpy.array(mode_numbers) + 1)
+    e_xys = eigvecs[:, keep_inds].T
+    eigvals = eigvals[keep_inds]
 
     #
     # Now solve for the eigenvector of the full operator, using the real operator's
     #  eigenvector as an initial guess for Rayleigh quotient iteration.
     #
-    A = cylindrical_operator(omega, dxes, epsilon, r0=r0, rmin=rmin)
+    A = cylindrical_operator(omega, dxes, epsilon, rmin=rmin)
     for nn in range(len(mode_numbers)):
         eigvals[nn], e_xys[nn, :] = rayleigh_quotient_iteration(A, e_xys[nn, :])
 
@@ -147,7 +140,15 @@ def solve_modes(
     wavenumbers = numpy.sqrt(eigvals)
     wavenumbers *= numpy.sign(numpy.real(wavenumbers))
 
-    return e_xys, wavenumbers
+    # Wavenumbers assume the mode is at rmin, which is unlikely
+    # Instead, return the wavenumber in inverse radians
+    angular_wavenumbers = wavenumbers * rmin
+
+    order = angular_wavenumbers.argsort()[::-1]
+    e_xys = e_xys[order]
+    angular_wavenumbers = angular_wavenumbers[order]
+
+    return e_xys, angular_wavenumbers
 
 
 def solve_mode(
@@ -164,8 +165,9 @@ def solve_mode(
        **kwargs: passed to `solve_modes()`
 
     Returns:
-        (e_xy, wavenumber)
+        (e_xy, angular_wavenumber)
     """
     kwargs['mode_numbers'] = [mode_number]
     e_xys, wavenumbers = solve_modes(*args, **kwargs)
-    return e_xys[0], wavenumbers[0]
+    return e_xys[0], angular_wavenumbers[0]
+

From 71c2bbfadae71d48fa566b960c79c19291bc8426 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 22:02:43 -0800
Subject: [PATCH 362/437] Add linear_wavenumbers() for calculating 1/distance
 wavenumbers

---
 meanas/fdfd/waveguide_cyl.py | 40 ++++++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index ef2c250..227008d 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -171,3 +171,43 @@ def solve_mode(
     e_xys, wavenumbers = solve_modes(*args, **kwargs)
     return e_xys[0], angular_wavenumbers[0]
 
+
+def linear_wavenumbers(
+        e_xys: vcfdfield_t,
+        angular_wavenumbers: ArrayLike,
+        epsilon: vfdfield_t,
+        dxes: dx_lists_t,
+        rmin: float,
+        ) -> NDArray[numpy.complex128]:
+    """
+    Calculate linear wavenumbers (1/distance) based on angular wavenumbers (1/rad)
+      and the mode's energy distribution.
+
+    Args:
+        e_xys: Vectorized mode fields with shape [num_modes, 2 * x *y)
+        angular_wavenumbers: Angular wavenumbers corresponding to the fields in `e_xys`
+        epsilon: Vectorized dielectric constant grid with shape (3, x, y)
+        dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
+        rmin: Radius at the left edge of the simulation domain (minimum 'x')
+
+    Returns:
+        NDArray containing the calculated linear (1/distance) wavenumbers
+    """
+    angular_wavenumbers = numpy.asarray(angular_wavenumbers)
+    mode_radii = numpy.empty_like(angular_wavenumbers, dtype=float)
+
+    wavenumbers = numpy.empty_like(angular_wavenumbers)
+    shape2d = (len(dxes[0][0]), len(dxes[0][1]))
+    epsilon2d = unvec(epsilon, shape2d)[:2]
+    grid_radii = rmin + numpy.cumsum(dxes[0][0])
+    for ii in range(angular_wavenumbers.size):
+        efield = unvec(e_xys[ii], shape2d, 2)
+        energy = numpy.real((efield * efield.conj()) * epsilon2d)
+        energy_vs_x = energy.sum(axis=(0, 2))
+        mode_radii[ii] = (grid_radii * energy_vs_x).sum() / energy_vs_x.sum()
+
+    logger.info(f'{mode_radii=}')
+    lin_wavenumbers = angular_wavenumbers / mode_radii
+    return lin_wavenumbers
+
+

From 651e255704ecd14e72a49f0a5662cc304accfd9f Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 22:15:18 -0800
Subject: [PATCH 363/437] add derivation for exy2e()

---
 meanas/fdfd/waveguide_2d.py | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 22248f1..f530062 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -535,6 +535,33 @@ def exy2e(
     Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
      into a vectorized E containing all three E components
 
+    From the operator derivation (see module docs), we have
+
+    $$
+    \imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
+    $$
+
+    as well as the intermediate equations
+
+    $$
+    \begin{aligned}
+    \gamma H_y &=  \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
+    \gamma H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
+    \end{aligned}
+    $$
+
+    Combining these, we get
+
+    $$
+    \begin{aligned}
+    E_z &= \frac{1}{\imath \omega \gamma \epsilon_{zz}} ((
+             \hat{\partial}_y \hat{\partial}_x H_z
+            -\hat{\partial}_x \hat{\partial}_y H_z)
+          + \imath \omega (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y))
+        &= \frac{1}{\gamma \epsilon_{zz}} (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y)
+    \end{aligned}
+    $$
+
     Args:
         wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
                     It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy`

From 53d5812b4ab37162135f518d8b5e4a3403b47d9f Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 22:34:35 -0800
Subject: [PATCH 364/437] [waveguide_2d] Remove \gamma from docs in favor of
 just using \beta

---
 meanas/fdfd/waveguide_2d.py | 73 ++++++++++++++++++-------------------
 1 file changed, 36 insertions(+), 37 deletions(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index f530062..c532490 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -18,8 +18,8 @@ $$
 \begin{aligned}
 \nabla \times \vec{E}(x, y, z) &= -\imath \omega \mu \vec{H} \\
 \nabla \times \vec{H}(x, y, z) &=  \imath \omega \epsilon \vec{E} \\
-\vec{E}(x,y,z) &= (\vec{E}_t(x, y) + E_z(x, y)\vec{z}) e^{-\gamma z} \\
-\vec{H}(x,y,z) &= (\vec{H}_t(x, y) + H_z(x, y)\vec{z}) e^{-\gamma z} \\
+\vec{E}(x,y,z) &= (\vec{E}_t(x, y) + E_z(x, y)\vec{z}) e^{-\imath \beta z} \\
+\vec{H}(x,y,z) &= (\vec{H}_t(x, y) + H_z(x, y)\vec{z}) e^{-\imath \beta z} \\
 \end{aligned}
 $$
 
@@ -40,56 +40,57 @@ Substituting in our expressions for $\vec{E}$, $\vec{H}$ and discretizing:
 
 $$
 \begin{aligned}
--\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \gamma E_y \\
--\imath \omega \mu_{yy} H_y &= -\gamma E_x - \tilde{\partial}_x E_z \\
+-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \imath \beta E_y \\
+-\imath \omega \mu_{yy} H_y &= -\imath \beta E_x - \tilde{\partial}_x E_z \\
 -\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\
-\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \gamma H_y \\
-\imath \omega \epsilon_{yy} E_y &= -\gamma H_x - \hat{\partial}_x H_z \\
+\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta H_y \\
+\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \hat{\partial}_x H_z \\
 \imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
 \end{aligned}
 $$
 
 Rewrite the last three equations as
+
 $$
 \begin{aligned}
-\gamma H_y &=  \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
-\gamma H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
+\imath \beta H_y &=  \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
+\imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
 \imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
 \end{aligned}
 $$
 
-Now apply $\gamma \tilde{\partial}_x$ to the last equation,
-then substitute in for $\gamma H_x$ and $\gamma H_y$:
+Now apply $\imath \beta \tilde{\partial}_x$ to the last equation,
+then substitute in for $\imath \beta H_x$ and $\imath \beta H_y$:
 
 $$
 \begin{aligned}
-\gamma \tilde{\partial}_x \imath \omega E_z &= \gamma \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y
-                                             - \gamma \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
+\imath \beta \tilde{\partial}_x \imath \omega E_z &= \imath \beta \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y
+                                                   - \imath \beta \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
         &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z)
          - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z)  \\
         &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x)
          - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y)  \\
-\gamma \tilde{\partial}_x E_z &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
-                               + \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
+\imath \beta \tilde{\partial}_x E_z &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+                                     + \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
 \end{aligned}
 $$
 
-With a similar approach (but using $\gamma \tilde{\partial}_y$ instead), we can get
+With a similar approach (but using $\imath \beta \tilde{\partial}_y$ instead), we can get
 
 $$
 \begin{aligned}
-\gamma \tilde{\partial}_y E_z &= \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
-                               + \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
+\imath \beta \tilde{\partial}_y E_z &= \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+                                     + \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
 \end{aligned}
 $$
 
-We can combine this equation for $\gamma \tilde{\partial}_y E_z$ with
+We can combine this equation for $\imath \beta \tilde{\partial}_y E_z$ with
 the unused $\imath \omega \mu_{xx} H_x$ and $\imath \omega \mu_{yy} H_y$ equations to get
 
 $$
 \begin{aligned}
--\imath \omega \mu_{xx} \gamma H_x &=  \gamma^2 E_y + \gamma \tilde{\partial}_y E_z \\
--\imath \omega \mu_{xx} \gamma H_x &=  \gamma^2 E_y + \tilde{\partial}_y (
+-\imath \omega \mu_{xx} \imath \beta H_x &=  -\beta^2 E_y + \imath \beta \tilde{\partial}_y E_z \\
+-\imath \omega \mu_{xx} \imath \beta H_x &=  -\beta^2 E_y + \tilde{\partial}_y (
                                       \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
                                     + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
                                     )\\
@@ -100,25 +101,24 @@ and
 
 $$
 \begin{aligned}
--\imath \omega \mu_{yy} \gamma H_y &= -\gamma^2 E_x - \gamma \tilde{\partial}_x E_z \\
--\imath \omega \mu_{yy} \gamma H_y &= -\gamma^2 E_x - \tilde{\partial}_x (
+-\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \imath \beta \tilde{\partial}_x E_z \\
+-\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \tilde{\partial}_x (
                                       \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
                                     + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
                                     )\\
 \end{aligned}
 $$
 
-However, based on our rewritten equation for $\gamma H_x$ and the so-far unused
+However, based on our rewritten equation for $\imath \beta H_x$ and the so-far unused
 equation for $\imath \omega \mu_{zz} H_z$ we can also write
 
 $$
 \begin{aligned}
--\imath \omega \mu_{xx} (\gamma H_x) &= -\imath \omega \mu_{xx} (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\
-                                     &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
-                                        +\imath \omega \mu_{xx} \hat{\partial}_x (
-                                           \frac{1}{-\imath \omega \mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x)) \\
-                                     &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
-                                        -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
+-\imath \omega \mu_{xx} (\imath \beta H_x) &= -\imath \omega \mu_{xx} (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\
+                 &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + \imath \omega \mu_{xx} \hat{\partial}_x (
+                         \frac{1}{-\imath \omega \mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x)) \\
+                 &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
+                         -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
 \end{aligned}
 $$
 
@@ -126,7 +126,7 @@ and, similarly,
 
 $$
 \begin{aligned}
--\imath \omega \mu_{yy} (\gamma H_y) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
+-\imath \omega \mu_{yy} (\imath \beta H_y) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
                                            +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
 \end{aligned}
 $$
@@ -135,12 +135,12 @@ By combining both pairs of expressions, we get
 
 $$
 \begin{aligned}
--\gamma^2 E_x - \tilde{\partial}_x (
+\beta^2 E_x - \tilde{\partial}_x (
     \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
   + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
     ) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
         +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
-\gamma^2 E_y + \tilde{\partial}_y (
+-\beta^2 E_y + \tilde{\partial}_y (
     \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
   + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
     ) &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
@@ -165,14 +165,13 @@ $$
                     E_y \end{bmatrix}
 $$
 
-where $\gamma = \imath\beta$. In the literature, $\beta$ is usually used to denote
-the lossless/real part of the propagation constant, but in `meanas` it is allowed to
-be complex.
+In the literature, $\beta$ is usually used to denote the lossless/real part of the propagation constant,
+but in `meanas` it is allowed to be complex.
 
 An equivalent eigenvalue problem can be formed using the $H_x$ and $H_y$ fields, if those are more convenient.
 
-Note that $E_z$ was never discretized, so $\gamma$ and $\beta$ will need adjustment
-to account for numerical dispersion if the result is introduced into a space with a discretized z-axis.
+Note that $E_z$ was never discretized, so $\beta$ will need adjustment to account for numerical dispersion
+if the result is introduced into a space with a discretized z-axis.
 
 
 """

From 4afc6cf62e2eb6f1f03d5b16bc754b1d7d5b3f3c Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 14 Jan 2025 22:34:52 -0800
Subject: [PATCH 365/437] cleanup latex

---
 meanas/fdfd/waveguide_2d.py | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index c532490..93e5174 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -530,22 +530,22 @@ def exy2e(
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
         ) -> sparse.spmatrix:
-    """
+    r"""
     Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
      into a vectorized E containing all three E components
 
     From the operator derivation (see module docs), we have
 
     $$
-    \imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
+    \imath \omega \epsilon_{zz} E_z = \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
     $$
 
     as well as the intermediate equations
 
     $$
     \begin{aligned}
-    \gamma H_y &=  \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
-    \gamma H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
+    \imath \beta H_y &=  \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
+    \imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
     \end{aligned}
     $$
 
@@ -553,11 +553,11 @@ def exy2e(
 
     $$
     \begin{aligned}
-    E_z &= \frac{1}{\imath \omega \gamma \epsilon_{zz}} ((
+    E_z &= \frac{1}{- \omega \beta \epsilon_{zz}} ((
              \hat{\partial}_y \hat{\partial}_x H_z
             -\hat{\partial}_x \hat{\partial}_y H_z)
           + \imath \omega (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y))
-        &= \frac{1}{\gamma \epsilon_{zz}} (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y)
+        &= \frac{1}{\imath \beta \epsilon_{zz}} (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y)
     \end{aligned}
     $$
 

From 1987ee473aeb182cf9c263768cb88a6e4a8271fe Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 28 Jan 2025 19:54:04 -0800
Subject: [PATCH 366/437] improve type annotations

---
 meanas/fdfd/waveguide_2d.py    |  2 +-
 meanas/fdfd/waveguide_cyl.py   |  4 ++--
 meanas/fdmath/functional.py    | 12 ++++++------
 meanas/fdmath/operators.py     | 10 +++++-----
 meanas/fdmath/vectorization.py |  6 +++---
 5 files changed, 17 insertions(+), 17 deletions(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 93e5174..1942202 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -931,7 +931,7 @@ def inner_product(    # TODO documentation
         prop_phase: float = 0,
         conj_h: bool = False,
         trapezoid: bool = False,
-        ) -> tuple[vcfdfield_t, vcfdfield_t]:
+        ) -> complex:
 
     shape = [s.size for s in dxes[0]]
 
diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 227008d..05e11e9 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -8,7 +8,7 @@ As the z-dependence is known, all the functions in this file assume a 2D grid
 """
 # TODO update module docs
 
-from typing import Any
+from typing import Any, cast
 from collections.abc import Sequence
 import logging
 
@@ -142,7 +142,7 @@ def solve_modes(
 
     # Wavenumbers assume the mode is at rmin, which is unlikely
     # Instead, return the wavenumber in inverse radians
-    angular_wavenumbers = wavenumbers * rmin
+    angular_wavenumbers = wavenumbers * cast(complex, rmin)
 
     order = angular_wavenumbers.argsort()[::-1]
     e_xys = e_xys[order]
diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py
index 1b5811d..034d4ba 100644
--- a/meanas/fdmath/functional.py
+++ b/meanas/fdmath/functional.py
@@ -14,7 +14,7 @@ from .types import fdfield_t, fdfield_updater_t
 
 
 def deriv_forward(
-        dx_e: Sequence[NDArray[floating]] | None = None,
+        dx_e: Sequence[NDArray[floating | complexfloating]] | None = None,
         ) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
     """
     Utility operators for taking discretized derivatives (backward variant).
@@ -38,7 +38,7 @@ def deriv_forward(
 
 
 def deriv_back(
-        dx_h: Sequence[NDArray[floating]] | None = None,
+        dx_h: Sequence[NDArray[floating | complexfloating]] | None = None,
         ) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
     """
     Utility operators for taking discretized derivatives (forward variant).
@@ -65,7 +65,7 @@ TT = TypeVar('TT', bound='NDArray[floating | complexfloating]')
 
 
 def curl_forward(
-        dx_e: Sequence[NDArray[floating]] | None = None,
+        dx_e: Sequence[NDArray[floating | complexfloating]] | None = None,
         ) -> Callable[[TT], TT]:
     r"""
     Curl operator for use with the E field.
@@ -94,7 +94,7 @@ def curl_forward(
 
 
 def curl_back(
-        dx_h: Sequence[NDArray[floating]] | None = None,
+        dx_h: Sequence[NDArray[floating | complexfloating]] | None = None,
         ) -> Callable[[TT], TT]:
     r"""
     Create a function which takes the backward curl of a field.
@@ -123,7 +123,7 @@ def curl_back(
 
 
 def curl_forward_parts(
-        dx_e: Sequence[NDArray[floating]] | None = None,
+        dx_e: Sequence[NDArray[floating | complexfloating]] | None = None,
         ) -> Callable:
     Dx, Dy, Dz = deriv_forward(dx_e)
 
@@ -136,7 +136,7 @@ def curl_forward_parts(
 
 
 def curl_back_parts(
-        dx_h: Sequence[NDArray[floating]] | None = None,
+        dx_h: Sequence[NDArray[floating | complexfloating]] | None = None,
         ) -> Callable:
     Dx, Dy, Dz = deriv_back(dx_h)
 
diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py
index 5d50670..946eb88 100644
--- a/meanas/fdmath/operators.py
+++ b/meanas/fdmath/operators.py
@@ -6,7 +6,7 @@ Basic discrete calculus etc.
 from collections.abc import Sequence
 import numpy
 from numpy.typing import NDArray
-from numpy import floating
+from numpy import floating, complexfloating
 from scipy import sparse
 
 from .types import vfdfield_t
@@ -97,7 +97,7 @@ def shift_with_mirror(
 
 
 def deriv_forward(
-        dx_e: Sequence[NDArray[floating]],
+        dx_e: Sequence[NDArray[floating | complexfloating]],
         ) -> list[sparse.spmatrix]:
     """
     Utility operators for taking discretized derivatives (forward variant).
@@ -124,7 +124,7 @@ def deriv_forward(
 
 
 def deriv_back(
-        dx_h: Sequence[NDArray[floating]],
+        dx_h: Sequence[NDArray[floating | complexfloating]],
         ) -> list[sparse.spmatrix]:
     """
     Utility operators for taking discretized derivatives (backward variant).
@@ -219,7 +219,7 @@ def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix:
 
 
 def curl_forward(
-        dx_e: Sequence[NDArray[floating]],
+        dx_e: Sequence[NDArray[floating | complexfloating]],
         ) -> sparse.spmatrix:
     """
     Curl operator for use with the E field.
@@ -235,7 +235,7 @@ def curl_forward(
 
 
 def curl_back(
-        dx_h: Sequence[NDArray[floating]],
+        dx_h: Sequence[NDArray[floating | complexfloating]],
         ) -> sparse.spmatrix:
     """
     Curl operator for use with the H field.
diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py
index 3871801..8f5ff39 100644
--- a/meanas/fdmath/vectorization.py
+++ b/meanas/fdmath/vectorization.py
@@ -49,15 +49,15 @@ def vec(
 
 
 @overload
-def unvec(v: None, shape: Sequence[int], nvdim: int) -> None:
+def unvec(v: None, shape: Sequence[int], nvdim: int = 3) -> None:
     pass
 
 @overload
-def unvec(v: vfdfield_t, shape: Sequence[int], nvdim: int) -> fdfield_t:
+def unvec(v: vfdfield_t, shape: Sequence[int], nvdim: int = 3) -> fdfield_t:
     pass
 
 @overload
-def unvec(v: vcfdfield_t, shape: Sequence[int], nvdim: int) -> cfdfield_t:
+def unvec(v: vcfdfield_t, shape: Sequence[int], nvdim: int = 3) -> cfdfield_t:
     pass
 
 def unvec(

From 83f4d87ad89e5d52c9c1357f920abf6c518b4234 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 28 Jan 2025 19:54:48 -0800
Subject: [PATCH 367/437] [fdfd.waveguide*] misc fixes

---
 meanas/fdfd/waveguide_2d.py  |  2 +-
 meanas/fdfd/waveguide_cyl.py | 10 +++++-----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py
index 1942202..5fda683 100644
--- a/meanas/fdfd/waveguide_2d.py
+++ b/meanas/fdfd/waveguide_2d.py
@@ -946,7 +946,7 @@ def inner_product(    # TODO documentation
 
     # Find time-averaged Sz and normalize to it
     dxes_real = [[numpy.real(dxyz) for dxyz in dxeh] for dxeh in dxes]
-    if integrate:
+    if trapezoid:
         Sz_a = numpy.trapezoid(numpy.trapezoid(E1[0] * H2[1], numpy.cumsum(dxes_real[0][1])), numpy.cumsum(dxes_real[1][0]))
         Sz_b = numpy.trapezoid(numpy.trapezoid(E1[1] * H2[0], numpy.cumsum(dxes_real[0][0])), numpy.cumsum(dxes_real[1][1]))
     else:
diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 05e11e9..4e9b2f6 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -19,7 +19,7 @@ from scipy import sparse
 from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t
 from ..fdmath.operators import deriv_forward, deriv_back
 from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
-
+from . import waveguide_2d
 
 logger = logging.getLogger(__name__)
 
@@ -62,9 +62,9 @@ def cylindrical_operator(
     Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
 
     eps_parts = numpy.split(epsilon, 3)
-    eps_x = sparse.diags(eps_parts[0])
-    eps_y = sparse.diags(eps_parts[1])
-    eps_z_inv = sparse.diags(1 / eps_parts[2])
+    eps_x = sparse.diags_array(eps_parts[0])
+    eps_y = sparse.diags_array(eps_parts[1])
+    eps_z_inv = sparse.diags_array(1 / eps_parts[2])
 
     omega2 = omega * omega
     diag = sparse.block_diag
@@ -168,7 +168,7 @@ def solve_mode(
         (e_xy, angular_wavenumber)
     """
     kwargs['mode_numbers'] = [mode_number]
-    e_xys, wavenumbers = solve_modes(*args, **kwargs)
+    e_xys, angular_wavenumbers = solve_modes(*args, **kwargs)
     return e_xys[0], angular_wavenumbers[0]
 
 

From 234e8d7ac39b29d0033aca7e72e1310b4761118f Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 28 Jan 2025 19:55:09 -0800
Subject: [PATCH 368/437] delete h version of operator in comment

---
 meanas/fdfd/waveguide_cyl.py | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 4e9b2f6..9476f4b 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -79,11 +79,6 @@ def cylindrical_operator(
     #     omega * omega * mu_yx @ eps_xy
     #     + mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx))
     #     + sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy
-
-    #     # H
-    #     omega * omega * eps_yx @ mu_xy
-    #     + eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx))
-    #     + sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy
     #     )
 
     op = sq0 + lin0 + lin1

From 1cb0cb2e4fb60770242d5acdfb3d4de9f888e888 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 28 Jan 2025 21:59:59 -0800
Subject: [PATCH 369/437] [fdfd.waveguide_cyl] Improve documentation and add
 auxiliary functions (e.g. exy2exyz)

---
 meanas/fdfd/waveguide_cyl.py | 338 ++++++++++++++++++++++++++++++++---
 1 file changed, 316 insertions(+), 22 deletions(-)

diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 9476f4b..2c00d02 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -1,13 +1,65 @@
-"""
+r"""
 Operators and helper functions for cylindrical waveguides with unchanging cross-section.
 
-WORK IN PROGRESS, CURRENTLY BROKEN
+Waveguide operator is derived according to 10.1364/OL.33.001848.
+The curl equations in the complex coordinate system become
 
-As the z-dependence is known, all the functions in this file assume a 2D grid
+$$
+\begin{aligned}
+-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \imath \beta frac{E_y}{\tilde{t}_x} \\
+-\imath \omega \mu_{yy} H_y &= -\imath \beta E_x - \frac{1}{\hat{t}_x} \tilde{\partial}_x \tilde{t}_x E_z \\
+-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\
+\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta \frac{H_y}{\hat{T}} \\
+\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \{1}{\tilde{t}_x} \hat{\partial}_x \hat{t}_x} H_z \\
+\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
+\end{aligned}
+$$
+
+where $t_x = 1 + \frac{\Delta_{x, m}}{R_0}$ is the grid spacing adjusted by the nominal radius $R0$.
+
+Rewrite the last three equations as
+
+$$
+\begin{aligned}
+\imath \beta H_y &=  \imath \omega \hat{t}_x \epsilon_{xx} E_x - \hat{t}_x \hat{\partial}_y H_z \\
+\imath \beta H_x &= -\imath \omega \hat{t}_x \epsilon_{yy} E_y - \hat{t}_x \hat{\partial}_x H_z \\
+\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
+\end{aligned}
+$$
+
+The derivation then follows the same steps as the straight waveguide, leading to the eigenvalue problem
+
+$$
+\beta^2 \begin{bmatrix} E_x \\
+                        E_y \end{bmatrix} =
+    (\omega^2 \begin{bmatrix} T_b T_b \mu_{yy} \epsilon_{xx} & 0 \\
+                                                            0 & T_a T_a \mu_{xx} \epsilon_{yy} \end{bmatrix} +
+              \begin{bmatrix} -T_b \mu_{yy} \hat{\partial}_y \\
+                               T_a \mu_{xx} \hat{\partial}_x \end{bmatrix} T_b \mu_{zz}^{-1}
+              \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} +
+      \begin{bmatrix} \tilde{\partial}_x \\
+                      \tilde{\partial}_y \end{bmatrix} T_a \epsilon_{zz}^{-1}
+                 \begin{bmatrix} \hat{\partial}_x T_b \epsilon_{xx} & \hat{\partial}_y T_a \epsilon_{yy} \end{bmatrix})
+    \begin{bmatrix} E_x \\
+                    E_y \end{bmatrix}
+$$
+
+which resembles the straight waveguide eigenproblem with additonal $T_a$ and $T_b$ terms. These
+are diagonal matrices containing the $t_x$ values:
+
+$$
+\begin{aligned}
+T_a &=  1 + \frac{\Delta_{x, m               }}{R_0}
+T_b &=  1 + \frac{\Delta_{x, m + \frac{1}{2} }}{R_0}
+\end{aligned}
+
+
+TODO: consider 10.1364/OE.20.021583 for an alternate approach
+$$
+
+As in the straight waveguide case, all the functions in this file assume a 2D grid
  (i.e. `dxes = [[[dr_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`).
 """
-# TODO update module docs
-
 from typing import Any, cast
 from collections.abc import Sequence
 import logging
@@ -30,13 +82,21 @@ def cylindrical_operator(
         epsilon: vfdfield_t,
         rmin: float,
         ) -> sparse.spmatrix:
-    """
+    r"""
     Cylindrical coordinate waveguide operator of the form
 
-    (NOTE: See 10.1364/OL.33.001848)
-    TODO: consider 10.1364/OE.20.021583
-
-    TODO
+    $$
+        (\omega^2 \begin{bmatrix} T_b T_b \mu_{yy} \epsilon_{xx} & 0 \\
+                                                                0 & T_a T_a \mu_{xx} \epsilon_{yy} \end{bmatrix} +
+                  \begin{bmatrix} -T_b \mu_{yy} \hat{\partial}_y \\
+                                   T_a \mu_{xx} \hat{\partial}_x \end{bmatrix} T_b \mu_{zz}^{-1}
+                  \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} +
+          \begin{bmatrix} \tilde{\partial}_x \\
+                          \tilde{\partial}_y \end{bmatrix} T_a \epsilon_{zz}^{-1}
+                     \begin{bmatrix} \hat{\partial}_x T_b \epsilon_{xx} & \hat{\partial}_y T_a \epsilon_{yy} \end{bmatrix})
+        \begin{bmatrix} E_x \\
+                        E_y \end{bmatrix}
+    $$
 
     for use with a field vector of the form `[E_r, E_y]`.
 
@@ -46,11 +106,13 @@ def cylindrical_operator(
     which can then be solved for the eigenmodes of the system
     (an `exp(-i * wavenumber * theta)` theta-dependence is assumed for the fields).
 
+    (NOTE: See module docs and 10.1364/OL.33.001848)
+
     Args:
         omega: The angular frequency of the system
         dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
         epsilon: Vectorized dielectric constant grid
-        rmin: Radius at the left edge of the simulation domain (minimum 'x')
+        rmin: Radius at the left edge of the simulation domain (at minimum 'x')
 
     Returns:
         Sparse matrix representation of the operator
@@ -74,13 +136,6 @@ def cylindrical_operator(
     lin0 = sparse.vstack((-Tb @ Dby, Ta @ Dbx)) @ Tb @ sparse.hstack((-Dfy, Dfx))
     lin1 = sparse.vstack((Dfx, Dfy)) @ Ta @ eps_z_inv @ sparse.hstack((Dbx @ Tb @ eps_x,
                                                                        Dby @ Ta @ eps_y))
-    # op = (
-    #     # E
-    #     omega * omega * mu_yx @ eps_xy
-    #     + mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx))
-    #     + sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy
-    #     )
-
     op = sq0 + lin0 + lin1
     return op
 
@@ -94,7 +149,6 @@ def solve_modes(
         mode_margin: int = 2,
         ) -> tuple[vcfdfield_t, NDArray[numpy.complex128]]:
     """
-    TODO: fixup
     Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
      of the bent waveguide with the specified mode number.
 
@@ -179,11 +233,13 @@ def linear_wavenumbers(
       and the mode's energy distribution.
 
     Args:
-        e_xys: Vectorized mode fields with shape [num_modes, 2 * x *y)
-        angular_wavenumbers: Angular wavenumbers corresponding to the fields in `e_xys`
+        e_xys: Vectorized mode fields with shape (num_modes, 2 * x *y)
+        angular_wavenumbers: Wavenumbers assuming fields have theta-dependence of
+            `exp(-i * angular_wavenumber * theta)`. They should satisfy
+            `operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
         epsilon: Vectorized dielectric constant grid with shape (3, x, y)
         dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
-        rmin: Radius at the left edge of the simulation domain (minimum 'x')
+        rmin: Radius at the left edge of the simulation domain (at minimum 'x')
 
     Returns:
         NDArray containing the calculated linear (1/distance) wavenumbers
@@ -206,3 +262,241 @@ def linear_wavenumbers(
     return lin_wavenumbers
 
 
+def exy2h(
+        angular_wavenumber: complex,
+        omega: float,
+        dxes: dx_lists_t,
+        rmin: float,
+        epsilon: vfdfield_t,
+        mu: vfdfield_t | None = None
+        ) -> sparse.spmatrix:
+    """
+    Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
+     into a vectorized H containing all three H components
+
+    Args:
+        angular_wavenumber: Wavenumber assuming fields have theta-dependence of
+            `exp(-i * angular_wavenumber * theta)`. It should satisfy
+            `operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
+        omega: The angular frequency of the system
+        dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
+        rmin: Radius at the left edge of the simulation domain (at minimum 'x')
+        epsilon: Vectorized dielectric constant grid
+        mu: Vectorized magnetic permeability grid (default 1 everywhere)
+
+    Returns:
+        Sparse matrix representing the operator.
+    """
+    e2hop = e2h(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, mu=mu)
+    return e2hop @ exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon)
+
+
+def exy2e(
+        angular_wavenumber: complex,
+        omega: float,
+        dxes: dx_lists_t,
+        rmin: float,
+        epsilon: vfdfield_t,
+        ) -> sparse.spmatrix:
+    """
+    Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
+     into a vectorized E containing all three E components
+
+    Unlike the straight waveguide case, the H_z components do not cancel and must be calculated
+    from E_x and E_y in order to then calculate E_z.
+
+    Args:
+        angular_wavenumber: Wavenumber assuming fields have theta-dependence of
+            `exp(-i * angular_wavenumber * theta)`. It should satisfy
+            `operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
+        omega: The angular frequency of the system
+        dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
+        rmin: Radius at the left edge of the simulation domain (at minimum 'x')
+        epsilon: Vectorized dielectric constant grid
+
+    Returns:
+        Sparse matrix representing the operator.
+    """
+    Dfx, Dfy = deriv_forward(dxes[0])
+    Dbx, Dby = deriv_back(dxes[1])
+    wavenumber = angular_wavenumber / rmin
+
+    Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
+    Tai = sparse.diags_array(1 / Ta.diagonal())
+    Tbi = sparse.diags_array(1 / Tb.diagonal())
+
+    epsilon_parts = numpy.split(epsilon, 3)
+    epsilon_x, epsilon_y = (sparse.diags_array(epsi) for epsi in epsilon_parts[:2])
+    epsilon_z_inv = sparse.diags_array(1 / epsilon_parts[2])
+
+    n_pts = dxes[0][0].size * dxes[0][1].size
+    zeros = sparse.coo_array((n_pts, n_pts))
+    keep_x = sparse.block_array([[sparse.eye_array(n_pts), None], [None, zeros]])
+    keep_y = sparse.block_array([[zeros, None], [None, sparse.eye_array(n_pts)]])
+
+    mu_z = numpy.ones(n_pts)
+    mu_z_inv = sparse.diags_array(1 / mu_z)
+    exy2hz = 1 / (-1j * omega) * mu_z_inv @ sparse.hstack((Dfy, -Dfx))
+    hxy2ez = 1 / (1j * omega) * epsilon_z_inv @ sparse.hstack((Dby, -Dbx))
+
+    exy2hy = Tb / (1j * wavenumber) @ (-1j * omega * sparse.hstack((epsilon_x, zeros)) - Dby @ exy2hz)
+    exy2hx = Tb / (1j * wavenumber) @ ( 1j * omega * sparse.hstack((zeros, epsilon_y)) - Tai @ Dbx @ Tb @ exy2hz)
+
+    exy2ez = hxy2ez @ sparse.vstack((exy2hx, exy2hy))
+
+    op = sparse.vstack((sparse.eye_array(2 * n_pts),
+                        exy2ez))
+    return op
+
+
+def e2h(
+        angular_wavenumber: complex,
+        omega: complex,
+        dxes: dx_lists_t,
+        rmin: float,
+        mu: vfdfield_t | None = None
+        ) -> sparse.spmatrix:
+    r"""
+    Returns an operator which, when applied to a vectorized E eigenfield, produces
+     the vectorized H eigenfield.
+
+    This operator is created directly from the initial coordinate-transformed equations:
+    $$
+    \begin{aligned}
+    \imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta \frac{H_y}{\hat{T}} \\
+    \imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \{1}{\tilde{t}_x} \hat{\partial}_x \hat{t}_x} H_z \\
+    \imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
+    \end{aligned}
+    $$
+
+    Args:
+        angular_wavenumber: Wavenumber assuming fields have theta-dependence of
+            `exp(-i * angular_wavenumber * theta)`. It should satisfy
+            `operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
+        omega: The angular frequency of the system
+        dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
+        rmin: Radius at the left edge of the simulation domain (at minimum 'x')
+        mu: Vectorized magnetic permeability grid (default 1 everywhere)
+
+    Returns:
+        Sparse matrix representation of the operator.
+    """
+    Dfx, Dfy = deriv_forward(dxes[0])
+    Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
+    Tai = sparse.diags_array(1 / Ta.diagonal())
+    Tbi = sparse.diags_array(1 / Tb.diagonal())
+
+    jB = 1j * angular_wavenumber / rmin
+    op = sparse.block_array([[    None, -jB * Tai,           -Dfy],
+                             [jB * Tbi,      None, Tbi @ Dfx @ Ta],
+                             [     Dfy,      -Dfx,           None]]) / (-1j * omega)
+    if mu is not None:
+        op = sparse.diags_array(1 / mu) @ op
+    return op
+
+
+def dxes2T(
+        dxes: dx_lists_t,
+        rmin: float,
+        ) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
+    r"""
+    Returns the $T_a$ and $T_b$ diagonal matrices which are used to apply the cylindrical
+      coordinate transformation in various operators.
+
+    Args:
+        dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
+        rmin: Radius at the left edge of the simulation domain (at minimum 'x')
+
+    Returns:
+        Sparse matrix representations of the operators Ta and Tb
+    """
+    ra = rmin + numpy.cumsum(dxes[0][0])                      # Radius at Ey points
+    rb = rmin + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0])   # Radius at Ex points
+    ta = ra / rmin
+    tb = rb / rmin
+
+    Ta = sparse.diags_array(vec(ta[:, None].repeat(dxes[0][1].size, axis=1)))
+    Tb = sparse.diags_array(vec(tb[:, None].repeat(dxes[1][1].size, axis=1)))
+    return Ta, Tb
+
+
+def normalized_fields_e(
+        e_xy: ArrayLike,
+        angular_wavenumber: complex,
+        omega: complex,
+        dxes: dx_lists_t,
+        rmin: float,
+        epsilon: vfdfield_t,
+        mu: vfdfield_t | None = None,
+        prop_phase: float = 0,
+        ) -> tuple[vcfdfield_t, vcfdfield_t]:
+    """
+    Given a vector `e_xy` containing the vectorized E_x and E_y fields,
+     returns normalized, vectorized E and H fields for the system.
+
+    Args:
+        e_xy: Vector containing E_x and E_y fields
+        angular_wavenumber: Wavenumber assuming fields have theta-dependence of
+            `exp(-i * angular_wavenumber * theta)`. It should satisfy
+            `operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
+        omega: The angular frequency of the system
+        dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
+        rmin: Radius at the left edge of the simulation domain (at minimum 'x')
+        epsilon: Vectorized dielectric constant grid
+        mu: Vectorized magnetic permeability grid (default 1 everywhere)
+        prop_phase: Phase shift `(dz * corrected_wavenumber)` over 1 cell in propagation direction.
+                    Default 0 (continuous propagation direction, i.e. dz->0).
+
+    Returns:
+        `(e, h)`, where each field is vectorized, normalized,
+        and contains all three vector components.
+    """
+    e = exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon) @ e_xy
+    h = exy2h(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon, mu=mu) @ e_xy
+    e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon,
+                                        mu=mu, prop_phase=prop_phase)
+    return e_norm, h_norm
+
+
+def _normalized_fields(
+        e: vcfdfield_t,
+        h: vcfdfield_t,
+        omega: complex,
+        dxes: dx_lists_t,
+        rmin: float,
+        epsilon: vfdfield_t,
+        mu: vfdfield_t | None = None,
+        prop_phase: float = 0,
+        ) -> tuple[vcfdfield_t, vcfdfield_t]:
+    h *= -1
+    # TODO documentation for normalized_fields
+    shape = [s.size for s in dxes[0]]
+    dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
+
+    # Find time-averaged Sz and normalize to it
+    # H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting
+    phase = numpy.exp(-1j * -prop_phase / 2)
+    Sz_tavg = waveguide_2d.inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real   # Note, using linear poynting vector
+    assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
+
+    energy = numpy.real(epsilon * e.conj() * e)
+
+    norm_amplitude = 1 / numpy.sqrt(Sz_tavg)
+    norm_angle = -numpy.angle(e[energy.argmax()])       # Will randomly add a negative sign when mode is symmetric
+
+    # Try to break symmetry to assign a consistent sign [experimental]
+    E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape)
+    sign = numpy.sign(E_weighted[:,
+                                 :max(shape[0] // 2, 1),
+                                 :max(shape[1] // 2, 1)].real.sum())
+    assert sign != 0
+
+    norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle)
+
+    print('\nAAA\n', waveguide_2d.inner_product(e, h, dxes, prop_phase=prop_phase))
+    e *= norm_factor
+    h *= norm_factor
+    print(f'{sign=} {norm_amplitude=} {norm_angle=} {prop_phase=}')
+    print(waveguide_2d.inner_product(e, h, dxes, prop_phase=prop_phase))
+
+    return e, h

From 99e8d32eb1a2d8eca5784ea61b91b32b160e1b20 Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 28 Jan 2025 22:06:32 -0800
Subject: [PATCH 370/437] [waveguide_cyl] frequency should be real

---
 meanas/fdfd/waveguide_cyl.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py
index 2c00d02..2134806 100644
--- a/meanas/fdfd/waveguide_cyl.py
+++ b/meanas/fdfd/waveguide_cyl.py
@@ -77,7 +77,7 @@ logger = logging.getLogger(__name__)
 
 
 def cylindrical_operator(
-        omega: complex,
+        omega: float,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
         rmin: float,
@@ -142,7 +142,7 @@ def cylindrical_operator(
 
 def solve_modes(
         mode_numbers: Sequence[int],
-        omega: complex,
+        omega: float,
         dxes: dx_lists_t,
         epsilon: vfdfield_t,
         rmin: float,
@@ -351,7 +351,7 @@ def exy2e(
 
 def e2h(
         angular_wavenumber: complex,
-        omega: complex,
+        omega: float,
         dxes: dx_lists_t,
         rmin: float,
         mu: vfdfield_t | None = None
@@ -423,7 +423,7 @@ def dxes2T(
 def normalized_fields_e(
         e_xy: ArrayLike,
         angular_wavenumber: complex,
-        omega: complex,
+        omega: float,
         dxes: dx_lists_t,
         rmin: float,
         epsilon: vfdfield_t,

From cd5cc9eb83493016745b765cf0937371bc3a571a Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Tue, 28 Jan 2025 22:07:19 -0800
Subject: [PATCH 371/437] [fdfd.eme] Add basic (WIP) eignmode expansion
 functionality

---
 meanas/fdfd/bloch.py | 49 +++++++++++++++++++++++++++++++
 meanas/fdfd/eme.py   | 68 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 117 insertions(+)
 create mode 100644 meanas/fdfd/eme.py

diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py
index 2e1da30..71b2a8b 100644
--- a/meanas/fdfd/bloch.py
+++ b/meanas/fdfd/bloch.py
@@ -799,3 +799,52 @@ def _rtrace_AtB(
 def _symmetrize(A: NDArray[numpy.complex128]) -> NDArray[numpy.complex128]:
     return (A + A.conj().T) * 0.5
 
+
+
+def inner_product(eL, hL, eR, hR) -> complex:
+    # assumes x-axis propagation
+
+    assert numpy.array_equal(eR.shape, hR.shape)
+    assert numpy.array_equal(eL.shape, hL.shape)
+    assert numpy.array_equal(eR.shape, eL.shape)
+
+    # Cross product, times 2 since it's 

, then divide by 4. # TODO might want to abs() this? + norm2R = (eR[1] * hR[2] - eR[2] * hR[1]).sum() / 2 + norm2L = (eL[1] * hL[2] - eL[2] * hL[1]).sum() / 2 + + # eRxhR_x = numpy.cross(eR.reshape(3, -1), hR.reshape(3, -1), axis=0).reshape(eR.shape)[0] / normR + # logger.info(f'power {eRxhR_x.sum() / 2}) + + eR /= numpy.sqrt(norm2R) + hR /= numpy.sqrt(norm2R) + eL /= numpy.sqrt(norm2L) + hL /= numpy.sqrt(norm2L) + + # (eR x hL)[0] and (eL x hR)[0] + eRxhL_x = eR[1] * hL[2] - eR[2] - hL[1] + eLxhR_x = eL[1] * hR[2] - eL[2] - hR[1] + + #return 1j * (eRxhL_x - eLxhR_x).sum() / numpy.sqrt(norm2R * norm2L) + #return (eRxhL_x.sum() - eLxhR_x.sum()) / numpy.sqrt(norm2R * norm2L) + return eRxhL_x.sum() - eLxhR_x.sum() + + +def trq(eI, hI, eO, hO) -> tuple[complex, complex]: + pp = inner_product(eO, hO, eI, hI) + pn = inner_product(eO, hO, eI, -hI) + np = inner_product(eO, -hO, eI, hI) + nn = inner_product(eO, -hO, eI, -hI) + + assert pp == -nn + assert pn == -np + + logger.info(f''' + {pp=:4g} {pn=:4g} + {nn=:4g} {np=:4g} + {nn * pp / pn=:4g} {-np=:4g} + ''') + + r = -pp / pn # -/ = -(-pp) / (-pn) + t = (np - nn * pp / pn) / 4 + + return t, r diff --git a/meanas/fdfd/eme.py b/meanas/fdfd/eme.py new file mode 100644 index 0000000..35e1e90 --- /dev/null +++ b/meanas/fdfd/eme.py @@ -0,0 +1,68 @@ +import numpy + +from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t +from .waveguide_2d import inner_product + + +def get_tr(ehL, wavenumbers_L, ehR, wavenumbers_R, dxes: dx_lists_t): + nL = len(wavenumbers_L) + nR = len(wavenumbers_R) + A12 = numpy.zeros((nL, nR), dtype=complex) + A21 = numpy.zeros((nL, nR), dtype=complex) + B11 = numpy.zeros((nL,), dtype=complex) + for ll in range(nL): + eL, hL = ehL[ll] + B11[ll] = inner_product(eL, hL, dxes=dxes, conj_h=False) + for rr in range(nR): + eR, hR = ehR[rr] + A12[ll, rr] = inner_product(eL, hR, dxes=dxes, conj_h=False) # TODO optimize loop? + A21[ll, rr] = inner_product(eR, hL, dxes=dxes, conj_h=False) + + # tt0 = 2 * numpy.linalg.pinv(A21 + numpy.conj(A12)) + tt0, _resid, _rank, _sing = numpy.linalg.lstsq(A21 + A12, numpy.diag(2 * B11), rcond=None) + + U, st, V = numpy.linalg.svd(tt0) + gain = st > 1 + st[gain] = 1 / st[gain] + tt = U @ numpy.diag(st) @ V + + # rr = 0.5 * (A21 - numpy.conj(A12)) @ tt + rr = numpy.diag(0.5 / B11) @ (A21 - A12) @ tt + + return tt, rr + + +def get_abcd(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs): + t12, r12 = get_tr(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs) + t21, r21 = get_tr(eR_xys, wavenumbers_R, eL_xys, wavenumbers_L, **kwargs) + t21i = numpy.linalg.pinv(t21) + A = t12 - r21 @ t21i @ r12 + B = r21 @ t21i + C = -t21i @ r12 + D = t21i + return sparse.block_array(((A, B), (C, D))) + + +def get_s( + eL_xys, + wavenumbers_L, + eR_xys, + wavenumbers_R, + force_nogain: bool = False, + force_reciprocal: bool = False, + **kwargs): + t12, r12 = get_tr(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs) + t21, r21 = get_tr(eR_xys, wavenumbers_R, eL_xys, wavenumbers_L, **kwargs) + + ss = numpy.block([[r12, t12], + [t21, r21]]) + + if force_nogain: + # force S @ S.H diagonal + U, sing, V = numpy.linalg.svd(ss) + ss = numpy.diag(sing) @ U @ V + + if force_reciprocal: + ss = 0.5 * (ss + ss.T) + + return ss From c4f8749941c119fa05d0bd16f7f759de6024057b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 5 Feb 2025 00:09:25 -0800 Subject: [PATCH 372/437] [fdfd.solvers.generic] report residual scaled to b --- meanas/fdfd/solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py index 5b48493..215b283 100644 --- a/meanas/fdfd/solvers.py +++ b/meanas/fdfd/solvers.py @@ -44,7 +44,7 @@ def _scipy_qmr( nonlocal ii ii += 1 if ii % 100 == 0: - cur_norm = norm(A @ xk - b) + cur_norm = norm(A @ xk - b) / norm(b) logger.info(f'Solver residual at iteration {ii} : {cur_norm}') if 'callback' in kwargs: From 777ecbc02463f60ae21ea2c008480340ffe22ed9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 5 Feb 2025 00:13:46 -0800 Subject: [PATCH 373/437] [fdfd.solvers.generic] add option to pass a guess solution --- meanas/fdfd/solvers.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py index 215b283..81d1d09 100644 --- a/meanas/fdfd/solvers.py +++ b/meanas/fdfd/solvers.py @@ -69,11 +69,13 @@ def generic( J: vcfdfield_t, epsilon: vfdfield_t, mu: vfdfield_t | None = None, + *, pec: vfdfield_t | None = None, pmc: vfdfield_t | None = None, adjoint: bool = False, matrix_solver: Callable[..., ArrayLike] = _scipy_qmr, matrix_solver_opts: dict[str, Any] | None = None, + E_guess: vcfdfield_t | None = None, ) -> vcfdfield_t: """ Conjugate gradient FDFD solver using CSR sparse matrices. @@ -100,6 +102,8 @@ def generic( which doesn't return convergence info and logs the residual every 100 iterations. matrix_solver_opts: Passed as kwargs to `matrix_solver(...)` + E_guess: Guess at the solution E-field. `matrix_solver` must accept an + `x0` argument with the same purpose. Returns: E-field which solves the system. @@ -120,6 +124,13 @@ def generic( A = Pl @ A0 @ Pr b = Pl @ b0 + if E_guess is not None: + if adjoint: + x0 = Pr.H @ E_guess + else: + x0 = Pl @ E_guess + matrix_solver_opts['x0'] = x0 + x = matrix_solver(A.tocsr(), b, **matrix_solver_opts) if adjoint: From c858b20d47077088b0e64296bec44a2c6b3b28d5 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 12 Mar 2025 23:19:20 -0700 Subject: [PATCH 374/437] Bump numpy dependency to >=2.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a6d31bd..2af4e57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ include = [ ] dynamic = ["version"] dependencies = [ - "numpy>=1.26", + "numpy>=2.0", "scipy~=1.14", ] From 9eb0e28bcbcf0bb32d36e94749f836a207f6dd7e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 12 Mar 2025 23:40:00 -0700 Subject: [PATCH 375/437] [meanas.fdtd.misc] add basic pulse and beam shapes --- meanas/fdtd/misc.py | 167 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 meanas/fdtd/misc.py diff --git a/meanas/fdtd/misc.py b/meanas/fdtd/misc.py new file mode 100644 index 0000000..160682d --- /dev/null +++ b/meanas/fdtd/misc.py @@ -0,0 +1,167 @@ +from typing import Callable +from collections.abc import Sequence +import logging + +import numpy +from numpy.typing import NDArray, ArrayLike +from numpy import pi + + +logger = logging.getLogger(__name__) + + +pulse_fn_t = Callable[[int | NDArray], tuple[float, float, float]] + + +def gaussian_packet( + wl: float, + dwl: float, + dt: float, + turn_on: float = 1e-10, + one_sided: bool = False, + ) -> tuple[pulse_fn_t, float]: + """ + Gaussian pulse (or gaussian ramp) for FDTD excitation + + exp(-a*t*t) ==> exp(-omega * omega / (4 * a)) [fourier, ignoring leading const.] + + FWHM_time is 2 * sqrt(2 * log(2)) * sqrt(2 / a) + FWHM_omega is 2 * sqrt(2 * log(2)) * sqrt(2 * a) = 4 * sqrt(log(2) * a) + + Args: + wl: wavelength + dwl: Gaussian's FWHM in wavelength space + dt: Timestep + turn_on: Max allowable amplitude at t=0 + one_sided: If `True`, source amplitude never decreases after reaching max + + Returns: + Source function: src(timestep) -> (envelope[tt], cos[... * tt], sin[... * tt]) + Delay: number of initial timesteps for which envelope[tt] will be 0 + """ + logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO + # dt * dw = 4 * ln(2) + + omega = 2 * pi / wl + freq = 1 / wl + fwhm_omega = dwl * omega * omega / (2 * pi) # dwl -> d_omega (approx) + alpha = (fwhm_omega * fwhm_omega) * numpy.log(2) / 8 + delay = numpy.sqrt(-numpy.log(turn_on) / alpha) + delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase + logger.info(f'src_time {2 * delay / dt}') + + def source_phasor(ii: int | NDArray) -> tuple[float, float, float]: + t0 = ii * dt - delay + envelope = numpy.sqrt(numpy.sqrt(2 * alpha / pi)) * numpy.exp(-alpha * t0 * t0) + + if one_sided and t0 > 0: + envelope = 1 + + cc = numpy.cos(omega * t0) + ss = numpy.sin(omega * t0) + + return envelope, cc, ss + + # nrm = numpy.exp(-omega * omega / alpha) / 2 + + return source_phasor, delay + + +def ricker_pulse( + wl: float, + dt: float, + turn_on: float = 1e-10, + ) -> tuple[pulse_fn_t, float]: + """ + Ricker wavelet (second derivative of a gaussian pulse) + + t0 = ii * dt - delay + R = w_peak * t0 / 2 + f(t) = (1 - 2 * (pi * f_peak * t0) ** 2) * exp(-(pi * f_peak * t0)**2 + = (1 - (w_peak * t0)**2 / 2 exp(-(w_peak * t0 / 2) **2) + = (1 - 2 * R * R) * exp(-R * R) + + # NOTE: don't use cosine/sine for J, just for phasor readout + + Args: + wl: wavelength + dt: Timestep + turn_on: Max allowable amplitude at t=0 + + Returns: + Source function: src(timestep) -> (envelope[tt], cos[... * tt], sin[... * tt]) + Delay: number of initial timesteps for which envelope[tt] will be 0 + """ + logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO + omega = 2 * pi / wl + freq = 1 / wl + r0 = omega / 2 + + from scipy.optimize import root_scalar + delay_results = root_scalar(lambda xx: (1 - omega * omega * tt * tt / 2) * numpy.exp(-omega * omega / 4 * tt * tt) - turn_on, x0=0, x1=-2 / omega) + delay = delay_results.root + delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase + + def source_phasor(ii: int | NDArray) -> tuple[float, float, float]: + t0 = ii * dt - delay + rr = omega * t0 / 2 + ff = (1 - 2 * rr * rr) * numpy.exp(-rr * rr) + + cc = numpy.cos(omega * t0) + ss = numpy.sin(omega * t0) + + return ff, cc, ss + + return source_phasor, delay + + +def gaussian_beam( + xyz: list[NDArray], + center: ArrayLike, + waist_radius: float, + wl: float, + tilt: float = 0, + ) -> NDArray[numpy.complex128]: + """ + Gaussian beam + (solution to paraxial Helmholtz equation) + + Default (no tilt) corresponds to a beam propagating in the -z direction. + + Args: + xyz: List of [[x0, x1, ...], [y0, ...], [z0, ...]] positions specifying grid + locations at which the field will be sampled. + center: [x, y, z] location of beam waist + waist_radius: Beam radius at the waist + wl: Wavelength + tilt: Rotation around y axis. Default (0) has beam propagating in -z direction. + """ + logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO + w0 = waist_radius + grids = numpy.asarray(numpy.meshgrid(*xyz, indexing='ij')) + grids -= numpy.asarray(center)[:, None, None, None] + + rot = numpy.array([ + [ numpy.cos(tilt), 0, numpy.sin(tilt)], + [ 0, 1, 0], + [-numpy.sin(tilt), 0, numpy.cos(tilt)], + ]) + + xx, yy, zz = numpy.einsum('ij,jxyz->ixyz', rot, grids) + r2 = xx * xx + yy * yy + z2 = zz * zz + + zr = pi * w0 * w0 / wl + zr2 = zr * zr + wz2 = w0 * w0 * (1 + z2 / zr2) + wz = numpy.sqrt(wz2) # == fwhm(z) / sqrt(2 * ln(2)) + + kk = 2 * pi / wl + Rz = zz * (1 + zr2 / z2) + gouy = numpy.arctan(zz / zr) + + gaussian = w0 / wz * numpy.exp(-r2 / wz2) * numpy.exp(1j * (kk * zz + kk * r2 / 2 / Rz - gouy)) + + row = gaussian[:, :, gaussian.shape[2] // 2] + norm = numpy.sqrt((row * row.conj()).sum()) + return gaussian / norm From 43e01a814d7d8fda7aa49fcced02a65d6a949e57 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 16 Apr 2025 22:19:14 -0700 Subject: [PATCH 376/437] examples will use new gridlock --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2af4e57..96e1c0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ path = "meanas/__init__.py" [project.optional-dependencies] dev = ["pytest", "pdoc", "gridlock"] -examples = ["gridlock"] +examples = ["gridlock>=2.0"] test = ["pytest"] From 35ecbad15e963b9ffae2996d85af5ec2fa54265b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 16 Apr 2025 22:19:21 -0700 Subject: [PATCH 377/437] remove old lint --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 96e1c0f..7b95a41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,6 @@ lint.ignore = [ "ANN002", # *args "ANN003", # **kwargs "ANN401", # Any - "ANN101", # self: Self "SIM108", # single-line if / else assignment "RET504", # x=y+z; return x "PIE790", # unnecessary pass From e3169b9e201bfd2b458011d07fbdd7da73179583 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 16 Apr 2025 22:20:16 -0700 Subject: [PATCH 378/437] bump version to v0.10 --- meanas/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/__init__.py b/meanas/__init__.py index 0757a5c..ef079fb 100644 --- a/meanas/__init__.py +++ b/meanas/__init__.py @@ -6,7 +6,7 @@ See the readme or `import meanas; help(meanas)` for more info. import pathlib -__version__ = '0.9' +__version__ = '0.10' __author__ = 'Jan Petykiewicz' From 4a80ca8b12b018fc7a66e339b747c75f1d08a14f Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 9 Dec 2025 22:55:52 -0800 Subject: [PATCH 379/437] [waveguide_cyl] silence some debug prints --- meanas/fdfd/waveguide_cyl.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index 2134806..b65e038 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -493,10 +493,6 @@ def _normalized_fields( norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) - print('\nAAA\n', waveguide_2d.inner_product(e, h, dxes, prop_phase=prop_phase)) e *= norm_factor h *= norm_factor - print(f'{sign=} {norm_amplitude=} {norm_angle=} {prop_phase=}') - print(waveguide_2d.inner_product(e, h, dxes, prop_phase=prop_phase)) - return e, h From 684b891e0f9b2063a42e7268bca4a310b5736a31 Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 9 Dec 2025 22:56:16 -0800 Subject: [PATCH 380/437] [waveguide_3d] clean up docstrings --- meanas/fdfd/waveguide_3d.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index 8bb0513..6e2a2db 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -161,25 +161,22 @@ def compute_overlap_e( axis: int, polarity: int, slices: Sequence[slice], - ) -> cfdfield_t: # TODO DOCS + ) -> cfdfield_t: """ Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) [assumes reflection symmetry]. - TODO: add reference + TODO: add reference or derivation for compute_overlap_e Args: E: E-field of the mode - H: H-field of the mode (advanced by half of a Yee cell from E) wavenumber: Wavenumber of the mode - omega: Angular frequency of the simulation dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` axis: Propagation axis (0=x, 1=y, 2=z) polarity: Propagation direction (+1 for +ve, -1 for -ve) slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use as the waveguide cross-section. slices[axis] should select only one item. - mu: Magnetic permeability (default 1 everywhere) Returns: overlap_e such that `numpy.sum(overlap_e * other_e.conj())` computes the overlap integral From b7ad5dea2b17ce7ae3fb082bef022776fb45a628 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 10 Dec 2025 02:05:24 -0800 Subject: [PATCH 381/437] [fdfd.bloch] drop unnecessary noqas --- meanas/fdfd/bloch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index 71b2a8b..deb6ec6 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -238,7 +238,7 @@ def maxwell_operator( # cross product and transform into xyz basis d_xyz = (n * hin_m - - m * hin_n) * k_mag # noqa: E128 + - m * hin_n) * k_mag # divide by epsilon temp = ifftn(d_xyz, axes=range(3)) # reuses d_xyz if using pyfftw @@ -254,7 +254,7 @@ def maxwell_operator( else: # transform from mn to xyz b_xyz = (m * b_m[:, :, :, None] - + n * b_n[:, :, :, None]) # noqa: E128 + + n * b_n[:, :, :, None]) # divide by mu temp = ifftn(b_xyz, axes=range(3)) @@ -305,7 +305,7 @@ def hmn_2_exyz( def operator(h: NDArray[numpy.complex128]) -> cfdfield_t: hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2)) d_xyz = (n * hin_m - - m * hin_n) * k_mag # noqa: E128 + - m * hin_n) * k_mag # divide by epsilon return numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0) @@ -403,7 +403,7 @@ def inverse_maxwell_operator_approx( else: # transform from mn to xyz h_xyz = (m * hin_m[:, :, :, None] - + n * hin_n[:, :, :, None]) # noqa: E128 + + n * hin_n[:, :, :, None]) # multiply by mu temp = ifftn(h_xyz, axes=range(3)) @@ -416,7 +416,7 @@ def inverse_maxwell_operator_approx( # cross product and transform into xyz basis e_xyz = (n * b_m - - m * b_n) / k_mag # noqa: E128 + - m * b_n) / k_mag # multiply by epsilon temp = ifftn(e_xyz, axes=range(3)) From b486fa325b11976b6463251cbb42eeaf346ef22f Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 10 Dec 2025 02:14:20 -0800 Subject: [PATCH 382/437] Rework field types, use sparse arrays instead of matrices, rework eme arg naming, improve type annotations and linter cleanup --- meanas/eigensolvers.py | 8 +- meanas/fdfd/bloch.py | 49 +++++--- meanas/fdfd/eme.py | 44 ++++--- meanas/fdfd/farfield.py | 10 +- meanas/fdfd/functional.py | 68 +++++------ meanas/fdfd/operators.py | 142 +++++++++++----------- meanas/fdfd/solvers.py | 20 ++-- meanas/fdfd/waveguide_2d.py | 212 ++++++++++++++++----------------- meanas/fdfd/waveguide_3d.py | 33 ++--- meanas/fdfd/waveguide_cyl.py | 79 ++++++------ meanas/fdmath/__init__.py | 34 +++++- meanas/fdmath/operators.py | 51 ++++---- meanas/fdmath/types.py | 61 +++++++++- meanas/fdmath/vectorization.py | 58 +++++++-- meanas/fdtd/energy.py | 84 ++++++------- meanas/fdtd/misc.py | 7 +- meanas/fdtd/pml.py | 4 +- meanas/test/test_fdfd.py | 26 ++-- meanas/test/test_fdfd_pml.py | 12 +- meanas/test/test_fdtd.py | 2 +- 20 files changed, 571 insertions(+), 433 deletions(-) diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py index e8630aa..21e2ec0 100644 --- a/meanas/eigensolvers.py +++ b/meanas/eigensolvers.py @@ -64,10 +64,10 @@ def rayleigh_quotient_iteration( (eigenvalues, eigenvectors) """ try: - (operator - sparse.eye(operator.shape[0])) + (operator - sparse.eye_array(operator.shape[0])) - def shift(eigval: float) -> sparse: - return eigval * sparse.eye(operator.shape[0]) + def shift(eigval: float) -> sparse.sparray: + return eigval * sparse.eye_array(operator.shape[0]) if solver is None: solver = spalg.spsolve @@ -130,7 +130,7 @@ def signed_eigensolve( # Try to combine, use general LinearOperator if we fail try: - shifted_operator = operator + shift * sparse.eye(operator.shape[0]) + shifted_operator = operator + shift * sparse.eye_array(operator.shape[0]) except TypeError: shifted_operator = operator + spalg.LinearOperator(shape=operator.shape, matvec=lambda v: shift * v) diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index deb6ec6..4eedcc4 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -106,7 +106,7 @@ import scipy.optimize from scipy.linalg import norm import scipy.sparse.linalg as spalg -from ..fdmath import fdfield_t, cfdfield_t +from ..fdmath import fdfield, cfdfield, cfdfield_t logger = logging.getLogger(__name__) @@ -183,8 +183,8 @@ def generate_kmn( def maxwell_operator( k0: ArrayLike, G_matrix: ArrayLike, - epsilon: fdfield_t, - mu: fdfield_t | None = None + epsilon: fdfield, + mu: fdfield | None = None ) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]: """ Generate the Maxwell operator @@ -276,7 +276,7 @@ def maxwell_operator( def hmn_2_exyz( k0: ArrayLike, G_matrix: ArrayLike, - epsilon: fdfield_t, + epsilon: fdfield, ) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]: """ Generate an operator which converts a vectorized spatial-frequency-space @@ -308,7 +308,8 @@ def hmn_2_exyz( - m * hin_n) * k_mag # divide by epsilon - return numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0) + exyz = numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0) + return cfdfield_t(exyz) return operator @@ -316,7 +317,7 @@ def hmn_2_exyz( def hmn_2_hxyz( k0: ArrayLike, G_matrix: ArrayLike, - epsilon: fdfield_t + epsilon: fdfield, ) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]: """ Generate an operator which converts a vectorized spatial-frequency-space @@ -343,8 +344,8 @@ def hmn_2_hxyz( def operator(h: NDArray[numpy.complex128]) -> cfdfield_t: hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2)) h_xyz = (m * hin_m - + n * hin_n) # noqa: E128 - return numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)]) + + n * hin_n) + return cfdfield_t(numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)])) return operator @@ -352,8 +353,8 @@ def hmn_2_hxyz( def inverse_maxwell_operator_approx( k0: ArrayLike, G_matrix: ArrayLike, - epsilon: fdfield_t, - mu: fdfield_t | None = None, + epsilon: fdfield, + mu: fdfield | None = None, ) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]: """ Generate an approximate inverse of the Maxwell operator, @@ -440,8 +441,8 @@ def find_k( tolerance: float, direction: ArrayLike, G_matrix: ArrayLike, - epsilon: fdfield_t, - mu: fdfield_t | None = None, + epsilon: fdfield, + mu: fdfield | None = None, band: int = 0, k_bounds: tuple[float, float] = (0, 0.5), k_guess: float | None = None, @@ -508,8 +509,8 @@ def eigsolve( num_modes: int, k0: ArrayLike, G_matrix: ArrayLike, - epsilon: fdfield_t, - mu: fdfield_t | None = None, + epsilon: fdfield, + mu: fdfield | None = None, tolerance: float = 1e-7, max_iters: int = 10000, reset_iters: int = 100, @@ -649,7 +650,7 @@ def eigsolve( def Qi_func(theta: float, Qi_memo=Qi_memo, ZtZ=ZtZ, DtD=DtD, symZtD=symZtD) -> float: # noqa: ANN001 if Qi_memo[0] == theta: - return cast(float, Qi_memo[1]) + return cast('float', Qi_memo[1]) c = numpy.cos(theta) s = numpy.sin(theta) @@ -668,8 +669,8 @@ def eigsolve( else: raise Exception('Inexplicable singularity in trace_func') from err Qi_memo[0] = theta - Qi_memo[1] = cast(float, Qi) - return cast(float, Qi) + Qi_memo[1] = cast('float', Qi) + return cast('float', Qi) def trace_func(theta: float, ZtAZ=ZtAZ, DtAD=DtAD, symZtAD=symZtAD) -> float: # noqa: ANN001 c = numpy.cos(theta) @@ -801,7 +802,12 @@ def _symmetrize(A: NDArray[numpy.complex128]) -> NDArray[numpy.complex128]: -def inner_product(eL, hL, eR, hR) -> complex: +def inner_product( + eL: cfdfield, + hL: cfdfield, + eR: cfdfield, + hR: cfdfield, + ) -> complex: # assumes x-axis propagation assert numpy.array_equal(eR.shape, hR.shape) @@ -829,7 +835,12 @@ def inner_product(eL, hL, eR, hR) -> complex: return eRxhL_x.sum() - eLxhR_x.sum() -def trq(eI, hI, eO, hO) -> tuple[complex, complex]: +def trq( + eI: cfdfield, + hI: cfdfield, + eO: cfdfield, + hO: cfdfield, + ) -> tuple[complex, complex]: pp = inner_product(eO, hO, eI, hI) pn = inner_product(eO, hO, eI, -hI) np = inner_product(eO, -hO, eI, hI) diff --git a/meanas/fdfd/eme.py b/meanas/fdfd/eme.py index 35e1e90..f834973 100644 --- a/meanas/fdfd/eme.py +++ b/meanas/fdfd/eme.py @@ -1,20 +1,29 @@ +from collections.abc import Sequence import numpy +from numpy.typing import NDArray +from scipy import sparse -from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t +from ..fdmath import dx_lists2_t, vcfdfield2 from .waveguide_2d import inner_product -def get_tr(ehL, wavenumbers_L, ehR, wavenumbers_R, dxes: dx_lists_t): +def get_tr( + ehLs: Sequence[Sequence[vcfdfield2]], + wavenumbers_L: Sequence[complex], + ehRs: Sequence[Sequence[vcfdfield2]], + wavenumbers_R: Sequence[complex], + dxes: dx_lists2_t, + ) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]: nL = len(wavenumbers_L) nR = len(wavenumbers_R) A12 = numpy.zeros((nL, nR), dtype=complex) A21 = numpy.zeros((nL, nR), dtype=complex) B11 = numpy.zeros((nL,), dtype=complex) for ll in range(nL): - eL, hL = ehL[ll] + eL, hL = ehLs[ll] B11[ll] = inner_product(eL, hL, dxes=dxes, conj_h=False) for rr in range(nR): - eR, hR = ehR[rr] + eR, hR = ehRs[rr] A12[ll, rr] = inner_product(eL, hR, dxes=dxes, conj_h=False) # TODO optimize loop? A21[ll, rr] = inner_product(eR, hL, dxes=dxes, conj_h=False) @@ -32,9 +41,15 @@ def get_tr(ehL, wavenumbers_L, ehR, wavenumbers_R, dxes: dx_lists_t): return tt, rr -def get_abcd(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs): - t12, r12 = get_tr(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs) - t21, r21 = get_tr(eR_xys, wavenumbers_R, eL_xys, wavenumbers_L, **kwargs) +def get_abcd( + ehLs: Sequence[Sequence[vcfdfield2]], + wavenumbers_L: Sequence[complex], + ehRs: Sequence[Sequence[vcfdfield2]], + wavenumbers_R: Sequence[complex], + **kwargs, + ) -> sparse.sparray: + t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs) + t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs) t21i = numpy.linalg.pinv(t21) A = t12 - r21 @ t21i @ r12 B = r21 @ t21i @@ -44,15 +59,16 @@ def get_abcd(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs): def get_s( - eL_xys, - wavenumbers_L, - eR_xys, - wavenumbers_R, + ehLs: Sequence[Sequence[vcfdfield2]], + wavenumbers_L: Sequence[complex], + ehRs: Sequence[Sequence[vcfdfield2]], + wavenumbers_R: Sequence[complex], force_nogain: bool = False, force_reciprocal: bool = False, - **kwargs): - t12, r12 = get_tr(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs) - t21, r21 = get_tr(eR_xys, wavenumbers_R, eL_xys, wavenumbers_L, **kwargs) + **kwargs, + ) -> NDArray[numpy.complex128]: + t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs) + t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs) ss = numpy.block([[r12, t12], [t21, r21]]) diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index 4829d86..86ec0d7 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -1,14 +1,16 @@ """ Functions for performing near-to-farfield transformation (and the reverse). """ -from typing import Any, cast -from collections.abc import Sequence +from typing import Any, cast, TYPE_CHECKING import numpy from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift from numpy import pi from ..fdmath import cfdfield_t +if TYPE_CHECKING: + from collections.abc import Sequence + def near_to_farfield( E_near: cfdfield_t, @@ -63,7 +65,7 @@ def near_to_farfield( padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int) if not hasattr(padded_size, '__len__'): padded_size = (padded_size, padded_size) # type: ignore # checked if sequence - padded_shape = cast(Sequence[int], padded_size) + padded_shape = cast('Sequence[int]', padded_size) En_fft = [fftshift(fft2(fftshift(Eni), s=padded_shape)) for Eni in E_near] Hn_fft = [fftshift(fft2(fftshift(Hni), s=padded_shape)) for Hni in H_near] @@ -172,7 +174,7 @@ def far_to_nearfield( padded_size = (2 ** numpy.ceil(numpy.log2(s))).astype(int) if not hasattr(padded_size, '__len__'): padded_size = (padded_size, padded_size) # type: ignore # checked if sequence - padded_shape = cast(Sequence[int], padded_size) + padded_shape = cast('Sequence[int]', padded_size) k = 2 * pi kxs = fftshift(fftfreq(s[0], 1 / (s[0] * dkx))) diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index f4a250f..440daf2 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -8,7 +8,7 @@ e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z) from collections.abc import Callable import numpy -from ..fdmath import dx_lists_t, fdfield_t, cfdfield_t, cfdfield_updater_t +from ..fdmath import dx_lists_t, cfdfield_t, fdfield, cfdfield, cfdfield_updater_t from ..fdmath.functional import curl_forward, curl_back @@ -18,8 +18,8 @@ __author__ = 'Jan Petykiewicz' def e_full( omega: complex, dxes: dx_lists_t, - epsilon: fdfield_t, - mu: fdfield_t | None = None, + epsilon: fdfield, + mu: fdfield | None = None, ) -> cfdfield_updater_t: """ Wave operator for use with E-field. See `operators.e_full` for details. @@ -37,13 +37,13 @@ def e_full( ch = curl_back(dxes[1]) ce = curl_forward(dxes[0]) - def op_1(e: cfdfield_t) -> cfdfield_t: + def op_1(e: cfdfield) -> cfdfield_t: curls = ch(ce(e)) - return curls - omega ** 2 * epsilon * e + return cfdfield_t(curls - omega ** 2 * epsilon * e) - def op_mu(e: cfdfield_t) -> cfdfield_t: + def op_mu(e: cfdfield) -> cfdfield_t: curls = ch(mu * ce(e)) # type: ignore # mu = None ok because we don't return the function - return curls - omega ** 2 * epsilon * e + return cfdfield_t(curls - omega ** 2 * epsilon * e) if mu is None: return op_1 @@ -53,9 +53,9 @@ def e_full( def eh_full( omega: complex, dxes: dx_lists_t, - epsilon: fdfield_t, - mu: fdfield_t | None = None, - ) -> Callable[[cfdfield_t, cfdfield_t], tuple[cfdfield_t, cfdfield_t]]: + epsilon: fdfield, + mu: fdfield | None = None, + ) -> Callable[[cfdfield, cfdfield], tuple[cfdfield_t, cfdfield_t]]: """ Wave operator for full (both E and H) field representation. See `operators.eh_full`. @@ -73,13 +73,13 @@ def eh_full( ch = curl_back(dxes[1]) ce = curl_forward(dxes[0]) - def op_1(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]: - return (ch(h) - 1j * omega * epsilon * e, - ce(e) + 1j * omega * h) + def op_1(e: cfdfield, h: cfdfield) -> tuple[cfdfield_t, cfdfield_t]: + return (cfdfield_t(ch(h) - 1j * omega * epsilon * e), + cfdfield_t(ce(e) + 1j * omega * h)) - def op_mu(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]: - return (ch(h) - 1j * omega * epsilon * e, - ce(e) + 1j * omega * mu * h) # type: ignore # mu=None ok + def op_mu(e: cfdfield, h: cfdfield) -> tuple[cfdfield_t, cfdfield_t]: + return (cfdfield_t(ch(h) - 1j * omega * epsilon * e), + cfdfield_t(ce(e) + 1j * omega * mu * h)) # type: ignore # mu=None ok if mu is None: return op_1 @@ -89,7 +89,7 @@ def eh_full( def e2h( omega: complex, dxes: dx_lists_t, - mu: fdfield_t | None = None, + mu: fdfield | None = None, ) -> cfdfield_updater_t: """ Utility operator for converting the `E` field into the `H` field. @@ -106,11 +106,11 @@ def e2h( """ ce = curl_forward(dxes[0]) - def e2h_1_1(e: cfdfield_t) -> cfdfield_t: - return ce(e) / (-1j * omega) + def e2h_1_1(e: cfdfield) -> cfdfield_t: + return cfdfield_t(ce(e) / (-1j * omega)) - def e2h_mu(e: cfdfield_t) -> cfdfield_t: - return ce(e) / (-1j * omega * mu) # type: ignore # mu=None ok + def e2h_mu(e: cfdfield) -> cfdfield_t: + return cfdfield_t(ce(e) / (-1j * omega * mu)) # type: ignore # mu=None ok if mu is None: return e2h_1_1 @@ -120,7 +120,7 @@ def e2h( def m2j( omega: complex, dxes: dx_lists_t, - mu: fdfield_t | None = None, + mu: fdfield | None = None, ) -> cfdfield_updater_t: """ Utility operator for converting magnetic current `M` distribution @@ -138,13 +138,13 @@ def m2j( """ ch = curl_back(dxes[1]) - def m2j_mu(m: cfdfield_t) -> cfdfield_t: + def m2j_mu(m: cfdfield) -> cfdfield_t: J = ch(m / mu) / (-1j * omega) # type: ignore # mu=None ok - return J + return cfdfield_t(J) - def m2j_1(m: cfdfield_t) -> cfdfield_t: + def m2j_1(m: cfdfield) -> cfdfield_t: J = ch(m) / (-1j * omega) - return J + return cfdfield_t(J) if mu is None: return m2j_1 @@ -152,11 +152,11 @@ def m2j( def e_tfsf_source( - TF_region: fdfield_t, + TF_region: fdfield, omega: complex, dxes: dx_lists_t, - epsilon: fdfield_t, - mu: fdfield_t | None = None, + epsilon: fdfield, + mu: fdfield | None = None, ) -> cfdfield_updater_t: """ Operator that turns an E-field distribution into a total-field/scattered-field @@ -178,13 +178,13 @@ def e_tfsf_source( # TODO documentation A = e_full(omega, dxes, epsilon, mu) - def op(e: cfdfield_t) -> cfdfield_t: + def op(e: cfdfield) -> cfdfield_t: neg_iwj = A(TF_region * e) - TF_region * A(e) - return neg_iwj / (-1j * omega) + return cfdfield_t(neg_iwj / (-1j * omega)) return op -def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], cfdfield_t]: +def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield, cfdfield], cfdfield_t]: r""" Generates a function that takes the single-frequency `E` and `H` fields and calculates the cross product `E` x `H` = $E \times H$ as required @@ -206,7 +206,7 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], c Returns: Function `f` that returns E x H as required for the poynting vector. """ - def exh(e: cfdfield_t, h: cfdfield_t) -> cfdfield_t: + def exh(e: cfdfield, h: cfdfield) -> cfdfield_t: s = numpy.empty_like(e) ex = e[0] * dxes[0][0][:, None, None] ey = e[1] * dxes[0][1][None, :, None] @@ -217,5 +217,5 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], c s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx - return s + return cfdfield_t(s) return exh diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 8c16ef7..829f43e 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -1,7 +1,7 @@ """ Sparse matrix operators for use with electromagnetic wave equations. -These functions return sparse-matrix (`scipy.sparse.spmatrix`) representations of +These functions return sparse-matrix (`scipy.sparse.sparray`) representations of a variety of operators, intended for use with E and H fields vectorized using the `meanas.fdmath.vectorization.vec()` and `meanas.fdmath.vectorization.unvec()` functions. @@ -30,7 +30,7 @@ The following operators are included: import numpy from scipy import sparse -from ..fdmath import vec, dx_lists_t, vfdfield_t, vcfdfield_t +from ..fdmath import vec, dx_lists_t, vfdfield, vcfdfield from ..fdmath.operators import shift_with_mirror, shift_circ, curl_forward, curl_back @@ -40,11 +40,11 @@ __author__ = 'Jan Petykiewicz' def e_full( omega: complex, dxes: dx_lists_t, - epsilon: vfdfield_t | vcfdfield_t, - mu: vfdfield_t | None = None, - pec: vfdfield_t | None = None, - pmc: vfdfield_t | None = None, - ) -> sparse.spmatrix: + epsilon: vfdfield | vcfdfield, + mu: vfdfield | None = None, + pec: vfdfield | None = None, + pmc: vfdfield | None = None, + ) -> sparse.sparray: r""" Wave operator $$ \nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon $$ @@ -77,20 +77,20 @@ def e_full( ce = curl_forward(dxes[0]) if pec is None: - pe = sparse.eye(epsilon.size) + pe = sparse.eye_array(epsilon.size) else: - pe = sparse.diags(numpy.where(pec, 0, 1)) # Set pe to (not PEC) + pe = sparse.diags_array(numpy.where(pec, 0, 1)) # Set pe to (not PEC) if pmc is None: - pm = sparse.eye(epsilon.size) + pm = sparse.eye_array(epsilon.size) else: - pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC) + pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # set pm to (not PMC) - e = sparse.diags(epsilon) + e = sparse.diags_array(epsilon) if mu is None: - m_div = sparse.eye(epsilon.size) + m_div = sparse.eye_array(epsilon.size) else: - m_div = sparse.diags(1 / mu) + m_div = sparse.diags_array(1 / mu) op = pe @ (ch @ pm @ m_div @ ce - omega**2 * e) @ pe return op @@ -98,7 +98,7 @@ def e_full( def e_full_preconditioners( dxes: dx_lists_t, - ) -> tuple[sparse.spmatrix, sparse.spmatrix]: + ) -> tuple[sparse.sparray, sparse.sparray]: """ Left and right preconditioners `(Pl, Pr)` for symmetrizing the `e_full` wave operator. @@ -118,19 +118,19 @@ def e_full_preconditioners( dxes[1][0][:, None, None] * dxes[1][1][None, :, None] * dxes[0][2][None, None, :]] p_vector = numpy.sqrt(vec(p_squared)) - P_left = sparse.diags(p_vector) - P_right = sparse.diags(1 / p_vector) + P_left = sparse.diags_array(p_vector) + P_right = sparse.diags_array(1 / p_vector) return P_left, P_right def h_full( omega: complex, dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, - pec: vfdfield_t | None = None, - pmc: vfdfield_t | None = None, - ) -> sparse.spmatrix: + epsilon: vfdfield, + mu: vfdfield | None = None, + pec: vfdfield | None = None, + pmc: vfdfield | None = None, + ) -> sparse.sparray: r""" Wave operator $$ \nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu $$ @@ -161,20 +161,20 @@ def h_full( ce = curl_forward(dxes[0]) if pec is None: - pe = sparse.eye(epsilon.size) + pe = sparse.eye_array(epsilon.size) else: - pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC) + pe = sparse.diags_array(numpy.where(pec, 0, 1)) # set pe to (not PEC) if pmc is None: - pm = sparse.eye(epsilon.size) + pm = sparse.eye_array(epsilon.size) else: - pm = sparse.diags(numpy.where(pmc, 0, 1)) # Set pe to (not PMC) + pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # Set pe to (not PMC) - e_div = sparse.diags(1 / epsilon) + e_div = sparse.diags_array(1 / epsilon) if mu is None: - m = sparse.eye(epsilon.size) + m = sparse.eye_array(epsilon.size) else: - m = sparse.diags(mu) + m = sparse.diags_array(mu) A = pm @ (ce @ pe @ e_div @ ch - omega**2 * m) @ pm return A @@ -183,11 +183,11 @@ def h_full( def eh_full( omega: complex, dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, - pec: vfdfield_t | None = None, - pmc: vfdfield_t | None = None, - ) -> sparse.spmatrix: + epsilon: vfdfield, + mu: vfdfield | None = None, + pec: vfdfield | None = None, + pmc: vfdfield | None = None, + ) -> sparse.sparray: r""" Wave operator for `[E, H]` field representation. This operator implements Maxwell's equations without cancelling out either E or H. The operator is @@ -227,35 +227,35 @@ def eh_full( Sparse matrix containing the wave operator. """ if pec is None: - pe = sparse.eye(epsilon.size) + pe = sparse.eye_array(epsilon.size) else: - pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC) + pe = sparse.diags_array(numpy.where(pec, 0, 1)) # set pe to (not PEC) if pmc is None: - pm = sparse.eye(epsilon.size) + pm = sparse.eye_array(epsilon.size) else: - pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC) + pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # set pm to (not PMC) - iwe = pe @ (1j * omega * sparse.diags(epsilon)) @ pe + iwe = pe @ (1j * omega * sparse.diags_array(epsilon)) @ pe iwm = 1j * omega if mu is not None: - iwm *= sparse.diags(mu) + iwm *= sparse.diags_array(mu) iwm = pm @ iwm @ pm A1 = pe @ curl_back(dxes[1]) @ pm A2 = pm @ curl_forward(dxes[0]) @ pe - A = sparse.bmat([[-iwe, A1], - [A2, iwm]]) + A = sparse.block_array([[-iwe, A1], + [A2, iwm]]) return A def e2h( omega: complex, dxes: dx_lists_t, - mu: vfdfield_t | None = None, - pmc: vfdfield_t | None = None, - ) -> sparse.spmatrix: + mu: vfdfield | None = None, + pmc: vfdfield | None = None, + ) -> sparse.sparray: """ Utility operator for converting the E field into the H field. For use with `e_full()` -- assumes that there is no magnetic current M. @@ -274,10 +274,10 @@ def e2h( op = curl_forward(dxes[0]) / (-1j * omega) if mu is not None: - op = sparse.diags(1 / mu) @ op + op = sparse.diags_array(1 / mu) @ op if pmc is not None: - op = sparse.diags(numpy.where(pmc, 0, 1)) @ op + op = sparse.diags_array(numpy.where(pmc, 0, 1)) @ op return op @@ -285,8 +285,8 @@ def e2h( def m2j( omega: complex, dxes: dx_lists_t, - mu: vfdfield_t | None = None, - ) -> sparse.spmatrix: + mu: vfdfield | None = None, + ) -> sparse.sparray: """ Operator for converting a magnetic current M into an electric current J. For use with eg. `e_full()`. @@ -302,12 +302,12 @@ def m2j( op = curl_back(dxes[1]) / (1j * omega) if mu is not None: - op = op @ sparse.diags(1 / mu) + op = op @ sparse.diags_array(1 / mu) return op -def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: +def poynting_e_cross(e: vcfdfield, dxes: dx_lists_t) -> sparse.sparray: """ Operator for computing the Poynting vector, containing the (E x) portion of the Poynting vector. @@ -330,13 +330,13 @@ def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: block_diags = [[ None, fx @ -Ez, fx @ Ey], [ fy @ Ez, None, fy @ -Ex], [ fz @ -Ey, fz @ Ex, None]] - block_matrix = sparse.bmat([[sparse.diags(x) if x is not None else None for x in row] - for row in block_diags]) - P = block_matrix @ sparse.diags(numpy.concatenate(dxbg)) + block_matrix = sparse.block_array([[sparse.diags_array(x) if x is not None else None for x in row] + for row in block_diags]) + P = block_matrix @ sparse.diags_array(numpy.concatenate(dxbg)) return P -def poynting_h_cross(h: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: +def poynting_h_cross(h: vcfdfield, dxes: dx_lists_t) -> sparse.sparray: """ Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector. @@ -353,23 +353,23 @@ def poynting_h_cross(h: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] - Hx, Hy, Hz = (sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg, strict=True)) + Hx, Hy, Hz = (sparse.diags_array(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg, strict=True)) - P = (sparse.bmat( + P = (sparse.block_array( [[ None, -Hz @ fx, Hy @ fx], [ Hz @ fy, None, -Hx @ fy], [-Hy @ fz, Hx @ fz, None]]) - @ sparse.diags(numpy.concatenate(dxag))) + @ sparse.diags_array(numpy.concatenate(dxag))) return P def e_tfsf_source( - TF_region: vfdfield_t, + TF_region: vfdfield, omega: complex, dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, - ) -> sparse.spmatrix: + epsilon: vfdfield, + mu: vfdfield | None = None, + ) -> sparse.sparray: """ Operator that turns a desired E-field distribution into a total-field/scattered-field (TFSF) source. @@ -390,18 +390,18 @@ def e_tfsf_source( """ # TODO documentation A = e_full(omega, dxes, epsilon, mu) - Q = sparse.diags(TF_region) + Q = sparse.diags_array(TF_region) return (A @ Q - Q @ A) / (-1j * omega) def e_boundary_source( - mask: vfdfield_t, + mask: vfdfield, omega: complex, dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, + epsilon: vfdfield, + mu: vfdfield | None = None, periodic_mask_edges: bool = False, - ) -> sparse.spmatrix: + ) -> sparse.sparray: """ Operator that turns an E-field distrubtion into a current (J) distribution along the edges (external and internal) of the provided mask. This is just an @@ -424,10 +424,10 @@ def e_boundary_source( shape = [len(dxe) for dxe in dxes[0]] jmask = numpy.zeros_like(mask, dtype=bool) - def shift_rot(axis: int, polarity: int) -> sparse.spmatrix: + def shift_rot(axis: int, polarity: int) -> sparse.sparray: return shift_circ(axis=axis, shape=shape, shift_distance=polarity) - def shift_mir(axis: int, polarity: int) -> sparse.spmatrix: + def shift_mir(axis: int, polarity: int) -> sparse.sparray: return shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity) shift = shift_rot if periodic_mask_edges else shift_mir @@ -436,7 +436,7 @@ def e_boundary_source( if shape[axis] == 1: continue for polarity in (-1, +1): - r = shift(axis, polarity) - sparse.eye(numpy.prod(shape)) # shifted minus original + r = shift(axis, polarity) - sparse.eye_array(numpy.prod(shape)) # shifted minus original r3 = sparse.block_diag((r, r, r)) jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask)) @@ -447,5 +447,5 @@ def e_boundary_source( # (numpy.roll(mask, -1, axis=2) != mask) | # (numpy.roll(mask, +1, axis=2) != mask)) - return sparse.diags(jmask.astype(int)) @ full + return sparse.diags_array(jmask.astype(int)) @ full diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py index 81d1d09..c0aed44 100644 --- a/meanas/fdfd/solvers.py +++ b/meanas/fdfd/solvers.py @@ -11,7 +11,7 @@ from numpy.typing import ArrayLike, NDArray from numpy.linalg import norm import scipy.sparse.linalg -from ..fdmath import dx_lists_t, vfdfield_t, vcfdfield_t +from ..fdmath import dx_lists_t, vfdfield, vcfdfield, vcfdfield_t from . import operators @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) def _scipy_qmr( - A: scipy.sparse.csr_matrix, + A: scipy.sparse.csr_array, b: ArrayLike, **kwargs: Any, ) -> NDArray[numpy.float64]: @@ -66,16 +66,16 @@ def _scipy_qmr( def generic( omega: complex, dxes: dx_lists_t, - J: vcfdfield_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, + J: vcfdfield, + epsilon: vfdfield, + mu: vfdfield | None = None, *, - pec: vfdfield_t | None = None, - pmc: vfdfield_t | None = None, + pec: vfdfield | None = None, + pmc: vfdfield | None = None, adjoint: bool = False, matrix_solver: Callable[..., ArrayLike] = _scipy_qmr, matrix_solver_opts: dict[str, Any] | None = None, - E_guess: vcfdfield_t | None = None, + E_guess: vcfdfield | None = None, ) -> vcfdfield_t: """ Conjugate gradient FDFD solver using CSR sparse matrices. @@ -95,7 +95,7 @@ def generic( (at H-field locations; non-zero value indicates PMC is present) adjoint: If true, solves the adjoint problem. matrix_solver: Called as `matrix_solver(A, b, **matrix_solver_opts) -> x`, - where `A`: `scipy.sparse.csr_matrix`; + where `A`: `scipy.sparse.csr_array`; `b`: `ArrayLike`; `x`: `ArrayLike`; Default is a wrapped version of `scipy.sparse.linalg.qmr()` @@ -138,4 +138,4 @@ def generic( else: x0 = Pr @ x - return x0 + return vcfdfield_t(x0) diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 5fda683..e8f766b 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -180,12 +180,12 @@ if the result is introduced into a space with a discretized z-axis. from typing import Any from collections.abc import Sequence import numpy -from numpy.typing import NDArray, ArrayLike +from numpy.typing import NDArray from numpy.linalg import norm from scipy import sparse from ..fdmath.operators import deriv_forward, deriv_back, cross -from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t +from ..fdmath import vec, unvec, dx_lists2_t, vcfdfield2_t, vcfdslice_t, vcfdfield2, vfdslice, vcfdslice from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration @@ -194,10 +194,10 @@ __author__ = 'Jan Petykiewicz' def operator_e( omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, - ) -> sparse.spmatrix: + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None, + ) -> sparse.sparray: r""" Waveguide operator of the form @@ -246,12 +246,12 @@ def operator_e( Dbx, Dby = deriv_back(dxes[1]) eps_parts = numpy.split(epsilon, 3) - eps_xy = sparse.diags(numpy.hstack((eps_parts[0], eps_parts[1]))) - eps_z_inv = sparse.diags(1 / eps_parts[2]) + eps_xy = sparse.diags_array(numpy.hstack((eps_parts[0], eps_parts[1]))) + eps_z_inv = sparse.diags_array(1 / eps_parts[2]) mu_parts = numpy.split(mu, 3) - mu_yx = sparse.diags(numpy.hstack((mu_parts[1], mu_parts[0]))) - mu_z_inv = sparse.diags(1 / mu_parts[2]) + mu_yx = sparse.diags_array(numpy.hstack((mu_parts[1], mu_parts[0]))) + mu_z_inv = sparse.diags_array(1 / mu_parts[2]) op = ( omega * omega * mu_yx @ eps_xy @@ -263,10 +263,10 @@ def operator_e( def operator_h( omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, - ) -> sparse.spmatrix: + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None, + ) -> sparse.sparray: r""" Waveguide operator of the form @@ -315,12 +315,12 @@ def operator_h( Dbx, Dby = deriv_back(dxes[1]) eps_parts = numpy.split(epsilon, 3) - eps_yx = sparse.diags(numpy.hstack((eps_parts[1], eps_parts[0]))) - eps_z_inv = sparse.diags(1 / eps_parts[2]) + eps_yx = sparse.diags_array(numpy.hstack((eps_parts[1], eps_parts[0]))) + eps_z_inv = sparse.diags_array(1 / eps_parts[2]) mu_parts = numpy.split(mu, 3) - mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) - mu_z_inv = sparse.diags(1 / mu_parts[2]) + mu_xy = sparse.diags_array(numpy.hstack((mu_parts[0], mu_parts[1]))) + mu_z_inv = sparse.diags_array(1 / mu_parts[2]) op = ( omega * omega * eps_yx @ mu_xy @@ -331,14 +331,14 @@ def operator_h( def normalized_fields_e( - e_xy: ArrayLike, + e_xy: vcfdfield2, wavenumber: complex, omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None, prop_phase: float = 0, - ) -> tuple[vcfdfield_t, vcfdfield_t]: + ) -> tuple[vcfdslice_t, vcfdslice_t]: """ Given a vector `e_xy` containing the vectorized E_x and E_y fields, returns normalized, vectorized E and H fields for the system. @@ -366,14 +366,14 @@ def normalized_fields_e( def normalized_fields_h( - h_xy: ArrayLike, + h_xy: vcfdfield2, wavenumber: complex, omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None, prop_phase: float = 0, - ) -> tuple[vcfdfield_t, vcfdfield_t]: + ) -> tuple[vcfdslice_t, vcfdslice_t]: """ Given a vector `h_xy` containing the vectorized H_x and H_y fields, returns normalized, vectorized E and H fields for the system. @@ -401,21 +401,19 @@ def normalized_fields_h( def _normalized_fields( - e: vcfdfield_t, - h: vcfdfield_t, + e: vcfdslice, + h: vcfdslice, omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None, prop_phase: float = 0, - ) -> tuple[vcfdfield_t, vcfdfield_t]: + ) -> tuple[vcfdslice_t, vcfdslice_t]: # TODO documentation shape = [s.size for s in dxes[0]] - dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)] # Find time-averaged Sz and normalize to it # H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting - phase = numpy.exp(-1j * -prop_phase / 2) Sz_tavg = inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}' @@ -436,16 +434,16 @@ def _normalized_fields( e *= norm_factor h *= norm_factor - return e, h + return vcfdslice_t(e), vcfdslice_t(h) def exy2h( wavenumber: complex, omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None - ) -> sparse.spmatrix: + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None + ) -> sparse.sparray: """ Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, into a vectorized H containing all three H components @@ -468,10 +466,10 @@ def exy2h( def hxy2e( wavenumber: complex, omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None - ) -> sparse.spmatrix: + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None + ) -> sparse.sparray: """ Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields, into a vectorized E containing all three E components @@ -493,9 +491,9 @@ def hxy2e( def hxy2h( wavenumber: complex, - dxes: dx_lists_t, - mu: vfdfield_t | None = None - ) -> sparse.spmatrix: + dxes: dx_lists2_t, + mu: vfdslice | None = None + ) -> sparse.sparray: """ Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields, into a vectorized H containing all three H components @@ -514,22 +512,22 @@ def hxy2h( if mu is not None: mu_parts = numpy.split(mu, 3) - mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) - mu_z_inv = sparse.diags(1 / mu_parts[2]) + mu_xy = sparse.diags_array(numpy.hstack((mu_parts[0], mu_parts[1]))) + mu_z_inv = sparse.diags_array(1 / mu_parts[2]) hxy2hz = mu_z_inv @ hxy2hz @ mu_xy n_pts = dxes[1][0].size * dxes[1][1].size - op = sparse.vstack((sparse.eye(2 * n_pts), + op = sparse.vstack((sparse.eye_array(2 * n_pts), hxy2hz)) return op def exy2e( wavenumber: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - ) -> sparse.spmatrix: + dxes: dx_lists2_t, + epsilon: vfdslice, + ) -> sparse.sparray: r""" Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, into a vectorized E containing all three E components @@ -575,13 +573,13 @@ def exy2e( if epsilon is not None: epsilon_parts = numpy.split(epsilon, 3) - epsilon_xy = sparse.diags(numpy.hstack((epsilon_parts[0], epsilon_parts[1]))) - epsilon_z_inv = sparse.diags(1 / epsilon_parts[2]) + epsilon_xy = sparse.diags_array(numpy.hstack((epsilon_parts[0], epsilon_parts[1]))) + epsilon_z_inv = sparse.diags_array(1 / epsilon_parts[2]) exy2ez = epsilon_z_inv @ exy2ez @ epsilon_xy n_pts = dxes[0][0].size * dxes[0][1].size - op = sparse.vstack((sparse.eye(2 * n_pts), + op = sparse.vstack((sparse.eye_array(2 * n_pts), exy2ez)) return op @@ -589,12 +587,12 @@ def exy2e( def e2h( wavenumber: complex, omega: complex, - dxes: dx_lists_t, - mu: vfdfield_t | None = None - ) -> sparse.spmatrix: + dxes: dx_lists2_t, + mu: vfdslice | None = None + ) -> sparse.sparray: """ Returns an operator which, when applied to a vectorized E eigenfield, produces - the vectorized H eigenfield. + the vectorized H eigenfield slice. Args: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` @@ -607,19 +605,19 @@ def e2h( """ op = curl_e(wavenumber, dxes) / (-1j * omega) if mu is not None: - op = sparse.diags(1 / mu) @ op + op = sparse.diags_array(1 / mu) @ op return op def h2e( wavenumber: complex, omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t - ) -> sparse.spmatrix: + dxes: dx_lists2_t, + epsilon: vfdslice, + ) -> sparse.sparray: """ Returns an operator which, when applied to a vectorized H eigenfield, produces - the vectorized E eigenfield. + the vectorized E eigenfield slice. Args: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` @@ -630,13 +628,13 @@ def h2e( Returns: Sparse matrix representation of the operator. """ - op = sparse.diags(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes) + op = sparse.diags_array(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes) return op -def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: +def curl_e(wavenumber: complex, dxes: dx_lists2_t) -> sparse.sparray: """ - Discretized curl operator for use with the waveguide E field. + Discretized curl operator for use with the waveguide E field slice. Args: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` @@ -645,18 +643,18 @@ def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: Returns: Sparse matrix representation of the operator. """ - n = 1 - for d in dxes[0]: - n *= len(d) + nn = 1 + for dd in dxes[0]: + nn *= len(dd) - Bz = -1j * wavenumber * sparse.eye(n) + Bz = -1j * wavenumber * sparse.eye_array(nn) Dfx, Dfy = deriv_forward(dxes[0]) return cross([Dfx, Dfy, Bz]) -def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: +def curl_h(wavenumber: complex, dxes: dx_lists2_t) -> sparse.sparray: """ - Discretized curl operator for use with the waveguide H field. + Discretized curl operator for use with the waveguide H field slice. Args: wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` @@ -665,22 +663,22 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: Returns: Sparse matrix representation of the operator. """ - n = 1 - for d in dxes[1]: - n *= len(d) + nn = 1 + for dd in dxes[1]: + nn *= len(dd) - Bz = -1j * wavenumber * sparse.eye(n) + Bz = -1j * wavenumber * sparse.eye_array(nn) Dbx, Dby = deriv_back(dxes[1]) return cross([Dbx, Dby, Bz]) def h_err( - h: vcfdfield_t, + h: vcfdslice, wavenumber: complex, omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None ) -> float: """ Calculates the relative error in the H field @@ -699,7 +697,7 @@ def h_err( ce = curl_e(wavenumber, dxes) ch = curl_h(wavenumber, dxes) - eps_inv = sparse.diags(1 / epsilon) + eps_inv = sparse.diags_array(1 / epsilon) if mu is None: op = ce @ eps_inv @ ch @ h - omega ** 2 * h @@ -710,12 +708,12 @@ def h_err( def e_err( - e: vcfdfield_t, + e: vcfdslice, wavenumber: complex, omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None, ) -> float: """ Calculates the relative error in the E field @@ -737,21 +735,21 @@ def e_err( if mu is None: op = ch @ ce @ e - omega ** 2 * (epsilon * e) else: - mu_inv = sparse.diags(1 / mu) + mu_inv = sparse.diags_array(1 / mu) op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e) return float(norm(op) / norm(e)) def sensitivity( - e_norm: vcfdfield_t, - h_norm: vcfdfield_t, + e_norm: vcfdslice, + h_norm: vcfdslice, wavenumber: complex, omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, - ) -> vcfdfield_t: + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None, + ) -> vcfdslice_t: r""" Given a waveguide structure (`dxes`, `epsilon`, `mu`) and mode fields (`e_norm`, `h_norm`, `wavenumber`, `omega`), calculates the sensitivity of the wavenumber @@ -825,11 +823,11 @@ def sensitivity( Dbx, Dby = deriv_back(dxes[1]) eps_x, eps_y, eps_z = numpy.split(epsilon, 3) - eps_xy = sparse.diags(numpy.hstack((eps_x, eps_y))) - eps_z_inv = sparse.diags(1 / eps_z) + eps_xy = sparse.diags_array(numpy.hstack((eps_x, eps_y))) + eps_z_inv = sparse.diags_array(1 / eps_z) mu_x, mu_y, _mu_z = numpy.split(mu, 3) - mu_yx = sparse.diags(numpy.hstack((mu_y, mu_x))) + mu_yx = sparse.diags_array(numpy.hstack((mu_y, mu_x))) da_exxhyy = vec(dxes[1][0][:, None] * dxes[0][1][None, :]) da_eyyhxx = vec(dxes[1][1][None, :] * dxes[0][0][:, None]) @@ -843,15 +841,15 @@ def sensitivity( norm = hv_yx_conj @ ev_xy sens_tot = numpy.concatenate([sens_xy1 + sens_xy2, sens_z]) / (2 * wavenumber * norm) - return sens_tot + return vcfdslice_t(sens_tot) def solve_modes( mode_numbers: Sequence[int], omega: complex, - dxes: dx_lists_t, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, + dxes: dx_lists2_t, + epsilon: vfdslice, + mu: vfdslice | None = None, mode_margin: int = 2, ) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]: """ @@ -907,7 +905,7 @@ def solve_mode( mode_number: int, *args: Any, **kwargs: Any, - ) -> tuple[vcfdfield_t, complex]: + ) -> tuple[vcfdfield2_t, complex]: """ Wrapper around `solve_modes()` that solves for a single mode. @@ -921,13 +919,13 @@ def solve_mode( """ kwargs['mode_numbers'] = [mode_number] e_xys, wavenumbers = solve_modes(*args, **kwargs) - return e_xys[0], wavenumbers[0] + return vcfdfield2_t(e_xys[0]), wavenumbers[0] def inner_product( # TODO documentation - e1: vcfdfield_t, - h2: vcfdfield_t, - dxes: dx_lists_t, + e1: vcfdfield2, + h2: vcfdfield2, + dxes: dx_lists2_t, prop_phase: float = 0, conj_h: bool = False, trapezoid: bool = False, diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index 6e2a2db..da50533 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -4,13 +4,13 @@ Tools for working with waveguide modes in 3D domains. This module relies heavily on `waveguide_2d` and mostly just transforms its parameters into 2D equivalents and expands the results back into 3D. """ -from typing import Any +from typing import Any, cast from collections.abc import Sequence import numpy from numpy.typing import NDArray from numpy import complexfloating -from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, cfdfield_t +from ..fdmath import vec, unvec, dx_lists_t, cfdfield_t, fdfield, cfdfield from . import operators, waveguide_2d @@ -21,8 +21,8 @@ def solve_mode( axis: int, polarity: int, slices: Sequence[slice], - epsilon: fdfield_t, - mu: fdfield_t | None = None, + epsilon: fdfield, + mu: fdfield | None = None, ) -> dict[str, complex | NDArray[complexfloating]]: """ Given a 3D grid, selects a slice from the grid and attempts to @@ -95,9 +95,10 @@ def solve_mode( # Expand E, H to full epsilon space we were given E = numpy.zeros_like(epsilon, dtype=complex) H = numpy.zeros_like(epsilon, dtype=complex) - for a, o in enumerate(reverse_order): - E[(a, *slices)] = e[o][:, :, None].transpose(reverse_order) - H[(a, *slices)] = h[o][:, :, None].transpose(reverse_order) + for aa, oo in enumerate(reverse_order): + iii = cast('tuple[slice | int]', (aa, *slices)) + E[iii] = e[oo][:, :, None].transpose(reverse_order) + H[iii] = h[oo][:, :, None].transpose(reverse_order) results = { 'wavenumber': wavenumber, @@ -109,15 +110,15 @@ def solve_mode( def compute_source( - E: cfdfield_t, + E: cfdfield, wavenumber: complex, omega: complex, dxes: dx_lists_t, axis: int, polarity: int, slices: Sequence[slice], - epsilon: fdfield_t, - mu: fdfield_t | None = None, + epsilon: fdfield, + mu: fdfield | None = None, ) -> cfdfield_t: """ Given an eigenmode obtained by `solve_mode`, returns the current source distribution @@ -151,11 +152,11 @@ def compute_source( masked_e2j = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon), mu=vec(mu)) J = unvec(masked_e2j @ vec(E_expanded), E.shape[1:]) - return J + return cfdfield_t(J) def compute_overlap_e( - E: cfdfield_t, + E: cfdfield, wavenumber: complex, dxes: dx_lists_t, axis: int, @@ -195,12 +196,12 @@ def compute_overlap_e( Etgt = numpy.zeros_like(Ee) Etgt[slices2] = Ee[slices2] - Etgt /= (Etgt.conj() * Etgt).sum() - return Etgt + Etgt /= (Etgt.conj() * Etgt).sum() # type: ignore + return cfdfield_t(Etgt) def expand_e( - E: cfdfield_t, + E: cfdfield, wavenumber: complex, dxes: dx_lists_t, axis: int, @@ -245,4 +246,4 @@ def expand_e( slices_in = (slice(None), *slices) E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in] - return E_expanded + return cfdfield_t(E_expanded) diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index b65e038..597e1cb 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -68,7 +68,7 @@ import numpy from numpy.typing import NDArray, ArrayLike from scipy import sparse -from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t +from ..fdmath import vec, unvec, dx_lists2_t, vcfdslice_t, vcfdfield2_t, vfdslice, vcfdslice, vcfdfield2 from ..fdmath.operators import deriv_forward, deriv_back from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration from . import waveguide_2d @@ -78,10 +78,10 @@ logger = logging.getLogger(__name__) def cylindrical_operator( omega: float, - dxes: dx_lists_t, - epsilon: vfdfield_t, + dxes: dx_lists2_t, + epsilon: vfdslice, rmin: float, - ) -> sparse.spmatrix: + ) -> sparse.sparray: r""" Cylindrical coordinate waveguide operator of the form @@ -143,11 +143,11 @@ def cylindrical_operator( def solve_modes( mode_numbers: Sequence[int], omega: float, - dxes: dx_lists_t, - epsilon: vfdfield_t, + dxes: dx_lists2_t, + epsilon: vfdslice, rmin: float, mode_margin: int = 2, - ) -> tuple[vcfdfield_t, NDArray[numpy.complex128]]: + ) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]: """ Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode of the bent waveguide with the specified mode number. @@ -191,7 +191,7 @@ def solve_modes( # Wavenumbers assume the mode is at rmin, which is unlikely # Instead, return the wavenumber in inverse radians - angular_wavenumbers = wavenumbers * cast(complex, rmin) + angular_wavenumbers = wavenumbers * cast('complex', rmin) order = angular_wavenumbers.argsort()[::-1] e_xys = e_xys[order] @@ -204,7 +204,7 @@ def solve_mode( mode_number: int, *args: Any, **kwargs: Any, - ) -> tuple[vcfdfield_t, complex]: + ) -> tuple[vcfdslice, complex]: """ Wrapper around `solve_modes()` that solves for a single mode. @@ -222,10 +222,10 @@ def solve_mode( def linear_wavenumbers( - e_xys: vcfdfield_t, + e_xys: list[vcfdfield2_t], angular_wavenumbers: ArrayLike, - epsilon: vfdfield_t, - dxes: dx_lists_t, + epsilon: vfdslice, + dxes: dx_lists2_t, rmin: float, ) -> NDArray[numpy.complex128]: """ @@ -247,7 +247,6 @@ def linear_wavenumbers( angular_wavenumbers = numpy.asarray(angular_wavenumbers) mode_radii = numpy.empty_like(angular_wavenumbers, dtype=float) - wavenumbers = numpy.empty_like(angular_wavenumbers) shape2d = (len(dxes[0][0]), len(dxes[0][1])) epsilon2d = unvec(epsilon, shape2d)[:2] grid_radii = rmin + numpy.cumsum(dxes[0][0]) @@ -265,11 +264,11 @@ def linear_wavenumbers( def exy2h( angular_wavenumber: complex, omega: float, - dxes: dx_lists_t, + dxes: dx_lists2_t, rmin: float, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None - ) -> sparse.spmatrix: + epsilon: vfdslice, + mu: vfdslice | None = None + ) -> sparse.sparray: """ Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, into a vectorized H containing all three H components @@ -294,10 +293,10 @@ def exy2h( def exy2e( angular_wavenumber: complex, omega: float, - dxes: dx_lists_t, + dxes: dx_lists2_t, rmin: float, - epsilon: vfdfield_t, - ) -> sparse.spmatrix: + epsilon: vfdslice, + ) -> sparse.sparray: """ Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, into a vectorized E containing all three E components @@ -323,7 +322,7 @@ def exy2e( Ta, Tb = dxes2T(dxes=dxes, rmin=rmin) Tai = sparse.diags_array(1 / Ta.diagonal()) - Tbi = sparse.diags_array(1 / Tb.diagonal()) + #Tbi = sparse.diags_array(1 / Tb.diagonal()) epsilon_parts = numpy.split(epsilon, 3) epsilon_x, epsilon_y = (sparse.diags_array(epsi) for epsi in epsilon_parts[:2]) @@ -331,8 +330,6 @@ def exy2e( n_pts = dxes[0][0].size * dxes[0][1].size zeros = sparse.coo_array((n_pts, n_pts)) - keep_x = sparse.block_array([[sparse.eye_array(n_pts), None], [None, zeros]]) - keep_y = sparse.block_array([[zeros, None], [None, sparse.eye_array(n_pts)]]) mu_z = numpy.ones(n_pts) mu_z_inv = sparse.diags_array(1 / mu_z) @@ -352,10 +349,10 @@ def exy2e( def e2h( angular_wavenumber: complex, omega: float, - dxes: dx_lists_t, + dxes: dx_lists2_t, rmin: float, - mu: vfdfield_t | None = None - ) -> sparse.spmatrix: + mu: vfdslice | None = None + ) -> sparse.sparray: r""" Returns an operator which, when applied to a vectorized E eigenfield, produces the vectorized H eigenfield. @@ -396,7 +393,7 @@ def e2h( def dxes2T( - dxes: dx_lists_t, + dxes: dx_lists2_t, rmin: float, ) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]: r""" @@ -421,15 +418,15 @@ def dxes2T( def normalized_fields_e( - e_xy: ArrayLike, + e_xy: vcfdfield2, angular_wavenumber: complex, omega: float, - dxes: dx_lists_t, + dxes: dx_lists2_t, rmin: float, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, + epsilon: vfdslice, + mu: vfdslice | None = None, prop_phase: float = 0, - ) -> tuple[vcfdfield_t, vcfdfield_t]: + ) -> tuple[vcfdslice_t, vcfdslice_t]: """ Given a vector `e_xy` containing the vectorized E_x and E_y fields, returns normalized, vectorized E and H fields for the system. @@ -459,23 +456,21 @@ def normalized_fields_e( def _normalized_fields( - e: vcfdfield_t, - h: vcfdfield_t, + e: vcfdslice, + h: vcfdslice, omega: complex, - dxes: dx_lists_t, - rmin: float, - epsilon: vfdfield_t, - mu: vfdfield_t | None = None, + dxes: dx_lists2_t, + rmin: float, # Currently unused, but may want to use cylindrical poynting + epsilon: vfdslice, + mu: vfdslice | None = None, prop_phase: float = 0, - ) -> tuple[vcfdfield_t, vcfdfield_t]: + ) -> tuple[vcfdslice_t, vcfdslice_t]: h *= -1 # TODO documentation for normalized_fields shape = [s.size for s in dxes[0]] - dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)] # Find time-averaged Sz and normalize to it # H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting - phase = numpy.exp(-1j * -prop_phase / 2) Sz_tavg = waveguide_2d.inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real # Note, using linear poynting vector assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}' @@ -495,4 +490,4 @@ def _normalized_fields( e *= norm_factor h *= norm_factor - return e, h + return vcfdslice_t(e), vcfdslice_t(h) diff --git a/meanas/fdmath/__init__.py b/meanas/fdmath/__init__.py index b1d8354..857cf18 100644 --- a/meanas/fdmath/__init__.py +++ b/meanas/fdmath/__init__.py @@ -742,12 +742,34 @@ normalized results are needed. """ from .types import ( - fdfield_t as fdfield_t, - vfdfield_t as vfdfield_t, - cfdfield_t as cfdfield_t, - vcfdfield_t as vcfdfield_t, - dx_lists_t as dx_lists_t, - dx_lists_mut as dx_lists_mut, + fdfield_t as fdfield_t, + vfdfield_t as vfdfield_t, + cfdfield_t as cfdfield_t, + vcfdfield_t as vcfdfield_t, + fdfield2_t as fdfield2_t, + vfdfield2_t as vfdfield2_t, + cfdfield2_t as cfdfield2_t, + vcfdfield2_t as vcfdfield2_t, + fdfield as fdfield, + vfdfield as vfdfield, + cfdfield as cfdfield, + vcfdfield as vcfdfield, + fdfield2 as fdfield2, + vfdfield2 as vfdfield2, + cfdfield2 as cfdfield2, + vcfdfield2 as vcfdfield2, + fdslice_t as fdslice_t, + vfdslice_t as vfdslice_t, + cfdslice_t as cfdslice_t, + vcfdslice_t as vcfdslice_t, + fdslice as fdslice, + vfdslice as vfdslice, + cfdslice as cfdslice, + vcfdslice as vcfdslice, + dx_lists_t as dx_lists_t, + dx_lists2_t as dx_lists2_t, + dx_lists_mut as dx_lists_mut, + dx_lists2_mut as dx_lists2_mut, fdfield_updater_t as fdfield_updater_t, cfdfield_updater_t as cfdfield_updater_t, ) diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py index 946eb88..19ccb80 100644 --- a/meanas/fdmath/operators.py +++ b/meanas/fdmath/operators.py @@ -16,7 +16,7 @@ def shift_circ( axis: int, shape: Sequence[int], shift_distance: int = 1, - ) -> sparse.spmatrix: + ) -> sparse.sparray: """ Utility operator for performing a circular shift along a specified axis by a specified number of elements. @@ -44,7 +44,7 @@ def shift_circ( vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) - d = sparse.csr_matrix(vij, shape=(n, n)) + d = sparse.csr_array(vij, shape=(n, n)) if shift_distance < 0: d = d.T @@ -56,7 +56,7 @@ def shift_with_mirror( axis: int, shape: Sequence[int], shift_distance: int = 1, - ) -> sparse.spmatrix: + ) -> sparse.sparray: """ Utility operator for performing an n-element shift along a specified axis, with mirror boundary conditions applied to the cells beyond the receding edge. @@ -92,13 +92,13 @@ def shift_with_mirror( vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) - d = sparse.csr_matrix(vij, shape=(n, n)) + d = sparse.csr_array(vij, shape=(n, n)) return d def deriv_forward( dx_e: Sequence[NDArray[floating | complexfloating]], - ) -> list[sparse.spmatrix]: + ) -> list[sparse.sparray]: """ Utility operators for taking discretized derivatives (forward variant). @@ -114,10 +114,10 @@ def deriv_forward( dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij') - def deriv(axis: int) -> sparse.spmatrix: - return shift_circ(axis, shape, 1) - sparse.eye(n) + def deriv(axis: int) -> sparse.sparray: + return shift_circ(axis, shape, 1) - sparse.eye_array(n) - Ds = [sparse.diags(+1 / dx.ravel(order='C')) @ deriv(a) + Ds = [sparse.diags_array(+1 / dx.ravel(order='C')) @ deriv(a) for a, dx in enumerate(dx_e_expanded)] return Ds @@ -125,7 +125,7 @@ def deriv_forward( def deriv_back( dx_h: Sequence[NDArray[floating | complexfloating]], - ) -> list[sparse.spmatrix]: + ) -> list[sparse.sparray]: """ Utility operators for taking discretized derivatives (backward variant). @@ -141,18 +141,18 @@ def deriv_back( dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij') - def deriv(axis: int) -> sparse.spmatrix: - return shift_circ(axis, shape, -1) - sparse.eye(n) + def deriv(axis: int) -> sparse.sparray: + return shift_circ(axis, shape, -1) - sparse.eye_array(n) - Ds = [sparse.diags(-1 / dx.ravel(order='C')) @ deriv(a) + Ds = [sparse.diags_array(-1 / dx.ravel(order='C')) @ deriv(a) for a, dx in enumerate(dx_h_expanded)] return Ds def cross( - B: Sequence[sparse.spmatrix], - ) -> sparse.spmatrix: + B: Sequence[sparse.sparray], + ) -> sparse.sparray: """ Cross product operator @@ -164,13 +164,14 @@ def cross( Sparse matrix corresponding to (B x), where x is the cross product. """ n = B[0].shape[0] - zero = sparse.csr_matrix((n, n)) - return sparse.bmat([[zero, -B[2], B[1]], - [B[2], zero, -B[0]], - [-B[1], B[0], zero]]) + zero = sparse.csr_array((n, n)) + return sparse.block_array([ + [zero, -B[2], B[1]], + [B[2], zero, -B[0]], + [-B[1], B[0], zero]]) -def vec_cross(b: vfdfield_t) -> sparse.spmatrix: +def vec_cross(b: vfdfield_t) -> sparse.sparray: """ Vector cross product operator @@ -182,11 +183,11 @@ def vec_cross(b: vfdfield_t) -> sparse.spmatrix: Sparse matrix corresponding to (b x), where x is the cross product. """ - B = [sparse.diags(c) for c in numpy.split(b, 3)] + B = [sparse.diags_array(c) for c in numpy.split(b, 3)] return cross(B) -def avg_forward(axis: int, shape: Sequence[int]) -> sparse.spmatrix: +def avg_forward(axis: int, shape: Sequence[int]) -> sparse.sparray: """ Forward average operator `(x4 = (x4 + x5) / 2)` @@ -201,10 +202,10 @@ def avg_forward(axis: int, shape: Sequence[int]) -> sparse.spmatrix: raise Exception(f'Invalid shape: {shape}') n = numpy.prod(shape) - return 0.5 * (sparse.eye(n) + shift_circ(axis, shape)) + return 0.5 * (sparse.eye_array(n) + shift_circ(axis, shape)) -def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix: +def avg_back(axis: int, shape: Sequence[int]) -> sparse.sparray: """ Backward average operator `(x4 = (x4 + x3) / 2)` @@ -220,7 +221,7 @@ def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix: def curl_forward( dx_e: Sequence[NDArray[floating | complexfloating]], - ) -> sparse.spmatrix: + ) -> sparse.sparray: """ Curl operator for use with the E field. @@ -236,7 +237,7 @@ def curl_forward( def curl_back( dx_h: Sequence[NDArray[floating | complexfloating]], - ) -> sparse.spmatrix: + ) -> sparse.sparray: """ Curl operator for use with the H field. diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py index d44b30a..222d18a 100644 --- a/meanas/fdmath/types.py +++ b/meanas/fdmath/types.py @@ -1,25 +1,64 @@ """ Types shared across multiple submodules """ +from typing import NewType from collections.abc import Sequence, Callable, MutableSequence from numpy.typing import NDArray from numpy import floating, complexfloating # Field types -fdfield_t = NDArray[floating] +fdfield_t = NewType('fdfield_t', NDArray[floating]) +type fdfield = fdfield_t | NDArray[floating] """Vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)""" -vfdfield_t = NDArray[floating] +vfdfield_t = NewType('vfdfield_t', NDArray[floating]) +type vfdfield = vfdfield_t | NDArray[floating] """Linearized vector field (single vector of length 3*X*Y*Z)""" -cfdfield_t = NDArray[complexfloating] +cfdfield_t = NewType('cfdfield_t', NDArray[complexfloating]) +type cfdfield = cfdfield_t | NDArray[complexfloating] """Complex vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)""" -vcfdfield_t = NDArray[complexfloating] +vcfdfield_t = NewType('vcfdfield_t', NDArray[complexfloating]) +type vcfdfield = vcfdfield_t | NDArray[complexfloating] """Linearized complex vector field (single vector of length 3*X*Y*Z)""" +fdslice_t = NewType('fdslice_t', NDArray[floating]) +type fdslice = fdslice_t | NDArray[floating] +"""Vector field slice with shape (3, X, Y) (e.g. `[E_x, E_y, E_z]` at a single Z position)""" + +vfdslice_t = NewType('vfdslice_t', NDArray[floating]) +type vfdslice = vfdslice_t | NDArray[floating] +"""Linearized vector field slice (single vector of length 3*X*Y)""" + +cfdslice_t = NewType('cfdslice_t', NDArray[complexfloating]) +type cfdslice = cfdslice_t | NDArray[complexfloating] +"""Complex vector field slice with shape (3, X, Y) (e.g. `[E_x, E_y, E_z]` at a single Z position)""" + +vcfdslice_t = NewType('vcfdslice_t', NDArray[complexfloating]) +type vcfdslice = vcfdslice_t | NDArray[complexfloating] +"""Linearized complex vector field slice (single vector of length 3*X*Y)""" + + +fdfield2_t = NewType('fdfield2_t', NDArray[floating]) +type fdfield2 = fdfield2_t | NDArray[floating] +"""2D Vector field with shape (2, X, Y) (e.g. `[E_x, E_y]`)""" + +vfdfield2_t = NewType('vfdfield2_t', NDArray[floating]) +type vfdfield2 = vfdfield2_t | NDArray[floating] +"""2D Linearized vector field (single vector of length 2*X*Y)""" + +cfdfield2_t = NewType('cfdfield2_t', NDArray[complexfloating]) +type cfdfield2 = cfdfield2_t | NDArray[complexfloating] +"""2D Complex vector field with shape (2, X, Y) (e.g. `[E_x, E_y]`)""" + +vcfdfield2_t = NewType('vcfdfield2_t', NDArray[complexfloating]) +type vcfdfield2 = vcfdfield2_t | NDArray[complexfloating] +"""2D Linearized complex vector field (single vector of length 2*X*Y)""" + + dx_lists_t = Sequence[Sequence[NDArray[floating | complexfloating]]] """ 'dxes' datastructure which contains grid cell width information in the following format: @@ -31,9 +70,23 @@ dx_lists_t = Sequence[Sequence[NDArray[floating | complexfloating]]] and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc. """ +dx_lists2_t = Sequence[Sequence[NDArray[floating | complexfloating]]] +""" + 2D 'dxes' datastructure which contains grid cell width information in the following format: + + [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...]], + [[dx_h[0], dx_h[1], ...], [dy_h[0], ...]]] + + where `dx_e[0]` is the x-width of the `x=0` cells, as used when calculating dE/dx, + and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc. +""" + dx_lists_mut = MutableSequence[MutableSequence[NDArray[floating | complexfloating]]] """Mutable version of `dx_lists_t`""" +dx_lists2_mut = MutableSequence[MutableSequence[NDArray[floating | complexfloating]]] +"""Mutable version of `dx_lists2_t`""" + fdfield_updater_t = Callable[..., fdfield_t] """Convenience type for functions which take and return an fdfield_t""" diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py index 8f5ff39..f2c01d0 100644 --- a/meanas/fdmath/vectorization.py +++ b/meanas/fdmath/vectorization.py @@ -7,9 +7,13 @@ Vectorized versions of the field use row-major (ie., C-style) ordering. from typing import overload from collections.abc import Sequence import numpy -from numpy.typing import ArrayLike +from numpy.typing import ArrayLike, NDArray -from .types import fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t +from .types import ( + fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t, + fdslice_t, vfdslice_t, cfdslice_t, vcfdslice_t, + fdfield2_t, vfdfield2_t, cfdfield2_t, vcfdfield2_t, + ) @overload @@ -25,12 +29,28 @@ def vec(f: cfdfield_t) -> vcfdfield_t: pass @overload -def vec(f: ArrayLike) -> vfdfield_t | vcfdfield_t: +def vec(f: fdfield2_t) -> vfdfield2_t: + pass + +@overload +def vec(f: cfdfield2_t) -> vcfdfield2_t: + pass + +@overload +def vec(f: fdslice_t) -> vfdslice_t: + pass + +@overload +def vec(f: cfdslice_t) -> vcfdslice_t: + pass + +@overload +def vec(f: ArrayLike) -> NDArray: pass def vec( - f: fdfield_t | cfdfield_t | ArrayLike | None, - ) -> vfdfield_t | vcfdfield_t | None: + f: fdfield_t | cfdfield_t | fdfield2_t | cfdfield2_t | fdslice_t | cfdslice_t | ArrayLike | None, + ) -> vfdfield_t | vcfdfield_t | vfdfield2_t | vcfdfield2_t | vfdslice_t | vcfdslice_t | NDArray | None: """ Create a 1D ndarray from a vector field which spans a 1-3D region. @@ -45,7 +65,7 @@ def vec( """ if f is None: return None - return numpy.ravel(f, order='C') + return numpy.ravel(f, order='C') # type: ignore @overload @@ -60,11 +80,31 @@ def unvec(v: vfdfield_t, shape: Sequence[int], nvdim: int = 3) -> fdfield_t: def unvec(v: vcfdfield_t, shape: Sequence[int], nvdim: int = 3) -> cfdfield_t: pass +@overload +def unvec(v: vfdfield2_t, shape: Sequence[int], nvdim: int = 3) -> fdfield2_t: + pass + +@overload +def unvec(v: vcfdfield2_t, shape: Sequence[int], nvdim: int = 3) -> cfdfield2_t: + pass + +@overload +def unvec(v: vfdslice_t, shape: Sequence[int], nvdim: int = 3) -> fdslice_t: + pass + +@overload +def unvec(v: vcfdslice_t, shape: Sequence[int], nvdim: int = 3) -> cfdslice_t: + pass + +@overload +def unvec(v: ArrayLike, shape: Sequence[int], nvdim: int = 3) -> NDArray: + pass + def unvec( - v: vfdfield_t | vcfdfield_t | None, + v: vfdfield_t | vcfdfield_t | vfdfield2_t | vcfdfield2_t | vfdslice_t | vcfdslice_t | ArrayLike | None, shape: Sequence[int], nvdim: int = 3, - ) -> fdfield_t | cfdfield_t | None: + ) -> fdfield_t | cfdfield_t | fdfield2_t | cfdfield2_t | fdslice_t | cfdslice_t | NDArray | None: """ Perform the inverse of vec(): take a 1D ndarray and output an `nvdim`-component field of form e.g. `[f_x, f_y, f_z]` (`nvdim=3`) where each of `f_*` is a len(shape)-dimensional @@ -82,5 +122,5 @@ def unvec( """ if v is None: return None - return v.reshape((nvdim, *shape), order='C') + return v.reshape((nvdim, *shape), order='C') # type: ignore diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index 43ea3a1..76888ca 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -1,6 +1,6 @@ import numpy -from ..fdmath import dx_lists_t, fdfield_t +from ..fdmath import dx_lists_t, fdfield_t, fdfield from ..fdmath.functional import deriv_back @@ -8,8 +8,8 @@ from ..fdmath.functional import deriv_back def poynting( - e: fdfield_t, - h: fdfield_t, + e: fdfield, + h: fdfield, dxes: dx_lists_t | None = None, ) -> fdfield_t: r""" @@ -84,14 +84,14 @@ def poynting( s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx - return s + return fdfield_t(s) def poynting_divergence( - s: fdfield_t | None = None, + s: fdfield | None = None, *, - e: fdfield_t | None = None, - h: fdfield_t | None = None, + e: fdfield | None = None, + h: fdfield | None = None, dxes: dx_lists_t | None = None, ) -> fdfield_t: """ @@ -122,15 +122,15 @@ def poynting_divergence( Dx, Dy, Dz = deriv_back() ds = Dx(s[0]) + Dy(s[1]) + Dz(s[2]) - return ds + return fdfield_t(ds) def energy_hstep( - e0: fdfield_t, - h1: fdfield_t, - e2: fdfield_t, - epsilon: fdfield_t | None = None, - mu: fdfield_t | None = None, + e0: fdfield, + h1: fdfield, + e2: fdfield, + epsilon: fdfield | None = None, + mu: fdfield | None = None, dxes: dx_lists_t | None = None, ) -> fdfield_t: """ @@ -150,15 +150,15 @@ def energy_hstep( Energy, at the time of the H-field `h1`. """ u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) - return u + return fdfield_t(u) def energy_estep( - h0: fdfield_t, - e1: fdfield_t, - h2: fdfield_t, - epsilon: fdfield_t | None = None, - mu: fdfield_t | None = None, + h0: fdfield, + e1: fdfield, + h2: fdfield, + epsilon: fdfield | None = None, + mu: fdfield | None = None, dxes: dx_lists_t | None = None, ) -> fdfield_t: """ @@ -178,17 +178,17 @@ def energy_estep( Energy, at the time of the E-field `e1`. """ u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) - return u + return fdfield_t(u) def delta_energy_h2e( dt: float, - e0: fdfield_t, - h1: fdfield_t, - e2: fdfield_t, - h3: fdfield_t, - epsilon: fdfield_t | None = None, - mu: fdfield_t | None = None, + e0: fdfield, + h1: fdfield, + e2: fdfield, + h3: fdfield, + epsilon: fdfield | None = None, + mu: fdfield | None = None, dxes: dx_lists_t | None = None, ) -> fdfield_t: """ @@ -211,17 +211,17 @@ def delta_energy_h2e( de = e2 * (e2 - e0) / dt dh = h1 * (h3 - h1) / dt du = dxmul(de, dh, epsilon, mu, dxes) - return du + return fdfield_t(du) def delta_energy_e2h( dt: float, - h0: fdfield_t, - e1: fdfield_t, - h2: fdfield_t, - e3: fdfield_t, - epsilon: fdfield_t | None = None, - mu: fdfield_t | None = None, + h0: fdfield, + e1: fdfield, + h2: fdfield, + e3: fdfield, + epsilon: fdfield | None = None, + mu: fdfield | None = None, dxes: dx_lists_t | None = None, ) -> fdfield_t: """ @@ -244,12 +244,12 @@ def delta_energy_e2h( de = e1 * (e3 - e1) / dt dh = h2 * (h2 - h0) / dt du = dxmul(de, dh, epsilon, mu, dxes) - return du + return fdfield_t(du) def delta_energy_j( - j0: fdfield_t, - e1: fdfield_t, + j0: fdfield, + e1: fdfield, dxes: dx_lists_t | None = None, ) -> fdfield_t: """ @@ -267,14 +267,14 @@ def delta_energy_j( * dxes[0][0][:, None, None] * dxes[0][1][None, :, None] * dxes[0][2][None, None, :]) - return du + return fdfield_t(du) def dxmul( - ee: fdfield_t, - hh: fdfield_t, - epsilon: fdfield_t | float | None = None, - mu: fdfield_t | float | None = None, + ee: fdfield, + hh: fdfield, + epsilon: fdfield | float | None = None, + mu: fdfield | float | None = None, dxes: dx_lists_t | None = None, ) -> fdfield_t: if epsilon is None: @@ -292,4 +292,4 @@ def dxmul( * dxes[1][0][:, None, None] * dxes[1][1][None, :, None] * dxes[1][2][None, None, :]) - return result + return fdfield_t(result) diff --git a/meanas/fdtd/misc.py b/meanas/fdtd/misc.py index 160682d..3fb3371 100644 --- a/meanas/fdtd/misc.py +++ b/meanas/fdtd/misc.py @@ -1,5 +1,4 @@ -from typing import Callable -from collections.abc import Sequence +from collections.abc import Callable import logging import numpy @@ -95,10 +94,10 @@ def ricker_pulse( logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO omega = 2 * pi / wl freq = 1 / wl - r0 = omega / 2 + # r0 = omega / 2 from scipy.optimize import root_scalar - delay_results = root_scalar(lambda xx: (1 - omega * omega * tt * tt / 2) * numpy.exp(-omega * omega / 4 * tt * tt) - turn_on, x0=0, x1=-2 / omega) + delay_results = root_scalar(lambda tt: (1 - omega * omega * tt * tt / 2) * numpy.exp(-omega * omega / 4 * tt * tt) - turn_on, x0=0, x1=-2 / omega) delay = delay_results.root delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py index 8098da0..fc456eb 100644 --- a/meanas/fdtd/pml.py +++ b/meanas/fdtd/pml.py @@ -13,7 +13,7 @@ from copy import deepcopy import numpy from numpy.typing import NDArray, DTypeLike -from ..fdmath import fdfield_t, dx_lists_t +from ..fdmath import fdfield, fdfield_t, dx_lists_t from ..fdmath.functional import deriv_forward, deriv_back @@ -97,7 +97,7 @@ def updates_with_cpml( cpml_params: Sequence[Sequence[dict[str, Any] | None]], dt: float, dxes: dx_lists_t, - epsilon: fdfield_t, + epsilon: fdfield, *, dtype: DTypeLike = numpy.float32, ) -> tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None], diff --git a/meanas/test/test_fdfd.py b/meanas/test/test_fdfd.py index 5f2cf11..82587b4 100644 --- a/meanas/test/test_fdfd.py +++ b/meanas/test/test_fdfd.py @@ -6,7 +6,7 @@ from numpy.typing import NDArray #from numpy.testing import assert_allclose, assert_array_equal from .. import fdfd -from ..fdmath import vec, unvec +from ..fdmath import vec, unvec, vcfdfield, vfdfield, dx_lists_t from .utils import assert_close # , assert_fields_close from .conftest import FixtureRequest @@ -102,16 +102,16 @@ def j_distribution( @dataclasses.dataclass() class FDResult: shape: tuple[int, ...] - dxes: list[list[NDArray[numpy.float64]]] - epsilon: NDArray[numpy.float64] + dxes: dx_lists_t + epsilon: vfdfield omega: complex - j: NDArray[numpy.complex128] - e: NDArray[numpy.complex128] - pmc: NDArray[numpy.float64] | None - pec: NDArray[numpy.float64] | None + j: vcfdfield + e: vcfdfield + pmc: vfdfield | None + pec: vfdfield | None -@pytest.fixture() +@pytest.fixture def sim( request: FixtureRequest, shape: tuple[int, ...], @@ -141,11 +141,11 @@ def sim( j_vec = vec(j_distribution) eps_vec = vec(epsilon) e_vec = fdfd.solvers.generic( - J=j_vec, - omega=omega, - dxes=dxes, - epsilon=eps_vec, - matrix_solver_opts={'atol': 1e-15, 'rtol': 1e-11}, + J = j_vec, + omega = omega, + dxes = dxes, + epsilon = eps_vec, + matrix_solver_opts = dict(atol=1e-15, rtol=1e-11), ) e = unvec(e_vec, shape[1:]) diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py index 832053d..540a3a0 100644 --- a/meanas/test/test_fdfd_pml.py +++ b/meanas/test/test_fdfd_pml.py @@ -5,7 +5,7 @@ from numpy.typing import NDArray from numpy.testing import assert_allclose from .. import fdfd -from ..fdmath import vec, unvec, dx_lists_mut +from ..fdmath import vec, unvec, dx_lists_mut, vfdfield, cfdfield_t #from .utils import assert_close, assert_fields_close from .test_fdfd import FDResult from .conftest import FixtureRequest @@ -70,15 +70,15 @@ def src_polarity(request: FixtureRequest) -> int: return request.param -@pytest.fixture() +@pytest.fixture def j_distribution( request: FixtureRequest, shape: tuple[int, ...], - epsilon: NDArray[numpy.float64], + epsilon: vfdfield, dxes: dx_lists_mut, omega: float, src_polarity: int, - ) -> NDArray[numpy.complex128]: + ) -> cfdfield_t: j = numpy.zeros(shape, dtype=complex) dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis @@ -109,7 +109,7 @@ def j_distribution( return j -@pytest.fixture() +@pytest.fixture def epsilon( request: FixtureRequest, shape: tuple[int, ...], @@ -144,7 +144,7 @@ def dxes( return dxes -@pytest.fixture() +@pytest.fixture def sim( request: FixtureRequest, shape: tuple[int, ...], diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 25ee891..03d2b7e 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -189,7 +189,7 @@ def j_distribution( return j -@pytest.fixture() +@pytest.fixture def sim( request: FixtureRequest, shape: tuple[int, ...], From d4f1008c5c863d9fba1fca11cc14b6c99e5e4078 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 10 Dec 2025 19:45:26 -0800 Subject: [PATCH 383/437] [fdfd.waveguide*] comment updates --- meanas/fdfd/waveguide_2d.py | 1 - meanas/fdfd/waveguide_3d.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index e8f766b..7d1f651 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -413,7 +413,6 @@ def _normalized_fields( shape = [s.size for s in dxes[0]] # Find time-averaged Sz and normalize to it - # H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting Sz_tavg = inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}' diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index da50533..5048dea 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -45,6 +45,7 @@ def solve_mode( 'E': NDArray[complexfloating], 'H': NDArray[complexfloating], 'wavenumber': complex, + 'wavenumber_2d': complex, } ``` """ @@ -196,6 +197,8 @@ def compute_overlap_e( Etgt = numpy.zeros_like(Ee) Etgt[slices2] = Ee[slices2] + # note no sqrt() when normalizing below since we want to get 1.0 after overlapping with the + # original field, not the normalized one Etgt /= (Etgt.conj() * Etgt).sum() # type: ignore return cfdfield_t(Etgt) From fb3bef23bfbccd3c6a103163e8a5309633cbad63 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 10 Dec 2025 21:14:34 -0800 Subject: [PATCH 384/437] [examples/fdfd] split fdfd example into two files --- examples/fdfd0.py | 103 ++++++++++++++++++++++ examples/{fdfd.py => fdfd1.py} | 153 +++++++-------------------------- 2 files changed, 135 insertions(+), 121 deletions(-) create mode 100644 examples/fdfd0.py rename examples/{fdfd.py => fdfd1.py} (54%) diff --git a/examples/fdfd0.py b/examples/fdfd0.py new file mode 100644 index 0000000..a6227c5 --- /dev/null +++ b/examples/fdfd0.py @@ -0,0 +1,103 @@ +import numpy +from numpy.linalg import norm +from matplotlib import pyplot, colors +import logging + +import meanas +from meanas import fdtd +from meanas.fdmath import vec, unvec +from meanas.fdfd import waveguide_3d, functional, scpml, operators +from meanas.fdfd.solvers import generic as generic_solver + +import gridlock + + +logging.basicConfig(level=logging.DEBUG) +logging.getLogger('matplotlib').setLevel(logging.WARNING) + +__author__ = 'Jan Petykiewicz' + + +def pcolor(ax, v) -> None: + mappable = ax.pcolor(v, cmap='seismic', norm=colors.CenteredNorm()) + ax.axis('equal') + ax.get_figure().colorbar(mappable) + + +def test0(solver=generic_solver): + dx = 50 # discretization (nm/cell) + pml_thickness = 10 # (number of cells) + + wl = 1550 # Excitation wavelength + omega = 2 * numpy.pi / wl + + # Device design parameters + radii = (1, 0.6) + th = 220 + center = [0, 0, 0] + + # refractive indices + n_ring = numpy.sqrt(12.6) # ~Si + n_air = 4.0 # air + + # Half-dimensions of the simulation grid + xyz_max = numpy.array([1.2, 1.2, 0.3]) * 1000 + pml_thickness * dx + + # Coordinates of the edges of the cells. + half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max] + edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords] + + # #### Create the grid, mask, and draw the device #### + grid = gridlock.Grid(edge_coords) + epsilon = grid.allocate(n_air**2, dtype=numpy.float32) + grid.draw_cylinder( + epsilon, + h = dict(axis='z', center=center[2], span=th), + radius = max(radii), + center2d = center[:2], + foreground = n_ring ** 2, + num_points = 24, + ) + grid.draw_cylinder( + epsilon, + h = dict(axis='z', center=center[2], span=th * 1.1), + radius = min(radii), + center2d = center[:2], + foreground = n_air ** 2, + num_points = 24, + ) + + dxes = [grid.dxyz, grid.autoshifted_dxyz()] + for a in (0, 1, 2): + for p in (-1, 1): + dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, + thickness=pml_thickness) + + J = [numpy.zeros_like(epsilon[0], dtype=complex) for _ in range(3)] + J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1 + + + # + # Solve! + # + sim_args = dict( + omega = omega, + dxes = dxes, + epsilon = vec(epsilon), + ) + x = solver(J=vec(J), **sim_args) + + A = operators.e_full(omega, dxes, vec(epsilon)).tocsr() + b = -1j * omega * vec(J) + print('Norm of the residual is ', norm(A @ x - b) / norm(b)) + + E = unvec(x, grid.shape) + + # + # Plot results + # + grid.visualize_slice(E.real, plane=dict(z=0), which_shifts=1, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr')) + + +if __name__ == '__main__': + test0() diff --git a/examples/fdfd.py b/examples/fdfd1.py similarity index 54% rename from examples/fdfd.py rename to examples/fdfd1.py index e768ba7..5596639 100644 --- a/examples/fdfd.py +++ b/examples/fdfd1.py @@ -1,6 +1,8 @@ import importlib import numpy from numpy.linalg import norm +from matplotlib import pyplot, colors +import logging import meanas from meanas import fdtd @@ -10,9 +12,6 @@ from meanas.fdfd.solvers import generic as generic_solver import gridlock -from matplotlib import pyplot - -import logging logging.basicConfig(level=logging.DEBUG) logging.getLogger('matplotlib').setLevel(logging.WARNING) @@ -20,86 +19,6 @@ logging.getLogger('matplotlib').setLevel(logging.WARNING) __author__ = 'Jan Petykiewicz' -def test0(solver=generic_solver): - dx = 50 # discretization (nm/cell) - pml_thickness = 10 # (number of cells) - - wl = 1550 # Excitation wavelength - omega = 2 * numpy.pi / wl - - # Device design parameters - radii = (1, 0.6) - th = 220 - center = [0, 0, 0] - - # refractive indices - n_ring = numpy.sqrt(12.6) # ~Si - n_air = 4.0 # air - - # Half-dimensions of the simulation grid - xyz_max = numpy.array([1.2, 1.2, 0.3]) * 1000 + pml_thickness * dx - - # Coordinates of the edges of the cells. - half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max] - edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords] - - # #### Create the grid, mask, and draw the device #### - grid = gridlock.Grid(edge_coords) - epsilon = grid.allocate(n_air**2, dtype=numpy.float32) - grid.draw_cylinder( - epsilon, - surface_normal=2, - center=center, - radius=max(radii), - thickness=th, - foreground=n_ring**2, - num_points=24, - ) - grid.draw_cylinder( - epsilon, - surface_normal=2, - center=center, - radius=min(radii), - thickness=th*1.1, - foreground=n_air ** 2, - num_points=24, - ) - - dxes = [grid.dxyz, grid.autoshifted_dxyz()] - for a in (0, 1, 2): - for p in (-1, 1): - dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, - thickness=pml_thickness) - - J = [numpy.zeros_like(epsilon[0], dtype=complex) for _ in range(3)] - J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1 - - - # - # Solve! - # - sim_args = { - 'omega': omega, - 'dxes': dxes, - 'epsilon': vec(epsilon), - } - x = solver(J=vec(J), **sim_args) - - A = operators.e_full(omega, dxes, vec(epsilon)).tocsr() - b = -1j * omega * vec(J) - print('Norm of the residual is ', norm(A @ x - b)) - - E = unvec(x, grid.shape) - - # - # Plot results - # - pyplot.figure() - pyplot.pcolor(numpy.real(E[1][:, :, grid.shape[2]//2]), cmap='seismic') - pyplot.axis('equal') - pyplot.show() - - def test1(solver=generic_solver): dx = 40 # discretization (nm/cell) pml_thickness = 10 # (number of cells) @@ -126,7 +45,7 @@ def test1(solver=generic_solver): # #### Create the grid and draw the device #### grid = gridlock.Grid(edge_coords) epsilon = grid.allocate(n_air**2, dtype=numpy.float32) - grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], foreground=n_wg**2) + grid.draw_cuboid(epsilon, x=dict(center=0, span=8e3), y=dict(center=0, span=w), z=dict(center=0, span=th), foreground=n_wg**2) dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (0, 1, 2): @@ -160,17 +79,9 @@ def test1(solver=generic_solver): # grid.draw_cuboid(pmcg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) # grid.visualize_isosurface(pmcg) - def pcolor(v) -> None: - vmax = numpy.max(numpy.abs(v)) - pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax) - pyplot.axis('equal') - pyplot.colorbar() - - ss = (1, slice(None), J.shape[2]//2+6, slice(None)) -# pyplot.figure() -# pcolor(J3[ss].T.imag) -# pyplot.figure() -# pcolor((numpy.abs(J3).sum(axis=2).sum(axis=0) > 0).astype(float).T) + grid.visualize_slice(J.imag, plane=dict(y=6*dx), which_shifts=1, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr')) + fig, ax = pyplot.subplots() + ax.pcolormesh((numpy.abs(J).sum(axis=2).sum(axis=0) > 0).astype(float).T, cmap='hot') pyplot.show(block=True) # @@ -196,16 +107,14 @@ def test1(solver=generic_solver): # Plot results # center = grid.pos2ind([0, 0, 0], None).astype(int) - pyplot.figure() - pyplot.subplot(2, 2, 1) - pcolor(numpy.real(E[1][center[0], :, :]).T) - pyplot.subplot(2, 2, 2) - pyplot.plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10)) - pyplot.grid(alpha=0.6) - pyplot.ylabel('log10 of field') - pyplot.subplot(2, 2, 3) - pcolor(numpy.real(E[1][:, :, center[2]]).T) - pyplot.subplot(2, 2, 4) + fig, axes = pyplot.subplots(2, 2) + grid.visualize_slice(E.real, plane=dict(x=0), which_shifts=1, ax=axes[0, 0], finalize=False, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr')) + grid.visualize_slice(E.real, plane=dict(z=0), which_shifts=1, ax=axes[0, 1], finalize=False, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr')) +# pcolor(axes[0, 0], numpy.real(E[1][center[0], :, :]).T) +# pcolor(axes[0, 1], numpy.real(E[1][:, :, center[2]]).T) + axes[1, 0].plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10)) + axes[1, 0].grid(alpha=0.6) + axes[1, 0].set_ylabel('log10 of field') def poyntings(E): H = functional.e2h(omega, dxes)(E) @@ -219,34 +128,35 @@ def test1(solver=generic_solver): return s0, s1, s2 s0x, s1x, s2x = poyntings(E) - pyplot.plot(s0x[0].sum(axis=2).sum(axis=1), label='s0', marker='.') - pyplot.plot(s1x[0].sum(axis=2).sum(axis=1), label='s1', marker='.') - pyplot.plot(s2x[0].sum(axis=2).sum(axis=1), label='s2', marker='.') - pyplot.plot(E[1][:, center[1], center[2]].real.T, label='Ey', marker='x') - pyplot.grid(alpha=0.6) - pyplot.legend() - pyplot.show() + ax = axes[1, 1] + ax.plot(s0x[0].sum(axis=2).sum(axis=1), label='s0', marker='.') + ax.plot(s1x[0].sum(axis=2).sum(axis=1), label='s1', marker='.') + ax.plot(s2x[0].sum(axis=2).sum(axis=1), label='s2', marker='.') + ax.plot(E[1][:, center[1], center[2]].real.T, label='Ey', marker='x') + ax.grid(alpha=0.6) + ax.legend() + + p_in = (-E * J.conj()).sum() / 2 * (dx * dx * dx) + print(f'{p_in=}') q = [] for i in range(-5, 30): e_ovl_rolled = numpy.roll(e_overlap, i, axis=1) - q += [numpy.abs(vec(E) @ vec(e_ovl_rolled).conj())] - pyplot.figure() - pyplot.plot(q, marker='.') - pyplot.grid(alpha=0.6) - pyplot.title('Overlap with mode') - pyplot.show() + q += [numpy.abs(vec(E).conj() @ vec(e_ovl_rolled))] + fig, ax = pyplot.subplots() + ax.plot(q, marker='.') + ax.grid(alpha=0.6) + ax.set_title('Overlap with mode') print('Average overlap with mode:', sum(q[8:32])/len(q[8:32])) + pyplot.show(block=True) + def module_available(name): return importlib.util.find_spec(name) is not None if __name__ == '__main__': - #test0() -# test1() - if module_available('opencl_fdfd'): from opencl_fdfd import cg_solver as opencl_solver test1(opencl_solver) @@ -257,3 +167,4 @@ if __name__ == '__main__': # test1(magma_solver) else: test1() + From c46bed8298daa66be3652d4ff4a2c7e83d92659b Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 10 Dec 2025 21:14:57 -0800 Subject: [PATCH 385/437] update optional deps --- pyproject.toml | 11 +- uv.lock | 806 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 815 insertions(+), 2 deletions(-) create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index 7b95a41..84c2be3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,9 +39,10 @@ include = [ ] dynamic = ["version"] dependencies = [ + "gridlock", "numpy>=2.0", "scipy~=1.14", - ] +] [tool.hatch.version] @@ -49,7 +50,10 @@ path = "meanas/__init__.py" [project.optional-dependencies] dev = ["pytest", "pdoc", "gridlock"] -examples = ["gridlock>=2.0"] +examples = [ + "gridlock>=2.1", + "matplotlib>=3.10.8", +] test = ["pytest"] @@ -95,3 +99,6 @@ module = [ "scipy.sparse.linalg", ] ignore_missing_imports = true + +[tool.uv.sources] +gridlock = { path = "../gridlock", editable = true } diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..efd9651 --- /dev/null +++ b/uv.lock @@ -0,0 +1,806 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "float-raster" +version = "0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/7e/57d91306c966fc5ce8e068650741b599011a14e96ff0b8ec226668094287/float_raster-0.8.tar.gz", hash = "sha256:90e9c00d3908a8e0d50cd97c6df42055ac0fcf900302a504a4becaa024c60c22", size = 29233, upload-time = "2024-07-29T09:10:12.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/69/bcdcf1b52420d4b97ab6a18798a4ebb92292948b555a9a6782cb628821e6/float_raster-0.8-py3-none-any.whl", hash = "sha256:7e4ce9ffaf972e3ee788f16b06ee0eb07488b74634ee6f3db2402bf10ef29be7", size = 42469, upload-time = "2024-07-29T09:10:09.91Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f9/0e84d593c0e12244150280a630999835a64f2852276161b62a0f98318de0/fonttools-4.61.0.tar.gz", hash = "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7", size = 3561884, upload-time = "2025-11-28T17:05:49.491Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/be/5aa89cdddf2863d8afbdc19eb8ec5d8d35d40eeeb8e6cf52c5ff1c2dbd33/fonttools-4.61.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a32a16951cbf113d38f1dd8551b277b6e06e0f6f776fece0f99f746d739e1be3", size = 2847553, upload-time = "2025-11-28T17:04:30.539Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3e/6ff643b07cead1236a534f51291ae2981721cf419135af5b740c002a66dd/fonttools-4.61.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:328a9c227984bebaf69f3ac9062265f8f6acc7ddf2e4e344c63358579af0aa3d", size = 2388298, upload-time = "2025-11-28T17:04:32.161Z" }, + { url = "https://files.pythonhosted.org/packages/c3/15/fca8dfbe7b482e6f240b1aad0ed7c6e2e75e7a28efa3d3a03b570617b5e5/fonttools-4.61.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0bafc8a3b3749c69cc610e5aa3da832d39c2a37a68f03d18ec9a02ecaac04a", size = 5054133, upload-time = "2025-11-28T17:04:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a2/821c61c691b21fd09e07528a9a499cc2b075ac83ddb644aa16c9875a64bc/fonttools-4.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5ca59b7417d149cf24e4c1933c9f44b2957424fc03536f132346d5242e0ebe5", size = 5031410, upload-time = "2025-11-28T17:04:36.141Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/8b16339e93d03c732c8a23edefe3061b17a5f9107ddc47a3215ecd054cac/fonttools-4.61.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:df8cbce85cf482eb01f4551edca978c719f099c623277bda8332e5dbe7dba09d", size = 5030005, upload-time = "2025-11-28T17:04:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/ac/eb/d4e150427bdaa147755239c931bbce829a88149ade5bfd8a327afe565567/fonttools-4.61.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7fb5b84f48a6a733ca3d7f41aa9551908ccabe8669ffe79586560abcc00a9cfd", size = 5154026, upload-time = "2025-11-28T17:04:40.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5f/3dd00ce0dba6759943c707b1830af8c0bcf6f8f1a9fe46cb82e7ac2aaa74/fonttools-4.61.0-cp311-cp311-win32.whl", hash = "sha256:787ef9dfd1ea9fe49573c272412ae5f479d78e671981819538143bec65863865", size = 2276035, upload-time = "2025-11-28T17:04:42.59Z" }, + { url = "https://files.pythonhosted.org/packages/4e/44/798c472f096ddf12955eddb98f4f7c906e7497695d04ce073ddf7161d134/fonttools-4.61.0-cp311-cp311-win_amd64.whl", hash = "sha256:14fafda386377b6131d9e448af42d0926bad47e038de0e5ba1d58c25d621f028", size = 2327290, upload-time = "2025-11-28T17:04:44.57Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/19e5939f773c7cb05480fe2e881d63870b63ee2b4bdb9a77d55b1d36c7b9/fonttools-4.61.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", size = 2846930, upload-time = "2025-11-28T17:04:46.639Z" }, + { url = "https://files.pythonhosted.org/packages/25/b2/0658faf66f705293bd7e739a4f038302d188d424926be9c59bdad945664b/fonttools-4.61.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", size = 2383016, upload-time = "2025-11-28T17:04:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/a3/1fa90b95b690f0d7541f48850adc40e9019374d896c1b8148d15012b2458/fonttools-4.61.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", size = 4949425, upload-time = "2025-11-28T17:04:50.482Z" }, + { url = "https://files.pythonhosted.org/packages/af/00/acf18c00f6c501bd6e05ee930f926186f8a8e268265407065688820f1c94/fonttools-4.61.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6cd0d9051b8ddaf7385f99dd82ec2a058e2b46cf1f1961e68e1ff20fcbb61af", size = 4999632, upload-time = "2025-11-28T17:04:52.508Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e0/19a2b86e54109b1d2ee8743c96a1d297238ae03243897bc5345c0365f34d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e074bc07c31406f45c418e17c1722e83560f181d122c412fa9e815df0ff74810", size = 4939438, upload-time = "2025-11-28T17:04:54.437Z" }, + { url = "https://files.pythonhosted.org/packages/04/35/7b57a5f57d46286360355eff8d6b88c64ab6331107f37a273a71c803798d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a9b78da5d5faa17e63b2404b77feeae105c1b7e75f26020ab7a27b76e02039f", size = 5088960, upload-time = "2025-11-28T17:04:56.348Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/6c5023eb2e0fe5d1ababc7e221e44acd3ff668781489cc1937a6f83d620a/fonttools-4.61.0-cp312-cp312-win32.whl", hash = "sha256:9821ed77bb676736b88fa87a737c97b6af06e8109667e625a4f00158540ce044", size = 2264404, upload-time = "2025-11-28T17:04:58.149Z" }, + { url = "https://files.pythonhosted.org/packages/36/0b/63273128c7c5df19b1e4cd92e0a1e6ea5bb74a400c4905054c96ad60a675/fonttools-4.61.0-cp312-cp312-win_amd64.whl", hash = "sha256:0011d640afa61053bc6590f9a3394bd222de7cfde19346588beabac374e9d8ac", size = 2314427, upload-time = "2025-11-28T17:04:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/334f0d7f181e5473cfb757e1b60f4e60e7fc64f28d406e5d364a952718c0/fonttools-4.61.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba774b8cbd8754f54b8eb58124e8bd45f736b2743325ab1a5229698942b9b433", size = 2841801, upload-time = "2025-11-28T17:05:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/cc/63/97b9c78e1f79bc741d4efe6e51f13872d8edb2b36e1b9fb2bab0d4491bb7/fonttools-4.61.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c84b430616ed73ce46e9cafd0bf0800e366a3e02fb7e1ad7c1e214dbe3862b1f", size = 2379024, upload-time = "2025-11-28T17:05:03.668Z" }, + { url = "https://files.pythonhosted.org/packages/4e/80/c87bc524a90dbeb2a390eea23eae448286983da59b7e02c67fa0ca96a8c5/fonttools-4.61.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2b734d8391afe3c682320840c8191de9bd24e7eb85768dd4dc06ed1b63dbb1b", size = 4923706, upload-time = "2025-11-28T17:05:05.494Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f6/a3b0374811a1de8c3f9207ec88f61ad1bb96f938ed89babae26c065c2e46/fonttools-4.61.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5c5fff72bf31b0e558ed085e4fd7ed96eb85881404ecc39ed2a779e7cf724eb", size = 4979751, upload-time = "2025-11-28T17:05:07.665Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3b/30f63b4308b449091573285f9d27619563a84f399946bca3eadc9554afbe/fonttools-4.61.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14a290c5c93fcab76b7f451e6a4b7721b712d90b3b5ed6908f1abcf794e90d6d", size = 4921113, upload-time = "2025-11-28T17:05:09.551Z" }, + { url = "https://files.pythonhosted.org/packages/41/6c/58e6e9b7d9d8bf2d7010bd7bb493060b39b02a12d1cda64a8bfb116ce760/fonttools-4.61.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:13e3e20a5463bfeb77b3557d04b30bd6a96a6bb5c15c7b2e7908903e69d437a0", size = 5063183, upload-time = "2025-11-28T17:05:11.677Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e3/52c790ab2b07492df059947a1fd7778e105aac5848c0473029a4d20481a2/fonttools-4.61.0-cp313-cp313-win32.whl", hash = "sha256:6781e7a4bb010be1cd69a29927b0305c86b843395f2613bdabe115f7d6ea7f34", size = 2263159, upload-time = "2025-11-28T17:05:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1f/116013b200fbeba871046554d5d2a45fefa69a05c40e9cdfd0d4fff53edc/fonttools-4.61.0-cp313-cp313-win_amd64.whl", hash = "sha256:c53b47834ae41e8e4829171cc44fec0fdf125545a15f6da41776b926b9645a9a", size = 2313530, upload-time = "2025-11-28T17:05:14.848Z" }, + { url = "https://files.pythonhosted.org/packages/d3/99/59b1e25987787cb714aa9457cee4c9301b7c2153f0b673e2b8679d37669d/fonttools-4.61.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:96dfc9bc1f2302224e48e6ee37e656eddbab810b724b52e9d9c13a57a6abad01", size = 2841429, upload-time = "2025-11-28T17:05:16.671Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/4c1911d4332c8a144bb3b44416e274ccca0e297157c971ea1b3fbb855590/fonttools-4.61.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3b2065d94e5d63aafc2591c8b6ccbdb511001d9619f1bca8ad39b745ebeb5efa", size = 2378987, upload-time = "2025-11-28T17:05:18.69Z" }, + { url = "https://files.pythonhosted.org/packages/24/b0/f442e90fde5d2af2ae0cb54008ab6411edc557ee33b824e13e1d04925ac9/fonttools-4.61.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e0d87e81e4d869549585ba0beb3f033718501c1095004f5e6aef598d13ebc216", size = 4873270, upload-time = "2025-11-28T17:05:20.625Z" }, + { url = "https://files.pythonhosted.org/packages/bb/04/f5d5990e33053c8a59b90b1d7e10ad9b97a73f42c745304da0e709635fab/fonttools-4.61.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cfa2eb9bae650e58f0e8ad53c49d19a844d6034d6b259f30f197238abc1ccee", size = 4968270, upload-time = "2025-11-28T17:05:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/94/9f/2091402e0d27c9c8c4bab5de0e5cd146d9609a2d7d1c666bbb75c0011c1a/fonttools-4.61.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4238120002e68296d55e091411c09eab94e111c8ce64716d17df53fd0eb3bb3d", size = 4919799, upload-time = "2025-11-28T17:05:24.437Z" }, + { url = "https://files.pythonhosted.org/packages/a8/72/86adab22fde710b829f8ffbc8f264df01928e5b7a8f6177fa29979ebf256/fonttools-4.61.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6ceac262cc62bec01b3bb59abccf41b24ef6580869e306a4e88b7e56bb4bdda", size = 5030966, upload-time = "2025-11-28T17:05:26.115Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a7/7c8e31b003349e845b853f5e0a67b95ff6b052fa4f5224f8b72624f5ac69/fonttools-4.61.0-cp314-cp314-win32.whl", hash = "sha256:adbb4ecee1a779469a77377bbe490565effe8fce6fb2e6f95f064de58f8bac85", size = 2267243, upload-time = "2025-11-28T17:05:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/20/ee/f434fe7749360497c52b7dcbcfdbccdaab0a71c59f19d572576066717122/fonttools-4.61.0-cp314-cp314-win_amd64.whl", hash = "sha256:02bdf8e04d1a70476564b8640380f04bb4ac74edc1fc71f1bacb840b3e398ee9", size = 2318822, upload-time = "2025-11-28T17:05:29.882Z" }, + { url = "https://files.pythonhosted.org/packages/33/b3/c16255320255e5c1863ca2b2599bb61a46e2f566db0bbb9948615a8fe692/fonttools-4.61.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:627216062d90ab0d98215176d8b9562c4dd5b61271d35f130bcd30f6a8aaa33a", size = 2924917, upload-time = "2025-11-28T17:05:31.46Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b8/08067ae21de705a817777c02ef36ab0b953cbe91d8adf134f9c2da75ed6d/fonttools-4.61.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b446623c9cd5f14a59493818eaa80255eec2468c27d2c01b56e05357c263195", size = 2413576, upload-time = "2025-11-28T17:05:33.343Z" }, + { url = "https://files.pythonhosted.org/packages/42/f1/96ff43f92addce2356780fdc203f2966206f3d22ea20e242c27826fd7442/fonttools-4.61.0-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:70e2a0c0182ee75e493ef33061bfebf140ea57e035481d2f95aa03b66c7a0e05", size = 4877447, upload-time = "2025-11-28T17:05:35.278Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1e/a3d8e51ed9ccfd7385e239ae374b78d258a0fb82d82cab99160a014a45d1/fonttools-4.61.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9064b0f55b947e929ac669af5311ab1f26f750214db6dd9a0c97e091e918f486", size = 5095681, upload-time = "2025-11-28T17:05:37.142Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f6/d256bd6c1065c146a0bdddf1c62f542e08ae5b3405dbf3fcc52be272f674/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cb5e45a824ce14b90510024d0d39dae51bd4fbb54c42a9334ea8c8cf4d95cbe", size = 4974140, upload-time = "2025-11-28T17:05:39.5Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0c/96633eb4b26f138cc48561c6e0c44b4ea48acea56b20b507d6b14f8e80ce/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e5ca8c62efdec7972dfdfd454415c4db49b89aeaefaaacada432f3b7eea9866", size = 5001741, upload-time = "2025-11-28T17:05:41.424Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/3b536bad3be4f26186f296e749ff17bad3e6d57232c104d752d24b2e265b/fonttools-4.61.0-cp314-cp314t-win32.whl", hash = "sha256:63c7125d31abe3e61d7bb917329b5543c5b3448db95f24081a13aaf064360fc8", size = 2330707, upload-time = "2025-11-28T17:05:43.548Z" }, + { url = "https://files.pythonhosted.org/packages/18/ea/e6b9ac610451ee9f04477c311ad126de971f6112cb579fa391d2a8edb00b/fonttools-4.61.0-cp314-cp314t-win_amd64.whl", hash = "sha256:67d841aa272be5500de7f447c40d1d8452783af33b4c3599899319f6ef9ad3c1", size = 2395950, upload-time = "2025-11-28T17:05:45.638Z" }, + { url = "https://files.pythonhosted.org/packages/0c/14/634f7daea5ffe6a5f7a0322ba8e1a0e23c9257b80aa91458107896d1dfc7/fonttools-4.61.0-py3-none-any.whl", hash = "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", size = 1144485, upload-time = "2025-11-28T17:05:47.573Z" }, +] + +[[package]] +name = "gridlock" +source = { editable = "../gridlock" } +dependencies = [ + { name = "float-raster" }, + { name = "numpy" }, +] + +[package.metadata] +requires-dist = [ + { name = "float-raster", specifier = ">=0.8" }, + { name = "matplotlib", marker = "extra == 'visualization'" }, + { name = "matplotlib", marker = "extra == 'visualization-isosurface'" }, + { name = "mpl-toolkits", marker = "extra == 'visualization-isosurface'" }, + { name = "numpy", specifier = ">=1.26" }, + { name = "skimage", marker = "extra == 'visualization-isosurface'", specifier = ">=0.13" }, +] +provides-extras = ["visualization", "visualization-isosurface"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "markdown2" +version = "2.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/f8/b2ae8bf5f28f9b510ae097415e6e4cb63226bb28d7ee01aec03a755ba03b/markdown2-2.5.4.tar.gz", hash = "sha256:a09873f0b3c23dbfae589b0080587df52ad75bb09a5fa6559147554736676889", size = 145652, upload-time = "2025-07-27T16:16:24.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/06/2697b5043c3ecb720ce0d243fc7cf5024c0b5b1e450506e9b21939019963/markdown2-2.5.4-py3-none-any.whl", hash = "sha256:3c4b2934e677be7fec0e6f2de4410e116681f4ad50ec8e5ba7557be506d3f439", size = 49954, upload-time = "2025-07-27T16:16:23.026Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "meanas" +source = { editable = "." } +dependencies = [ + { name = "gridlock" }, + { name = "numpy" }, + { name = "scipy" }, +] + +[package.optional-dependencies] +dev = [ + { name = "gridlock" }, + { name = "pdoc" }, + { name = "pytest" }, +] +examples = [ + { name = "gridlock" }, + { name = "matplotlib" }, +] +test = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "gridlock", editable = "../gridlock" }, + { name = "gridlock", marker = "extra == 'dev'", editable = "../gridlock" }, + { name = "gridlock", marker = "extra == 'examples'", editable = "../gridlock" }, + { name = "matplotlib", marker = "extra == 'examples'", specifier = ">=3.10.8" }, + { name = "numpy", specifier = ">=2.0" }, + { name = "pdoc", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "scipy", specifier = "~=1.14" }, +] +provides-extras = ["dev", "examples", "test"] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pdoc" +version = "16.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown2" }, + { name = "markupsafe" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/fe/ab3f34a5fb08c6b698439a2c2643caf8fef0d61a86dd3fdcd5501c670ab8/pdoc-16.0.0.tar.gz", hash = "sha256:fdadc40cc717ec53919e3cd720390d4e3bcd40405cb51c4918c119447f913514", size = 111890, upload-time = "2025-10-27T16:02:16.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/a1/56a17b7f9e18c2bb8df73f3833345d97083b344708b97bab148fdd7e0b82/pdoc-16.0.0-py3-none-any.whl", hash = "sha256:070b51de2743b9b1a4e0ab193a06c9e6c12cf4151cf9137656eebb16e8556628", size = 100014, upload-time = "2025-10-27T16:02:15.007Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, + { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] From be647658d345753f82a29549fa3699215a8b116b Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 10 Dec 2025 23:07:28 -0800 Subject: [PATCH 386/437] [eigensolvers] Increase number of lanczos vectors (ncv) based on number of requested eigenvalues --- meanas/eigensolvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/eigensolvers.py b/meanas/eigensolvers.py index 21e2ec0..98e7a15 100644 --- a/meanas/eigensolvers.py +++ b/meanas/eigensolvers.py @@ -135,7 +135,7 @@ def signed_eigensolve( shifted_operator = operator + spalg.LinearOperator(shape=operator.shape, matvec=lambda v: shift * v) - shifted_eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=50) + shifted_eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=2 * how_many + 50) eigenvalues = shifted_eigenvalues - shift k = eigenvalues.argsort() From 7e8ff233561d1233f42800a0b0056919d20b6b4f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 19:23:09 -0700 Subject: [PATCH 387/437] misc example updates --- examples/fdfd1.py | 5 +- examples/fdtd.py | 170 +++++++++++++++++++++++++++------------------- 2 files changed, 103 insertions(+), 72 deletions(-) diff --git a/examples/fdfd1.py b/examples/fdfd1.py index 5596639..f983d37 100644 --- a/examples/fdfd1.py +++ b/examples/fdfd1.py @@ -1,4 +1,5 @@ import importlib +import logging import numpy from numpy.linalg import norm from matplotlib import pyplot, colors @@ -6,12 +7,14 @@ import logging import meanas from meanas import fdtd -from meanas.fdmath import vec, unvec +from meanas.fdmath import vec, unvec, fdfield_t from meanas.fdfd import waveguide_3d, functional, scpml, operators from meanas.fdfd.solvers import generic as generic_solver import gridlock +from matplotlib import pyplot + logging.basicConfig(level=logging.DEBUG) logging.getLogger('matplotlib').setLevel(logging.WARNING) diff --git a/examples/fdtd.py b/examples/fdtd.py index 8378b34..284ce07 100644 --- a/examples/fdtd.py +++ b/examples/fdtd.py @@ -1,18 +1,25 @@ """ -Example code for running an OpenCL FDTD simulation +Example code for running an FDTD simulation See main() for simulation setup. """ import sys import time +import copy import numpy import h5py +from numpy.linalg import norm from meanas import fdtd from meanas.fdtd import cpml_params, updates_with_cpml -from masque import Pattern, shapes +from meanas.fdtd.misc import gaussian_packet + +from meanas.fdfd.operators import e_full +from meanas.fdfd.scpml import stretch_with_scpml +from meanas.fdmath import vec +from masque import Pattern, Circle, Polygon import gridlock import pcgen @@ -41,50 +48,51 @@ def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern: `masque.Pattern` object containing the L3 design """ - default_args = {'hole_dose': 1, - 'trench_dose': 1, - 'hole_layer': 0, - 'trench_layer': 1, - 'shifts_a': (0.15, 0, 0.075), - 'shifts_r': (1.0, 1.0, 1.0), - 'xy_size': (10, 10), - 'perturbed_radius': 1.1, - 'trench_width': 1.2e3, - } + default_args = { + 'hole_layer': 0, + 'trench_layer': 1, + 'shifts_a': (0.15, 0, 0.075), + 'shifts_r': (1.0, 1.0, 1.0), + 'xy_size': (10, 10), + 'perturbed_radius': 1.1, + 'trench_width': 1.2e3, + } kwargs = {**default_args, **kwargs} - xyr = pcgen.l3_shift_perturbed_defect(mirror_dims=kwargs['xy_size'], - perturbed_radius=kwargs['perturbed_radius'], - shifts_a=kwargs['shifts_a'], - shifts_r=kwargs['shifts_r']) + xyr = pcgen.l3_shift_perturbed_defect( + mirror_dims=kwargs['xy_size'], + perturbed_radius=kwargs['perturbed_radius'], + shifts_a=kwargs['shifts_a'], + shifts_r=kwargs['shifts_r'], + ) xyr *= a xyr[:, 2] *= radius pat = Pattern() - pat.name = f'L3p-a{a:g}r{radius:g}rp{kwargs["perturbed_radius"]:g}' - pat.shapes += [shapes.Circle(radius=r, offset=(x, y), - dose=kwargs['hole_dose'], - layer=kwargs['hole_layer']) - for x, y, r in xyr] + #pat.name = f'L3p-a{a:g}r{radius:g}rp{kwargs["perturbed_radius"]:g}' + pat.shapes[(kwargs['hole_layer'], 0)] += [ + Circle(radius=r, offset=(x, y)) + for x, y, r in xyr] maxes = numpy.max(numpy.fabs(xyr), axis=0) - pat.shapes += [shapes.Polygon.rectangle( - lx=(2 * maxes[0]), ly=kwargs['trench_width'], - offset=(0, s * (maxes[1] + a + kwargs['trench_width'] / 2)), - dose=kwargs['trench_dose'], layer=kwargs['trench_layer']) - for s in (-1, 1)] + pat.shapes[(kwargs['trench_layer'], 0)] += [ + Polygon.rectangle( + lx=(2 * maxes[0]), ly=kwargs['trench_width'], + offset=(0, s * (maxes[1] + a + kwargs['trench_width'] / 2)) + ) + for s in (-1, 1)] return pat def main(): dtype = numpy.float32 - max_t = 8000 # number of timesteps + max_t = 3600 # number of timesteps dx = 40 # discretization (nm/cell) pml_thickness = 8 # (number of cells) wl = 1550 # Excitation wavelength and fwhm - dwl = 200 + dwl = 100 # Device design parameters xy_size = numpy.array([10, 10]) @@ -107,69 +115,89 @@ def main(): # #### Create the grid, mask, and draw the device #### grid = gridlock.Grid(edge_coords) - epsilon = grid.allocate(n_air**2, dtype=dtype) - grid.draw_slab(epsilon, - surface_normal=2, - center=[0, 0, 0], - thickness=th, - eps=n_slab**2) + epsilon = grid.allocate(n_air ** 2, dtype=dtype) + grid.draw_slab( + epsilon, + slab = dict(axis='z', center=0, span=th), + foreground = n_slab ** 2, + ) + mask = perturbed_l3(a, r) + grid.draw_polygons( + epsilon, + slab = dict(axis='z', center=0, span=2 * th), + foreground = n_air ** 2, + offset2d = (0, 0), + polygons = mask.as_polygons(library=None), + ) - grid.draw_polygons(epsilon, - surface_normal=2, - center=[0, 0, 0], - thickness=2 * th, - eps=n_air**2, - polygons=mask.as_polygons()) + print(f'{grid.shape=}') - print(grid.shape) - - dt = .99/numpy.sqrt(3) - e = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)] - h = [numpy.zeros_like(epsilon[0], dtype=dtype) for _ in range(3)] + dt = dx * 0.99 / numpy.sqrt(3) + ee = numpy.zeros_like(epsilon, dtype=dtype) + hh = numpy.zeros_like(epsilon, dtype=dtype) dxes = [grid.dxyz, grid.autoshifted_dxyz()] # PMLs in every direction - pml_params = [[cpml_params(axis=dd, polarity=pp, dt=dt, - thickness=pml_thickness, epsilon_eff=1.0**2) - for pp in (-1, +1)] - for dd in range(3)] - update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt, - dxes=dxes, epsilon=epsilon) + pml_params = [ + [cpml_params(axis=dd, polarity=pp, dt=dt, thickness=pml_thickness, epsilon_eff=n_air ** 2) + for pp in (-1, +1)] + for dd in range(3)] + update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt, dxes=dxes, epsilon=epsilon) + + # sample_interval = numpy.floor(1 / (2 * 1 / wl * dt)).astype(int) + # print(f'Save time interval would be {sample_interval} * dt = {sample_interval * dt:3g}') + # Source parameters and function - w = 2 * numpy.pi * dx / wl - fwhm = dwl * w * w / (2 * numpy.pi * dx) - alpha = (fwhm ** 2) / 8 * numpy.log(2) - delay = 7/numpy.sqrt(2 * alpha) + source_phasor, _delay = gaussian_packet(wl=wl, dwl=100, dt=dt, turn_on=1e-5) + aa, cc, ss = source_phasor(numpy.arange(max_t)) + srca_real = aa * cc + src_maxt = numpy.argwhere(numpy.diff(aa < 1e-5))[-1] + assert aa[src_maxt - 1] >= 1e-5 + phasor_norm = dt / (aa * cc * cc).sum() - def field_source(i): - t0 = i * dt - delay - return numpy.sin(w * t0) * numpy.exp(-alpha * t0**2) + Jph = numpy.zeros_like(epsilon, dtype=complex) + Jph[1, *(grid.shape // 2)] = epsilon[1, *(grid.shape // 2)] + Eph = numpy.zeros_like(Jph) # #### Run a bunch of iterations #### output_file = h5py.File('simulation_output.h5', 'w') start = time.perf_counter() - for t in range(max_t): - update_E(e, h, epsilon) + for tt in range(max_t): + update_E(ee, hh, epsilon) - e[1][tuple(grid.shape//2)] += field_source(t) - update_H(e, h) + if tt < src_maxt: + ee[1, *(grid.shape // 2)] -= srca_real[tt] + update_H(ee, hh) - avg_rate = (t + 1)/(time.perf_counter() - start)) - print(f'iteration {t}: average {avg_rate} iterations per sec') + avg_rate = (tt + 1) / (time.perf_counter() - start) sys.stdout.flush() - if t % 20 == 0: - r = sum([(f * f * e).sum() for f, e in zip(e, epsilon)]) - print('E sum', r) + if tt % 200 == 0: + print(f'iteration {tt}: average {avg_rate} iterations per sec') + E_energy_sum = (ee * ee * epsilon).sum() + print(f'{E_energy_sum=}') # Save field slices - if (t % 20 == 0 and (max_t - t <= 1000 or t <= 2000)) or t == max_t-1: - print('saving E-field') - for j, f in enumerate(e): - output_file['/E{}_t{}'.format('xyz'[j], t)] = f[:, :, round(f.shape[2]/2)] + if (tt % 20 == 0 and (max_t - tt <= 1000 or tt <= 2000)) or tt == max_t - 1: + print(f'saving E-field at iteration {tt}') + output_file[f'/E_t{tt}'] = ee[:, :, :, ee.shape[3] // 2] + + Eph += (cc[tt] - 1j * ss[tt]) * phasor_norm * ee + + omega = 2 * numpy.pi / wl + Eph *= numpy.exp(-1j * dt / 2 * omega) + b = -1j * omega * Jph + dxes_fdfd = copy.deepcopy(dxes) + for pp in (-1, +1): + for dd in range(3): + stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_air ** 2, thickness=pml_thickness) + A = e_full(omega=omega, dxes=dxes, epsilon=epsilon) + residual = norm(A @ vec(ee) - vec(b)) / norm(vec(b)) + print(f'FDFD residual is {residual}') + if __name__ == '__main__': main() From f5af0fef55a99a0bc405f881c62d48194ae20bb8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 19:24:41 -0700 Subject: [PATCH 388/437] [waveguide_3d] fixup and doc update --- meanas/fdfd/waveguide_3d.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index 5048dea..61ed38e 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -157,19 +157,31 @@ def compute_source( def compute_overlap_e( - E: cfdfield, + E: cfdfield_t, wavenumber: complex, dxes: dx_lists_t, axis: int, polarity: int, slices: Sequence[slice], + omega: float, ) -> cfdfield_t: """ Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) [assumes reflection symmetry]. - TODO: add reference or derivation for compute_overlap_e + E x H_mode + E_mode x H + -> Ex Hmy - EyHmx + Emx Hy - Emy Hx (Z-prop) + Ex we/B Emx + Ex i/B dy Hmz - Ey (-we/B Emy) - Ey i/B dx Hmz + we/B (Ex Emx + Ey Emy) + i/B (Ex dy Hmz - Ey dx Hmz) + we/B (Ex Emx + Ey Emy) + i/B (Ex dy (dx Emy - dy Emx) - Ey dx (dx Emy - dy Emx)) + we/B (Ex Emx + Ey Emy) + i/B (Ex dy dx Emy - Ex dy dy Emx - Ey dx dx Emy - Ey dx dy Emx) + + Ex j/wu (-jB Emx - dx Emz) - Ey j/wu (dy Emz + jB Emy) + B/wu (Ex Emx + Ey Emy) - j/wu (Ex dx Emz + Ey dy Emz) + + + TODO: add reference Args: E: E-field of the mode @@ -197,9 +209,8 @@ def compute_overlap_e( Etgt = numpy.zeros_like(Ee) Etgt[slices2] = Ee[slices2] - # note no sqrt() when normalizing below since we want to get 1.0 after overlapping with the - # original field, not the normalized one - Etgt /= (Etgt.conj() * Etgt).sum() # type: ignore + # Note: We normalize so that (Etgt @ E.conj()) == 1, so (Etgt @ Etgt.conj) != 1 + Etgt /= (Etgt.conj() * Etgt).sum() return cfdfield_t(Etgt) From 49132118837d12bac3c5d509a9d32d233abb812a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 19:29:33 -0700 Subject: [PATCH 389/437] [fdfd.eme] fix abcd array construction --- meanas/fdfd/eme.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/meanas/fdfd/eme.py b/meanas/fdfd/eme.py index f834973..cb1b99e 100644 --- a/meanas/fdfd/eme.py +++ b/meanas/fdfd/eme.py @@ -55,7 +55,13 @@ def get_abcd( B = r21 @ t21i C = -t21i @ r12 D = t21i - return sparse.block_array(((A, B), (C, D))) + return sparse.block_array( + [ + [sparse.csr_array(A), sparse.csr_array(B)], + [sparse.csr_array(C), sparse.csr_array(D)], + ], + format='csr', + ) def get_s( From 9d419aa3ea50dd70c1dc0e6b75d42075003acb42 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 19:33:40 -0700 Subject: [PATCH 390/437] [fdtd.misc.gaussian_beam] avoid some nans at w0 near 0 --- meanas/fdtd/misc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meanas/fdtd/misc.py b/meanas/fdtd/misc.py index 3fb3371..1d90c32 100644 --- a/meanas/fdtd/misc.py +++ b/meanas/fdtd/misc.py @@ -156,11 +156,11 @@ def gaussian_beam( wz = numpy.sqrt(wz2) # == fwhm(z) / sqrt(2 * ln(2)) kk = 2 * pi / wl - Rz = zz * (1 + zr2 / z2) + inv_Rz = numpy.divide(zz, z2 + zr2, out=numpy.zeros_like(zz), where=(z2 + zr2) != 0) gouy = numpy.arctan(zz / zr) - gaussian = w0 / wz * numpy.exp(-r2 / wz2) * numpy.exp(1j * (kk * zz + kk * r2 / 2 / Rz - gouy)) + gaussian = w0 / wz * numpy.exp(-r2 / wz2) * numpy.exp(1j * (kk * zz + kk * r2 * inv_Rz / 2 - gouy)) row = gaussian[:, :, gaussian.shape[2] // 2] - norm = numpy.sqrt((row * row.conj()).sum()) + norm = numpy.linalg.norm(row) return gaussian / norm From 8d49901b58b3489905aa28a561a84e7468bb1603 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 19:35:03 -0700 Subject: [PATCH 391/437] [fdtd.misc] fix some packets/pulses --- meanas/fdtd/misc.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/meanas/fdtd/misc.py b/meanas/fdtd/misc.py index 1d90c32..89ccb3d 100644 --- a/meanas/fdtd/misc.py +++ b/meanas/fdtd/misc.py @@ -53,8 +53,8 @@ def gaussian_packet( t0 = ii * dt - delay envelope = numpy.sqrt(numpy.sqrt(2 * alpha / pi)) * numpy.exp(-alpha * t0 * t0) - if one_sided and t0 > 0: - envelope = 1 + if one_sided: + envelope = numpy.where(t0 > 0, 1.0, envelope) cc = numpy.cos(omega * t0) ss = numpy.sin(omega * t0) @@ -94,10 +94,14 @@ def ricker_pulse( logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO omega = 2 * pi / wl freq = 1 / wl - # r0 = omega / 2 from scipy.optimize import root_scalar - delay_results = root_scalar(lambda tt: (1 - omega * omega * tt * tt / 2) * numpy.exp(-omega * omega / 4 * tt * tt) - turn_on, x0=0, x1=-2 / omega) + delay_results = root_scalar( + lambda tt: (1 - omega * omega * tt * tt / 2) * numpy.exp(-omega * omega * tt * tt / 4) - turn_on, + x0=0, + x1=-2 / omega, + ) + delay = delay_results.root delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase From 74bebea837883ad01ce07b63b1c7d460524261de Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 19:37:35 -0700 Subject: [PATCH 392/437] [fdfd.farfield] fix kys calculation and some near-0 behavior --- meanas/fdfd/farfield.py | 65 ++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index 86ec0d7..0051cd0 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -78,15 +78,12 @@ def near_to_farfield( kx, ky = numpy.meshgrid(kxx, kyy, indexing='ij') kxy2 = kx * kx + ky * ky kxy = numpy.sqrt(kxy2) - kz = numpy.sqrt(k * k - kxy2) + kz = numpy.sqrt(numpy.maximum(0, k * k - kxy2)) - sin_th = ky / kxy - cos_th = kx / kxy + sin_th = numpy.divide(ky, kxy, out=numpy.zeros_like(ky), where=kxy != 0) + cos_th = numpy.divide(kx, kxy, out=numpy.ones_like(kx), where=kxy != 0) cos_phi = kz / k - sin_th[numpy.logical_and(kx == 0, ky == 0)] = 0 - cos_th[numpy.logical_and(kx == 0, ky == 0)] = 1 - # Normalized vector potentials N, L N = [-Hn_fft[1] * cos_phi * cos_th + Hn_fft[0] * cos_phi * sin_th, Hn_fft[1] * sin_th + Hn_fft[0] * cos_th] # noqa: E127 @@ -114,8 +111,8 @@ def near_to_farfield( outputs = { 'E': E_far, 'H': H_far, - 'dkx': kx[1] - kx[0], - 'dky': ky[1] - ky[0], + 'dkx': float(kxx[1] - kxx[0]), + 'dky': float(kyy[1] - kyy[0]), 'kx': kx, 'ky': ky, 'theta': theta, @@ -177,22 +174,19 @@ def far_to_nearfield( padded_shape = cast('Sequence[int]', padded_size) k = 2 * pi - kxs = fftshift(fftfreq(s[0], 1 / (s[0] * dkx))) - kys = fftshift(fftfreq(s[0], 1 / (s[1] * dky))) + kxs = dkx * fftshift(fftfreq(s[0], d=1 / s[0])) + kys = dky * fftshift(fftfreq(s[1], d=1 / s[1])) kx, ky = numpy.meshgrid(kxs, kys, indexing='ij') kxy2 = kx * kx + ky * ky kxy = numpy.sqrt(kxy2) - kz = numpy.sqrt(k * k - kxy2) + kz = numpy.sqrt(numpy.maximum(0, k * k - kxy2)) - sin_th = ky / kxy - cos_th = kx / kxy + sin_th = numpy.divide(ky, kxy, out=numpy.zeros_like(ky), where=kxy != 0) + cos_th = numpy.divide(kx, kxy, out=numpy.ones_like(kx), where=kxy != 0) cos_phi = kz / k - sin_th[numpy.logical_and(kx == 0, ky == 0)] = 0 - cos_th[numpy.logical_and(kx == 0, ky == 0)] = 1 - theta = numpy.arctan2(ky, kx) phi = numpy.arccos(cos_phi) theta[numpy.logical_and(kx == 0, ky == 0)] = 0 @@ -212,21 +206,41 @@ def far_to_nearfield( N = [L[1], -L[0]] # noqa: E128 - En_fft = [-( L[0] * sin_th + L[1] * cos_phi * cos_th) / cos_phi, - -(-L[0] * cos_th + L[1] * cos_phi * sin_th) / cos_phi] + En_fft = [ + numpy.divide( + -(L[0] * sin_th + L[1] * cos_phi * cos_th), + cos_phi, + out=numpy.zeros_like(L[0]), + where=cos_phi != 0, + ), + numpy.divide( + -(-L[0] * cos_th + L[1] * cos_phi * sin_th), + cos_phi, + out=numpy.zeros_like(L[0]), + where=cos_phi != 0, + ), + ] - Hn_fft = [( N[0] * sin_th + N[1] * cos_phi * cos_th) / cos_phi, - (-N[0] * cos_th + N[1] * cos_phi * sin_th) / cos_phi] - - for i in range(2): - En_fft[i][cos_phi == 0] = 0 - Hn_fft[i][cos_phi == 0] = 0 + Hn_fft = [ + numpy.divide( + N[0] * sin_th + N[1] * cos_phi * cos_th, + cos_phi, + out=numpy.zeros_like(N[0]), + where=cos_phi != 0, + ), + numpy.divide( + -N[0] * cos_th + N[1] * cos_phi * sin_th, + cos_phi, + out=numpy.zeros_like(N[0]), + where=cos_phi != 0, + ), + ] E_near = [ifftshift(ifft2(ifftshift(Ei), s=padded_shape)) for Ei in En_fft] H_near = [ifftshift(ifft2(ifftshift(Hi), s=padded_shape)) for Hi in Hn_fft] dx = 2 * pi / (s[0] * dkx) - dy = 2 * pi / (s[0] * dky) + dy = 2 * pi / (s[1] * dky) outputs = { 'E': E_near, @@ -236,4 +250,3 @@ def far_to_nearfield( } return outputs - From 7eea919f94b9f499ed4d76fa9f9b504c31c8a120 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 19:38:31 -0700 Subject: [PATCH 393/437] [fdtd.boundaries] use tuples for indexing --- meanas/fdtd/boundaries.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py index 131d741..aa0bff5 100644 --- a/meanas/fdtd/boundaries.py +++ b/meanas/fdtd/boundaries.py @@ -28,17 +28,19 @@ def conducting_boundary( shifted1_slice = [slice(None)] * 3 boundary_slice[direction] = 0 shifted1_slice[direction] = 1 + boundary = tuple(boundary_slice) + shifted1 = tuple(shifted1_slice) def en(e: fdfield_t) -> fdfield_t: - e[direction][boundary_slice] = 0 - e[u][boundary_slice] = e[u][shifted1_slice] - e[v][boundary_slice] = e[v][shifted1_slice] + e[direction][boundary] = 0 + e[u][boundary] = e[u][shifted1] + e[v][boundary] = e[v][shifted1] return e def hn(h: fdfield_t) -> fdfield_t: - h[direction][boundary_slice] = h[direction][shifted1_slice] - h[u][boundary_slice] = 0 - h[v][boundary_slice] = 0 + h[direction][boundary] = h[direction][shifted1] + h[u][boundary] = 0 + h[v][boundary] = 0 return h return en, hn @@ -50,20 +52,23 @@ def conducting_boundary( boundary_slice[direction] = -1 shifted1_slice[direction] = -2 shifted2_slice[direction] = -3 + boundary = tuple(boundary_slice) + shifted1 = tuple(shifted1_slice) + shifted2 = tuple(shifted2_slice) def ep(e: fdfield_t) -> fdfield_t: - e[direction][boundary_slice] = -e[direction][shifted2_slice] - e[direction][shifted1_slice] = 0 - e[u][boundary_slice] = e[u][shifted1_slice] - e[v][boundary_slice] = e[v][shifted1_slice] + e[direction][boundary] = -e[direction][shifted2] + e[direction][shifted1] = 0 + e[u][boundary] = e[u][shifted1] + e[v][boundary] = e[v][shifted1] return e def hp(h: fdfield_t) -> fdfield_t: - h[direction][boundary_slice] = h[direction][shifted1_slice] - h[u][boundary_slice] = -h[u][shifted2_slice] - h[u][shifted1_slice] = 0 - h[v][boundary_slice] = -h[v][shifted2_slice] - h[v][shifted1_slice] = 0 + h[direction][boundary] = h[direction][shifted1] + h[u][boundary] = -h[u][shifted2] + h[u][shifted1] = 0 + h[v][boundary] = -h[v][shifted2] + h[v][shifted1] = 0 return h return ep, hp From bc55baf4a6c246db410b54efd9574d1fa7be42ac Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 19:38:55 -0700 Subject: [PATCH 394/437] [tests] add coverage and test options --- pyproject.toml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 84c2be3..97df3f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,12 +49,12 @@ dependencies = [ path = "meanas/__init__.py" [project.optional-dependencies] -dev = ["pytest", "pdoc", "gridlock"] +dev = ["pytest", "coverage", "pdoc", "gridlock"] examples = [ "gridlock>=2.1", "matplotlib>=3.10.8", ] -test = ["pytest"] +test = ["pytest", "coverage"] [tool.ruff] @@ -100,5 +100,16 @@ module = [ ] ignore_missing_imports = true -[tool.uv.sources] -gridlock = { path = "../gridlock", editable = true } +#[tool.uv.sources] +#gridlock = { path = "../gridlock", editable = true } + +[tool.pytest.ini_options] +addopts = "-rsXx" +testpaths = ["meanas"] + +[tool.coverage.run] +source = ["meanas"] + +[tool.coverage.report] +show_missing = true +omit = ["meanas/test/*"] From 38a5c1a9aa89801017e519dba2ca24cfe47e98c6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 20:16:16 -0700 Subject: [PATCH 395/437] [tests] add some more tests around numerical self-consistency --- meanas/test/test_eigensolvers_numerics.py | 47 +++++++++ meanas/test/test_waveguide_2d_numerics.py | 108 +++++++++++++++++++++ meanas/test/test_waveguide_mode_helpers.py | 103 ++++++++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 meanas/test/test_eigensolvers_numerics.py create mode 100644 meanas/test/test_waveguide_2d_numerics.py create mode 100644 meanas/test/test_waveguide_mode_helpers.py diff --git a/meanas/test/test_eigensolvers_numerics.py b/meanas/test/test_eigensolvers_numerics.py new file mode 100644 index 0000000..8d0c5c9 --- /dev/null +++ b/meanas/test/test_eigensolvers_numerics.py @@ -0,0 +1,47 @@ +import numpy +from numpy.linalg import norm +from scipy import sparse +import scipy.sparse.linalg as spalg + +from ..eigensolvers import rayleigh_quotient_iteration, signed_eigensolve + + +def test_rayleigh_quotient_iteration_with_linear_operator() -> None: + operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() + linear_operator = spalg.LinearOperator( + shape=operator.shape, + dtype=complex, + matvec=lambda vv: operator @ vv, + ) + + def dense_solver( + shifted_operator: spalg.LinearOperator, + rhs: numpy.ndarray, + ) -> numpy.ndarray: + basis = numpy.eye(operator.shape[0], dtype=complex) + columns = [shifted_operator.matvec(basis[:, ii]) for ii in range(operator.shape[0])] + dense_matrix = numpy.column_stack(columns) + return numpy.linalg.lstsq(dense_matrix, rhs, rcond=None)[0] + + guess = numpy.array([0.0, 1.0, 1e-6, 0.0], dtype=complex) + eigval, eigvec = rayleigh_quotient_iteration( + linear_operator, + guess, + iterations=8, + solver=dense_solver, + ) + + residual = norm(operator @ eigvec - eigval * eigvec) + assert abs(eigval - 3.0) < 1e-12 + assert residual < 1e-12 + + +def test_signed_eigensolve_negative_returns_largest_negative_mode() -> None: + operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() + + eigvals, eigvecs = signed_eigensolve(operator, how_many=1, negative=True) + + assert eigvals.shape == (1,) + assert eigvecs.shape == (4, 1) + assert abs(eigvals[0] + 2.0) < 1e-12 + assert abs(eigvecs[3, 0]) > 0.99 diff --git a/meanas/test/test_waveguide_2d_numerics.py b/meanas/test/test_waveguide_2d_numerics.py new file mode 100644 index 0000000..5667edc --- /dev/null +++ b/meanas/test/test_waveguide_2d_numerics.py @@ -0,0 +1,108 @@ +import numpy +from numpy.linalg import norm + +from ..fdmath import vec +from ..fdfd import waveguide_2d + + +OMEGA = 1 / 1500 +GRID_SHAPE = (5, 5) +DXES_2D = [[numpy.ones(GRID_SHAPE[0]), numpy.ones(GRID_SHAPE[1])] for _ in range(2)] + + +def build_asymmetric_epsilon() -> numpy.ndarray: + epsilon = numpy.ones((3, *GRID_SHAPE), dtype=float) + epsilon[:, 2, 1] = 2.0 + return vec(epsilon) + + +def test_waveguide_2d_solved_modes_are_ordered_and_low_residual() -> None: + epsilon = build_asymmetric_epsilon() + operator_e = waveguide_2d.operator_e(OMEGA, DXES_2D, epsilon) + + e_xys, wavenumbers = waveguide_2d.solve_modes( + [0, 1], + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + ) + + assert numpy.all(numpy.diff(numpy.real(wavenumbers)) <= 0) + + for e_xy, wavenumber in zip(e_xys, wavenumbers, strict=True): + residual = norm(operator_e @ e_xy - (wavenumber ** 2) * e_xy) / norm(e_xy) + assert residual < 1e-6 + + +def test_waveguide_2d_normalized_fields_are_consistent() -> None: + epsilon = build_asymmetric_epsilon() + operator_h = waveguide_2d.operator_h(OMEGA, DXES_2D, epsilon) + + e_xy, wavenumber = waveguide_2d.solve_mode( + 0, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + ) + e_field, h_field = waveguide_2d.normalized_fields_e( + e_xy, + wavenumber=wavenumber, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + ) + h_xy = numpy.concatenate(numpy.split(h_field, 3)[:2]) + + overlap = waveguide_2d.inner_product(e_field, h_field, DXES_2D, conj_h=True) + h_residual = norm(operator_h @ h_xy - (wavenumber ** 2) * h_xy) / norm(h_xy) + + assert abs(overlap.real - 1.0) < 1e-10 + assert abs(overlap.imag) < 1e-10 + assert waveguide_2d.e_err(e_field, wavenumber, OMEGA, DXES_2D, epsilon) < 1e-6 + assert waveguide_2d.h_err(h_field, wavenumber, OMEGA, DXES_2D, epsilon) < 1e-6 + assert h_residual < 1e-6 + + +def test_waveguide_2d_sensitivity_matches_finite_difference() -> None: + epsilon = build_asymmetric_epsilon() + e_xy, wavenumber = waveguide_2d.solve_mode( + 0, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + ) + e_field, h_field = waveguide_2d.normalized_fields_e( + e_xy, + wavenumber=wavenumber, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + ) + sensitivity = waveguide_2d.sensitivity( + e_field, + h_field, + wavenumber=wavenumber, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + ) + + target_index = int(numpy.argmax(numpy.abs(sensitivity))) + delta = 1e-4 + epsilon_perturbed = epsilon.copy() + epsilon_perturbed[target_index] += delta + + _, perturbed_wavenumber = waveguide_2d.solve_mode( + 0, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon_perturbed, + ) + finite_difference = (perturbed_wavenumber - wavenumber) / delta + + numpy.testing.assert_allclose( + sensitivity[target_index], + finite_difference, + rtol=0.1, + atol=1e-6, + ) diff --git a/meanas/test/test_waveguide_mode_helpers.py b/meanas/test/test_waveguide_mode_helpers.py new file mode 100644 index 0000000..2bf77f2 --- /dev/null +++ b/meanas/test/test_waveguide_mode_helpers.py @@ -0,0 +1,103 @@ +import numpy +from numpy.linalg import norm + +from ..fdmath import vec +from ..fdfd import waveguide_3d, waveguide_cyl + + +OMEGA = 1 / 1500 + + +def test_waveguide_3d_solve_mode_and_expand_e_are_phase_consistent() -> None: + epsilon = numpy.ones((3, 5, 5, 1), dtype=float) + dxes = [[numpy.ones(5), numpy.ones(5), numpy.ones(1)] for _ in range(2)] + axis = 0 + polarity = 1 + slices = (slice(0, 1), slice(None), slice(None)) + + result = waveguide_3d.solve_mode( + 0, + omega=OMEGA, + dxes=dxes, + axis=axis, + polarity=polarity, + slices=slices, + epsilon=epsilon, + ) + expanded = waveguide_3d.expand_e( + E=result['E'], + wavenumber=result['wavenumber'], + dxes=dxes, + axis=axis, + polarity=polarity, + slices=slices, + ) + + dx_prop = 0.5 * numpy.array([dx[2][slices[2]] for dx in dxes]).sum() + expected_wavenumber = 2 / dx_prop * numpy.arcsin(result['wavenumber_2d'] * dx_prop / 2) + solved_slice = (slice(None), *slices) + + assert result['E'].shape == epsilon.shape + assert result['H'].shape == epsilon.shape + assert numpy.isfinite(result['E']).all() + assert numpy.isfinite(result['H']).all() + assert abs(result['wavenumber'] - expected_wavenumber) < 1e-12 + assert numpy.allclose(expanded[solved_slice], result['E'][solved_slice]) + + component, _x, y_index, z_index = numpy.unravel_index( + numpy.abs(result['E']).argmax(), + result['E'].shape, + ) + values = expanded[component, :, y_index, z_index] + ratios = values[1:] / values[:-1] + expected_ratio = numpy.exp(-1j * result['wavenumber']) + + numpy.testing.assert_allclose(ratios, expected_ratio, rtol=1e-6, atol=1e-9) + + +def test_waveguide_cyl_solved_modes_are_ordered_and_low_residual() -> None: + shape = (5, 5) + dxes = [[numpy.ones(shape[0]), numpy.ones(shape[1])] for _ in range(2)] + epsilon = vec(numpy.ones((3, *shape), dtype=float)) + rmin = 10.0 + + e_xys, angular_wavenumbers = waveguide_cyl.solve_modes( + [0, 1], + omega=OMEGA, + dxes=dxes, + epsilon=epsilon, + rmin=rmin, + ) + operator = waveguide_cyl.cylindrical_operator(OMEGA, dxes, epsilon, rmin=rmin) + + assert numpy.all(numpy.diff(numpy.real(angular_wavenumbers)) <= 0) + + for e_xy, angular_wavenumber in zip(e_xys, angular_wavenumbers, strict=True): + eigenvalue = (angular_wavenumber / rmin) ** 2 + residual = norm(operator @ e_xy - eigenvalue * e_xy) / norm(e_xy) + assert residual < 1e-6 + + +def test_waveguide_cyl_linear_wavenumbers_are_finite_and_ordered() -> None: + shape = (5, 5) + dxes = [[numpy.ones(shape[0]), numpy.ones(shape[1])] for _ in range(2)] + epsilon = vec(numpy.ones((3, *shape), dtype=float)) + + e_xys, angular_wavenumbers = waveguide_cyl.solve_modes( + [0, 1], + omega=OMEGA, + dxes=dxes, + epsilon=epsilon, + rmin=10.0, + ) + linear_wavenumbers = waveguide_cyl.linear_wavenumbers( + e_xys, + angular_wavenumbers, + epsilon=epsilon, + dxes=dxes, + rmin=10.0, + ) + + assert numpy.isfinite(linear_wavenumbers).all() + assert numpy.all(numpy.real(linear_wavenumbers) > 0) + assert numpy.all(numpy.diff(numpy.real(linear_wavenumbers)) <= 0) From 593098bf8fc92b9ee8920a2783c08393e516bddf Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 20:30:28 -0700 Subject: [PATCH 396/437] [fdfd.functional] fix handling of mu in e_full and m2j sign --- meanas/fdfd/functional.py | 12 +-- meanas/test/test_fdfd_functional.py | 130 ++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 meanas/test/test_fdfd_functional.py diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index 440daf2..9d98798 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -41,8 +41,8 @@ def e_full( curls = ch(ce(e)) return cfdfield_t(curls - omega ** 2 * epsilon * e) - def op_mu(e: cfdfield) -> cfdfield_t: - curls = ch(mu * ce(e)) # type: ignore # mu = None ok because we don't return the function + def op_mu(e: cfdfield_t) -> cfdfield_t: + curls = ch(ce(e) / mu) # type: ignore # mu = None ok because we don't return the function return cfdfield_t(curls - omega ** 2 * epsilon * e) if mu is None: @@ -138,12 +138,12 @@ def m2j( """ ch = curl_back(dxes[1]) - def m2j_mu(m: cfdfield) -> cfdfield_t: - J = ch(m / mu) / (-1j * omega) # type: ignore # mu=None ok + def m2j_mu(m: cfdfield_t) -> cfdfield_t: + J = ch(m / mu) / (1j * omega) # type: ignore # mu=None ok return cfdfield_t(J) - def m2j_1(m: cfdfield) -> cfdfield_t: - J = ch(m) / (-1j * omega) + def m2j_1(m: cfdfield_t) -> cfdfield_t: + J = ch(m) / (1j * omega) return cfdfield_t(J) if mu is None: diff --git a/meanas/test/test_fdfd_functional.py b/meanas/test/test_fdfd_functional.py new file mode 100644 index 0000000..f4fd4bb --- /dev/null +++ b/meanas/test/test_fdfd_functional.py @@ -0,0 +1,130 @@ +import numpy +from numpy.testing import assert_allclose + +from ..fdmath import vec, unvec +from ..fdfd import functional, operators + + +OMEGA = 1 / 1500 +SHAPE = (2, 3, 2) +ATOL = 1e-9 +RTOL = 1e-9 + +DXES = [ + [numpy.array([1.0, 1.5]), numpy.array([0.75, 1.25, 1.5]), numpy.array([1.2, 0.8])], + [numpy.array([0.9, 1.4]), numpy.array([0.8, 1.1, 1.4]), numpy.array([1.0, 0.7])], +] + +EPSILON = numpy.stack([ + numpy.linspace(1.0, 2.2, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(1.1, 2.3, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(1.2, 2.4, numpy.prod(SHAPE)).reshape(SHAPE), +]) +MU = numpy.stack([ + numpy.linspace(2.0, 3.2, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(2.1, 3.3, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(2.2, 3.4, numpy.prod(SHAPE)).reshape(SHAPE), +]) + +E_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) + 0.5j).astype(complex) +H_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) * 0.25 - 0.75j).astype(complex) + +TF_REGION = numpy.zeros((3, *SHAPE), dtype=float) +TF_REGION[:, 0, 1, 0] = 1.0 + + +def apply_matrix(op: operators.sparse.spmatrix, field: numpy.ndarray) -> numpy.ndarray: + return unvec(op @ vec(field), SHAPE) + + +def assert_fields_match(actual: numpy.ndarray, expected: numpy.ndarray) -> None: + assert_allclose(actual, expected, atol=ATOL, rtol=RTOL) + + +def test_e_full_matches_sparse_operator_without_mu() -> None: + matrix_result = apply_matrix( + operators.e_full(OMEGA, DXES, vec(EPSILON)), + E_FIELD, + ) + functional_result = functional.e_full(OMEGA, DXES, EPSILON)(E_FIELD) + + assert_fields_match(functional_result, matrix_result) + + +def test_e_full_matches_sparse_operator_with_mu() -> None: + matrix_result = apply_matrix( + operators.e_full(OMEGA, DXES, vec(EPSILON), vec(MU)), + E_FIELD, + ) + functional_result = functional.e_full(OMEGA, DXES, EPSILON, MU)(E_FIELD) + + assert_fields_match(functional_result, matrix_result) + + +def test_eh_full_matches_sparse_operator_with_mu() -> None: + matrix_result = operators.eh_full(OMEGA, DXES, vec(EPSILON), vec(MU)) @ numpy.concatenate([vec(E_FIELD), vec(H_FIELD)]) + matrix_e, matrix_h = (unvec(part, SHAPE) for part in numpy.split(matrix_result, 2)) + functional_e, functional_h = functional.eh_full(OMEGA, DXES, EPSILON, MU)(E_FIELD, H_FIELD) + + assert_fields_match(functional_e, matrix_e) + assert_fields_match(functional_h, matrix_h) + + +def test_e2h_matches_sparse_operator_with_mu() -> None: + matrix_result = apply_matrix( + operators.e2h(OMEGA, DXES, vec(MU)), + E_FIELD, + ) + functional_result = functional.e2h(OMEGA, DXES, MU)(E_FIELD) + + assert_fields_match(functional_result, matrix_result) + + +def test_m2j_matches_sparse_operator_without_mu() -> None: + matrix_result = apply_matrix( + operators.m2j(OMEGA, DXES), + H_FIELD, + ) + functional_result = functional.m2j(OMEGA, DXES)(H_FIELD) + + assert_fields_match(functional_result, matrix_result) + + +def test_m2j_matches_sparse_operator_with_mu() -> None: + matrix_result = apply_matrix( + operators.m2j(OMEGA, DXES, vec(MU)), + H_FIELD, + ) + functional_result = functional.m2j(OMEGA, DXES, MU)(H_FIELD) + + assert_fields_match(functional_result, matrix_result) + + +def test_e_tfsf_source_matches_sparse_operator_without_mu() -> None: + matrix_result = apply_matrix( + operators.e_tfsf_source(vec(TF_REGION), OMEGA, DXES, vec(EPSILON)), + E_FIELD, + ) + functional_result = functional.e_tfsf_source(TF_REGION, OMEGA, DXES, EPSILON)(E_FIELD) + + assert_fields_match(functional_result, matrix_result) + + +def test_e_tfsf_source_matches_sparse_operator_with_mu() -> None: + matrix_result = apply_matrix( + operators.e_tfsf_source(vec(TF_REGION), OMEGA, DXES, vec(EPSILON), vec(MU)), + E_FIELD, + ) + functional_result = functional.e_tfsf_source(TF_REGION, OMEGA, DXES, EPSILON, MU)(E_FIELD) + + assert_fields_match(functional_result, matrix_result) + + +def test_poynting_e_cross_h_matches_sparse_operator() -> None: + matrix_result = apply_matrix( + operators.poynting_e_cross(vec(E_FIELD), DXES), + H_FIELD, + ) + functional_result = functional.poynting_e_cross_h(DXES)(E_FIELD, H_FIELD) + + assert_fields_match(functional_result, matrix_result) From f35b334100c28e2fd4b0e8787517f41360f7430a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 20:44:36 -0700 Subject: [PATCH 397/437] [fdfd.waveguide_3d] improve handling of out-of-bounds overlap_e windows --- meanas/fdfd/waveguide_3d.py | 24 ++- meanas/test/test_waveguide_mode_helpers.py | 213 +++++++++++++++++++-- 2 files changed, 218 insertions(+), 19 deletions(-) diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index 61ed38e..19975db 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -5,6 +5,8 @@ This module relies heavily on `waveguide_2d` and mostly just transforms its parameters into 2D equivalents and expands the results back into 3D. """ from typing import Any, cast +import warnings +from typing import Any from collections.abc import Sequence import numpy from numpy.typing import NDArray @@ -200,17 +202,33 @@ def compute_overlap_e( Ee = expand_e(E=E, wavenumber=wavenumber, dxes=dxes, axis=axis, polarity=polarity, slices=slices) - start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity)) + axis_size = E.shape[axis + 1] + if polarity > 0: + start = slices[axis].start - 2 + stop = slices[axis].start + else: + start = slices[axis].stop + stop = slices[axis].stop + 2 + + clipped_start = max(0, start) + clipped_stop = min(axis_size, stop) + if clipped_start >= clipped_stop: + raise ValueError('Requested overlap window lies outside the domain') + if clipped_start != start or clipped_stop != stop: + warnings.warn('Requested overlap window was clipped to fit within the domain', RuntimeWarning) slices2_l = list(slices) - slices2_l[axis] = slice(start, stop) + slices2_l[axis] = slice(clipped_start, clipped_stop) slices2 = (slice(None), *slices2_l) Etgt = numpy.zeros_like(Ee) Etgt[slices2] = Ee[slices2] # Note: We normalize so that (Etgt @ E.conj()) == 1, so (Etgt @ Etgt.conj) != 1 - Etgt /= (Etgt.conj() * Etgt).sum() + norm = (Etgt.conj() * Etgt).sum() + if norm == 0: + raise ValueError('Requested overlap window contains no overlap field support') + Etgt /= norm return cfdfield_t(Etgt) diff --git a/meanas/test/test_waveguide_mode_helpers.py b/meanas/test/test_waveguide_mode_helpers.py index 2bf77f2..7bbcd88 100644 --- a/meanas/test/test_waveguide_mode_helpers.py +++ b/meanas/test/test_waveguide_mode_helpers.py @@ -1,29 +1,56 @@ +import contextlib +import io import numpy from numpy.linalg import norm +import pytest +import warnings -from ..fdmath import vec -from ..fdfd import waveguide_3d, waveguide_cyl +from ..fdmath import vec, unvec +from ..fdfd import waveguide_2d, waveguide_3d, waveguide_cyl OMEGA = 1 / 1500 -def test_waveguide_3d_solve_mode_and_expand_e_are_phase_consistent() -> None: +def build_waveguide_3d_mode( + *, + slice_start: int, + polarity: int, + ) -> tuple[numpy.ndarray, list[list[numpy.ndarray]], tuple[slice, slice, slice], dict[str, complex | numpy.ndarray]]: epsilon = numpy.ones((3, 5, 5, 1), dtype=float) dxes = [[numpy.ones(5), numpy.ones(5), numpy.ones(1)] for _ in range(2)] - axis = 0 - polarity = 1 - slices = (slice(0, 1), slice(None), slice(None)) - + slices = (slice(slice_start, slice_start + 1), slice(None), slice(None)) result = waveguide_3d.solve_mode( 0, omega=OMEGA, dxes=dxes, - axis=axis, + axis=0, polarity=polarity, slices=slices, epsilon=epsilon, ) + return epsilon, dxes, slices, result + + +def build_waveguide_cyl_fixture( + *, + nonuniform: bool = False, + ) -> tuple[list[list[numpy.ndarray]], numpy.ndarray, float]: + if nonuniform: + dxes = [ + [numpy.array([1.0, 1.5, 1.2, 0.8, 1.1]), numpy.ones(5)], + [numpy.array([0.9, 1.4, 1.0, 0.7, 1.2]), numpy.ones(5)], + ] + else: + dxes = [[numpy.ones(5), numpy.ones(5)] for _ in range(2)] + epsilon = vec(numpy.ones((3, 5, 5), dtype=float)) + return dxes, epsilon, 10.0 + + +def test_waveguide_3d_solve_mode_and_expand_e_are_phase_consistent() -> None: + epsilon, dxes, slices, result = build_waveguide_3d_mode(slice_start=0, polarity=1) + axis = 0 + polarity = 1 expanded = waveguide_3d.expand_e( E=result['E'], wavenumber=result['wavenumber'], @@ -55,11 +82,88 @@ def test_waveguide_3d_solve_mode_and_expand_e_are_phase_consistent() -> None: numpy.testing.assert_allclose(ratios, expected_ratio, rtol=1e-6, atol=1e-9) +@pytest.mark.parametrize( + ('polarity', 'expected_range'), + [(1, (0, 1)), (-1, (3, 4))], + ) +def test_waveguide_3d_compute_overlap_e_uses_adjacent_window( + polarity: int, + expected_range: tuple[int, int], + ) -> None: + _epsilon, dxes, slices, result = build_waveguide_3d_mode(slice_start=2, polarity=polarity) + + with warnings.catch_warnings(record=True) as caught: + overlap = waveguide_3d.compute_overlap_e( + E=result['E'], + wavenumber=result['wavenumber'], + dxes=dxes, + axis=0, + polarity=polarity, + slices=slices, + omega=OMEGA, + ) + + nonzero = numpy.argwhere(numpy.abs(overlap) > 0) + + assert not caught + assert numpy.isfinite(overlap).all() + assert nonzero[:, 1].min() == expected_range[0] + assert nonzero[:, 1].max() == expected_range[1] + + +@pytest.mark.parametrize( + ('polarity', 'slice_start', 'expected_index'), + [(1, 1, 0), (-1, 3, 4)], + ) +def test_waveguide_3d_compute_overlap_e_warns_when_window_is_clipped( + polarity: int, + slice_start: int, + expected_index: int, + ) -> None: + _epsilon, dxes, slices, result = build_waveguide_3d_mode(slice_start=slice_start, polarity=polarity) + + with pytest.warns(RuntimeWarning, match='clipped'): + overlap = waveguide_3d.compute_overlap_e( + E=result['E'], + wavenumber=result['wavenumber'], + dxes=dxes, + axis=0, + polarity=polarity, + slices=slices, + omega=OMEGA, + ) + + nonzero = numpy.argwhere(numpy.abs(overlap) > 0) + + assert numpy.isfinite(overlap).all() + assert nonzero[:, 1].min() == expected_index + assert nonzero[:, 1].max() == expected_index + + +@pytest.mark.parametrize( + ('polarity', 'slice_start'), + [(1, 0), (-1, 4)], + ) +def test_waveguide_3d_compute_overlap_e_rejects_empty_overlap_window( + polarity: int, + slice_start: int, + ) -> None: + _epsilon, dxes, slices, result = build_waveguide_3d_mode(slice_start=slice_start, polarity=polarity) + + with pytest.raises(ValueError, match='outside the domain'): + waveguide_3d.compute_overlap_e( + E=result['E'], + wavenumber=result['wavenumber'], + dxes=dxes, + axis=0, + polarity=polarity, + slices=slices, + omega=OMEGA, + ) + + def test_waveguide_cyl_solved_modes_are_ordered_and_low_residual() -> None: - shape = (5, 5) - dxes = [[numpy.ones(shape[0]), numpy.ones(shape[1])] for _ in range(2)] - epsilon = vec(numpy.ones((3, *shape), dtype=float)) - rmin = 10.0 + dxes, epsilon, rmin = build_waveguide_cyl_fixture() e_xys, angular_wavenumbers = waveguide_cyl.solve_modes( [0, 1], @@ -79,9 +183,7 @@ def test_waveguide_cyl_solved_modes_are_ordered_and_low_residual() -> None: def test_waveguide_cyl_linear_wavenumbers_are_finite_and_ordered() -> None: - shape = (5, 5) - dxes = [[numpy.ones(shape[0]), numpy.ones(shape[1])] for _ in range(2)] - epsilon = vec(numpy.ones((3, *shape), dtype=float)) + dxes, epsilon, rmin = build_waveguide_cyl_fixture() e_xys, angular_wavenumbers = waveguide_cyl.solve_modes( [0, 1], @@ -95,9 +197,88 @@ def test_waveguide_cyl_linear_wavenumbers_are_finite_and_ordered() -> None: angular_wavenumbers, epsilon=epsilon, dxes=dxes, - rmin=10.0, + rmin=rmin, ) assert numpy.isfinite(linear_wavenumbers).all() assert numpy.all(numpy.real(linear_wavenumbers) > 0) assert numpy.all(numpy.diff(numpy.real(linear_wavenumbers)) <= 0) + + +def test_waveguide_cyl_dxes2t_matches_expected_radius_scaling() -> None: + dxes, _epsilon, rmin = build_waveguide_cyl_fixture(nonuniform=True) + Ta, Tb = waveguide_cyl.dxes2T(dxes, rmin) + + ta = (rmin + numpy.cumsum(dxes[0][0])) / rmin + tb = (rmin + dxes[0][0] / 2 + numpy.cumsum(dxes[1][0])) / rmin + + numpy.testing.assert_allclose(Ta.diagonal(), numpy.repeat(ta, dxes[0][1].size)) + numpy.testing.assert_allclose(Tb.diagonal(), numpy.repeat(tb, dxes[1][1].size)) + + +def test_waveguide_cyl_exy2e_and_exy2h_return_finite_full_fields() -> None: + dxes, epsilon, rmin = build_waveguide_cyl_fixture() + mu = vec(2 * numpy.ones((3, 5, 5), dtype=float)) + e_xy, angular_wavenumber = waveguide_cyl.solve_mode( + 0, + omega=OMEGA, + dxes=dxes, + epsilon=epsilon, + rmin=rmin, + ) + + e_field = waveguide_cyl.exy2e( + angular_wavenumber=angular_wavenumber, + omega=OMEGA, + dxes=dxes, + rmin=rmin, + epsilon=epsilon, + ) @ e_xy + h_field = waveguide_cyl.exy2h( + angular_wavenumber=angular_wavenumber, + omega=OMEGA, + dxes=dxes, + rmin=rmin, + epsilon=epsilon, + mu=mu, + ) @ e_xy + + assert e_field.shape == (3 * 25,) + assert h_field.shape == (3 * 25,) + assert numpy.isfinite(e_field).all() + assert numpy.isfinite(h_field).all() + assert unvec(e_field, (5, 5)).shape == (3, 5, 5) + assert unvec(h_field, (5, 5)).shape == (3, 5, 5) + + +@pytest.mark.parametrize('use_mu', [False, True]) +def test_waveguide_cyl_normalized_fields_are_unit_norm_and_silent(use_mu: bool) -> None: + dxes, epsilon, rmin = build_waveguide_cyl_fixture() + mu = vec(2 * numpy.ones((3, 5, 5), dtype=float)) if use_mu else None + e_xy, angular_wavenumber = waveguide_cyl.solve_mode( + 0, + omega=OMEGA, + dxes=dxes, + epsilon=epsilon, + rmin=rmin, + ) + + output = io.StringIO() + with contextlib.redirect_stdout(output): + e_field, h_field = waveguide_cyl.normalized_fields_e( + e_xy, + angular_wavenumber=angular_wavenumber, + omega=OMEGA, + dxes=dxes, + rmin=rmin, + epsilon=epsilon, + mu=mu, + ) + + overlap = waveguide_2d.inner_product(e_field, h_field, dxes, conj_h=True) + + assert output.getvalue() == '' + assert numpy.isfinite(e_field).all() + assert numpy.isfinite(h_field).all() + assert abs(overlap.real - 1.0) < 1e-10 + assert abs(overlap.imag) < 1e-10 From 07b16ad86a8ea128160117a454e499d842da79a9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 20:59:24 -0700 Subject: [PATCH 398/437] [bloch] fixup some vectorization and add tests --- meanas/fdfd/bloch.py | 30 +++++----- meanas/test/test_bloch_foundations.py | 85 +++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 meanas/test/test_bloch_foundations.py diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index 4eedcc4..e53acb3 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -136,6 +136,14 @@ except ImportError: logger.info('Using numpy fft') +def _assemble_hmn_vector( + h_m: NDArray[numpy.complex128], + h_n: NDArray[numpy.complex128], + ) -> NDArray[numpy.complex128]: + stacked = numpy.concatenate((numpy.ravel(h_m), numpy.ravel(h_n))) + return stacked[:, None] + + def generate_kmn( k0: ArrayLike, G_matrix: ArrayLike, @@ -253,8 +261,8 @@ def maxwell_operator( h_m, h_n = b_m, b_n else: # transform from mn to xyz - b_xyz = (m * b_m[:, :, :, None] - + n * b_n[:, :, :, None]) + b_xyz = (m * b_m + + n * b_n) # noqa: E128 # divide by mu temp = ifftn(b_xyz, axes=range(3)) @@ -265,10 +273,7 @@ def maxwell_operator( h_m = numpy.sum(h_xyz * m, axis=3) h_n = numpy.sum(h_xyz * n, axis=3) - h.shape = (h.size,) - h = numpy.concatenate((h_m.ravel(), h_n.ravel()), axis=None, out=h) # ravel and merge - h.shape = (h.size, 1) - return h + return _assemble_hmn_vector(h_m, h_n) return operator @@ -403,8 +408,8 @@ def inverse_maxwell_operator_approx( b_m, b_n = hin_m, hin_n else: # transform from mn to xyz - h_xyz = (m * hin_m[:, :, :, None] - + n * hin_n[:, :, :, None]) + h_xyz = (m * hin_m + + n * hin_n) # noqa: E128 # multiply by mu temp = ifftn(h_xyz, axes=range(3)) @@ -412,8 +417,8 @@ def inverse_maxwell_operator_approx( b_xyz = fftn(temp, axes=range(3)) # transform back to mn - b_m = numpy.sum(b_xyz * m, axis=3) - b_n = numpy.sum(b_xyz * n, axis=3) + b_m = numpy.sum(b_xyz * m, axis=3, keepdims=True) + b_n = numpy.sum(b_xyz * n, axis=3, keepdims=True) # cross product and transform into xyz basis e_xyz = (n * b_m @@ -428,10 +433,7 @@ def inverse_maxwell_operator_approx( h_m = numpy.sum(d_xyz * n, axis=3, keepdims=True) / +k_mag h_n = numpy.sum(d_xyz * m, axis=3, keepdims=True) / -k_mag - h.shape = (h.size,) - h = numpy.concatenate((h_m, h_n), axis=None, out=h) - h.shape = (h.size, 1) - return h + return _assemble_hmn_vector(h_m, h_n) return operator diff --git a/meanas/test/test_bloch_foundations.py b/meanas/test/test_bloch_foundations.py new file mode 100644 index 0000000..0321365 --- /dev/null +++ b/meanas/test/test_bloch_foundations.py @@ -0,0 +1,85 @@ +import numpy +from numpy.linalg import norm + +from ..fdfd import bloch + + +SHAPE = (2, 2, 2) +K0 = numpy.array([0.1, 0.2, 0.3]) +G_MATRIX = numpy.eye(3) +EPSILON = numpy.ones((3, *SHAPE), dtype=float) +MU = numpy.stack([ + numpy.linspace(2.0, 2.7, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(2.1, 2.8, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(2.2, 2.9, numpy.prod(SHAPE)).reshape(SHAPE), +]) +H_MN = (numpy.arange(2 * numpy.prod(SHAPE)) + 0.25j).astype(complex) +ZERO_H_MN = numpy.zeros_like(H_MN) + + +def test_generate_kmn_general_case_returns_orthonormal_basis() -> None: + k_mag, m_vecs, n_vecs = bloch.generate_kmn(K0, G_MATRIX, SHAPE) + + assert k_mag.shape == SHAPE + (1,) + assert m_vecs.shape == SHAPE + (3,) + assert n_vecs.shape == SHAPE + (3,) + assert numpy.isfinite(k_mag).all() + assert numpy.isfinite(m_vecs).all() + assert numpy.isfinite(n_vecs).all() + + numpy.testing.assert_allclose(norm(m_vecs.reshape(-1, 3), axis=1), 1.0) + numpy.testing.assert_allclose(norm(n_vecs.reshape(-1, 3), axis=1), 1.0) + numpy.testing.assert_allclose(numpy.sum(m_vecs * n_vecs, axis=3), 0.0, atol=1e-12) + + +def test_generate_kmn_z_aligned_uses_default_transverse_basis() -> None: + k_mag, m_vecs, n_vecs = bloch.generate_kmn([0.0, 0.0, 0.25], G_MATRIX, (1, 1, 1)) + + assert numpy.isfinite(k_mag).all() + numpy.testing.assert_allclose(m_vecs[0, 0, 0], [0.0, 1.0, 0.0]) + numpy.testing.assert_allclose(numpy.sum(m_vecs * n_vecs, axis=3), 0.0, atol=1e-12) + numpy.testing.assert_allclose(norm(n_vecs.reshape(-1, 3), axis=1), 1.0) + + +def test_maxwell_operator_returns_finite_column_vector_without_mu() -> None: + operator = bloch.maxwell_operator(K0, G_MATRIX, EPSILON) + + result = operator(H_MN.copy()) + zero_result = operator(ZERO_H_MN.copy()) + + assert result.shape == (2 * numpy.prod(SHAPE), 1) + assert numpy.isfinite(result).all() + numpy.testing.assert_allclose(zero_result, 0.0) + + +def test_maxwell_operator_returns_finite_column_vector_with_mu() -> None: + operator = bloch.maxwell_operator(K0, G_MATRIX, EPSILON, MU) + + result = operator(H_MN.copy()) + zero_result = operator(ZERO_H_MN.copy()) + + assert result.shape == (2 * numpy.prod(SHAPE), 1) + assert numpy.isfinite(result).all() + numpy.testing.assert_allclose(zero_result, 0.0) + + +def test_inverse_maxwell_operator_returns_finite_column_vector_for_both_mu_branches() -> None: + for mu in (None, MU): + operator = bloch.inverse_maxwell_operator_approx(K0, G_MATRIX, EPSILON, mu) + + result = operator(H_MN.copy()) + zero_result = operator(ZERO_H_MN.copy()) + + assert result.shape == (2 * numpy.prod(SHAPE), 1) + assert numpy.isfinite(result).all() + numpy.testing.assert_allclose(zero_result, 0.0) + + +def test_bloch_field_converters_return_finite_fields() -> None: + e_field = bloch.hmn_2_exyz(K0, G_MATRIX, EPSILON)(H_MN.copy()) + h_field = bloch.hmn_2_hxyz(K0, G_MATRIX, EPSILON)(H_MN.copy()) + + assert e_field.shape == (3, *SHAPE) + assert h_field.shape == (3, *SHAPE) + assert numpy.isfinite(e_field).all() + assert numpy.isfinite(h_field).all() From 87bb3af3f907722b7dc9f4a3c8772c37d26090de Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 21:34:36 -0700 Subject: [PATCH 399/437] [fdfd] minor fixes and more tests --- meanas/fdfd/scpml.py | 10 +- meanas/fdfd/solvers.py | 12 +- meanas/test/test_fdfd_algebra_helpers.py | 170 ++++++++++++++++++++++ meanas/test/test_fdfd_solvers.py | 140 ++++++++++++++++++ meanas/test/test_fdmath_functional.py | 60 ++++++++ meanas/test/test_waveguide_2d_numerics.py | 15 +- 6 files changed, 392 insertions(+), 15 deletions(-) create mode 100644 meanas/test/test_fdfd_algebra_helpers.py create mode 100644 meanas/test/test_fdfd_solvers.py create mode 100644 meanas/test/test_fdmath_functional.py diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py index f0a8843..7030876 100644 --- a/meanas/fdfd/scpml.py +++ b/meanas/fdfd/scpml.py @@ -128,6 +128,11 @@ def stretch_with_scpml( dx_ai = dxes[0][axis].astype(complex) dx_bi = dxes[1][axis].astype(complex) + if thickness == 0: + dxes[0][axis] = dx_ai + dxes[1][axis] = dx_bi + return dxes + pos = numpy.hstack((0, dx_ai.cumsum())) pos_a = (pos[:-1] + pos[1:]) / 2 pos_b = pos[:-1] @@ -153,10 +158,7 @@ def stretch_with_scpml( def l_d(x: NDArray[numpy.float64]) -> NDArray[numpy.float64]: return (x - bound) / (pos[-1] - bound) - if thickness == 0: - slc = slice(None) - else: - slc = slice(-thickness, None) + slc = slice(-thickness, None) dx_ai[slc] *= 1 + 1j * s_function(l_d(pos_a[slc])) / d / s_correction dx_bi[slc] *= 1 + 1j * s_function(l_d(pos_b[slc])) / d / s_correction diff --git a/meanas/fdfd/solvers.py b/meanas/fdfd/solvers.py index c0aed44..e1f394c 100644 --- a/meanas/fdfd/solvers.py +++ b/meanas/fdfd/solvers.py @@ -48,9 +48,11 @@ def _scipy_qmr( logger.info(f'Solver residual at iteration {ii} : {cur_norm}') if 'callback' in kwargs: + callback = kwargs['callback'] + def augmented_callback(xk: ArrayLike) -> None: log_residual(xk) - kwargs['callback'](xk) + callback(xk) kwargs['callback'] = augmented_callback else: @@ -118,15 +120,15 @@ def generic( Pl, Pr = operators.e_full_preconditioners(dxes) if adjoint: - A = (Pl @ A0 @ Pr).H - b = Pr.H @ b0 + A = (Pl @ A0 @ Pr).T.conjugate() + b = Pr.T.conjugate() @ b0 else: A = Pl @ A0 @ Pr b = Pl @ b0 if E_guess is not None: if adjoint: - x0 = Pr.H @ E_guess + x0 = Pr.T.conjugate() @ E_guess else: x0 = Pl @ E_guess matrix_solver_opts['x0'] = x0 @@ -134,7 +136,7 @@ def generic( x = matrix_solver(A.tocsr(), b, **matrix_solver_opts) if adjoint: - x0 = Pl.H @ x + x0 = Pl.T.conjugate() @ x else: x0 = Pr @ x diff --git a/meanas/test/test_fdfd_algebra_helpers.py b/meanas/test/test_fdfd_algebra_helpers.py new file mode 100644 index 0000000..c1d1b3f --- /dev/null +++ b/meanas/test/test_fdfd_algebra_helpers.py @@ -0,0 +1,170 @@ +import numpy +from numpy.testing import assert_allclose + +from ..fdmath import vec, unvec +from ..fdmath import functional as fd_functional +from ..fdfd import operators, scpml + + +OMEGA = 1 / 1500 +SHAPE = (2, 3, 2) +DXES = [ + [numpy.array([1.0, 1.5]), numpy.array([0.75, 1.25, 1.5]), numpy.array([1.2, 0.8])], + [numpy.array([0.9, 1.4]), numpy.array([0.8, 1.1, 1.4]), numpy.array([1.0, 0.7])], +] + +EPSILON = numpy.stack([ + numpy.linspace(1.0, 2.2, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(1.1, 2.3, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(1.2, 2.4, numpy.prod(SHAPE)).reshape(SHAPE), +]) +MU = numpy.stack([ + numpy.linspace(2.0, 3.2, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(2.1, 3.3, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(2.2, 3.4, numpy.prod(SHAPE)).reshape(SHAPE), +]) + +H_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) * 0.25 - 0.75j).astype(complex) +E_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) + 0.5j).astype(complex) + +PEC = numpy.zeros((3, *SHAPE), dtype=float) +PEC[1, 0, 1, 0] = 1.0 +PMC = numpy.zeros((3, *SHAPE), dtype=float) +PMC[2, 1, 2, 1] = 1.0 + +BOUNDARY_SHAPE = (3, 4, 3) +BOUNDARY_DXES = [ + [numpy.array([1.0, 1.5, 0.8]), numpy.array([0.75, 1.25, 1.5, 0.9]), numpy.array([1.2, 0.8, 1.1])], + [numpy.array([0.9, 1.4, 1.0]), numpy.array([0.8, 1.1, 1.4, 1.0]), numpy.array([1.0, 0.7, 1.3])], +] +BOUNDARY_EPSILON = numpy.stack([ + numpy.linspace(1.0, 2.2, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE), + numpy.linspace(1.1, 2.3, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE), + numpy.linspace(1.2, 2.4, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE), +]) +BOUNDARY_FIELD = (numpy.arange(3 * numpy.prod(BOUNDARY_SHAPE)).reshape((3, *BOUNDARY_SHAPE)) + 0.5j).astype(complex) + + +def _apply_matrix(op: operators.sparse.spmatrix, field: numpy.ndarray, shape: tuple[int, ...]) -> numpy.ndarray: + return unvec(op @ vec(field), shape) + + +def _dense_h_full(mu: numpy.ndarray | None) -> numpy.ndarray: + ce = fd_functional.curl_forward(DXES[0]) + ch = fd_functional.curl_back(DXES[1]) + pe = numpy.where(PEC, 0.0, 1.0) + pm = numpy.where(PMC, 0.0, 1.0) + magnetic = numpy.ones_like(EPSILON) if mu is None else mu + + masked_h = pm * H_FIELD + curl_term = ch(masked_h) + curl_term = pe * (curl_term / EPSILON) + curl_term = ce(curl_term) + return pm * (curl_term - OMEGA**2 * magnetic * masked_h) + + +def _normalized_distance(u: numpy.ndarray, size: int, thickness: int) -> numpy.ndarray: + return ((thickness - u).clip(0) + (u - (size - thickness)).clip(0)) / thickness + + +def test_h_full_matches_dense_reference_with_and_without_mu() -> None: + for mu in (None, MU): + matrix_result = _apply_matrix( + operators.h_full(OMEGA, DXES, vec(EPSILON), None if mu is None else vec(mu), vec(PEC), vec(PMC)), + H_FIELD, + SHAPE, + ) + dense_result = _dense_h_full(mu) + assert_allclose(matrix_result, dense_result, atol=1e-10, rtol=1e-10) + + +def test_poynting_h_cross_matches_negative_e_cross_relation() -> None: + h_cross_e = _apply_matrix(operators.poynting_h_cross(vec(H_FIELD), DXES), E_FIELD, SHAPE) + e_cross_h = _apply_matrix(operators.poynting_e_cross(vec(E_FIELD), DXES), H_FIELD, SHAPE) + + assert_allclose(h_cross_e, -e_cross_h, atol=1e-10, rtol=1e-10) + + +def test_e_boundary_source_interior_mask_is_independent_of_periodic_edges() -> None: + mask = numpy.zeros((3, *BOUNDARY_SHAPE), dtype=float) + mask[:, 1, 1, 1] = 1.0 + + periodic = operators.e_boundary_source(vec(mask), OMEGA, BOUNDARY_DXES, vec(BOUNDARY_EPSILON), periodic_mask_edges=True) + mirrored = operators.e_boundary_source(vec(mask), OMEGA, BOUNDARY_DXES, vec(BOUNDARY_EPSILON), periodic_mask_edges=False) + + assert_allclose(periodic.toarray(), mirrored.toarray()) + + +def test_e_boundary_source_periodic_edges_add_opposite_face_response() -> None: + mask = numpy.zeros((3, *BOUNDARY_SHAPE), dtype=float) + mask[:, 0, 1, 1] = 1.0 + + periodic = operators.e_boundary_source(vec(mask), OMEGA, BOUNDARY_DXES, vec(BOUNDARY_EPSILON), periodic_mask_edges=True) + mirrored = operators.e_boundary_source(vec(mask), OMEGA, BOUNDARY_DXES, vec(BOUNDARY_EPSILON), periodic_mask_edges=False) + diff = unvec((periodic - mirrored) @ vec(BOUNDARY_FIELD), BOUNDARY_SHAPE) + + assert numpy.isfinite(diff).all() + assert_allclose(diff[:, 1:-1, :, :], 0.0) + assert numpy.linalg.norm(diff[:, -1, :, :]) > 0 + + +def test_prepare_s_function_matches_closed_form_polynomial() -> None: + ln_r = -12.0 + order = 3.0 + distances = numpy.array([0.0, 0.25, 0.5, 1.0]) + s_function = scpml.prepare_s_function(ln_R=ln_r, m=order) + expected = (order + 1) * ln_r / 2 * distances**order + + assert_allclose(s_function(distances), expected) + + +def test_uniform_grid_scpml_matches_expected_stretch_profile() -> None: + s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0) + dxes = scpml.uniform_grid_scpml((6, 4, 3), (2, 0, 1), omega=2.0, epsilon_effective=4.0, s_function=s_function) + correction = numpy.sqrt(4.0) * 2.0 + + for axis, size, thickness in ((0, 6, 2), (2, 3, 1)): + grid = numpy.arange(size, dtype=float) + expected_a = 1 + 1j * s_function(_normalized_distance(grid, size, thickness)) / correction + expected_b = 1 + 1j * s_function(_normalized_distance(grid + 0.5, size, thickness)) / correction + assert_allclose(dxes[0][axis], expected_a) + assert_allclose(dxes[1][axis], expected_b) + + assert_allclose(dxes[0][1], 1.0) + assert_allclose(dxes[1][1], 1.0) + assert numpy.isfinite(dxes[0][0]).all() + assert numpy.isfinite(dxes[1][0]).all() + + +def test_stretch_with_scpml_only_modifies_requested_front_edge() -> None: + s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0) + base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)] + stretched = scpml.stretch_with_scpml(base, axis=0, polarity=1, omega=2.0, epsilon_effective=4.0, thickness=2, s_function=s_function) + + assert_allclose(stretched[0][0][2:], 1.0) + assert_allclose(stretched[1][0][2:], 1.0) + assert_allclose(stretched[0][0][-2:], 1.0) + assert_allclose(stretched[1][0][-2:], 1.0) + assert numpy.linalg.norm(stretched[0][0][:2] - 1.0) > 0 + assert numpy.linalg.norm(stretched[1][0][:2] - 1.0) > 0 + + +def test_stretch_with_scpml_only_modifies_requested_back_edge() -> None: + s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0) + base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)] + stretched = scpml.stretch_with_scpml(base, axis=0, polarity=-1, omega=2.0, epsilon_effective=4.0, thickness=2, s_function=s_function) + + assert_allclose(stretched[0][0][:4], 1.0) + assert_allclose(stretched[1][0][:4], 1.0) + assert numpy.linalg.norm(stretched[0][0][-2:] - 1.0) > 0 + assert numpy.linalg.norm(stretched[1][0][-2:] - 1.0) > 0 + + +def test_stretch_with_scpml_thickness_zero_is_noop() -> None: + s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0) + base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)] + stretched = scpml.stretch_with_scpml(base, axis=0, polarity=-1, omega=2.0, epsilon_effective=4.0, thickness=0, s_function=s_function) + + for grid_group in stretched: + for axis_grid in grid_group: + assert_allclose(axis_grid, 1.0) diff --git a/meanas/test/test_fdfd_solvers.py b/meanas/test/test_fdfd_solvers.py new file mode 100644 index 0000000..18c54be --- /dev/null +++ b/meanas/test/test_fdfd_solvers.py @@ -0,0 +1,140 @@ +import numpy +from numpy.testing import assert_allclose +from scipy import sparse + +from ..fdfd import solvers + + +def test_scipy_qmr_wraps_user_callback_without_recursion(monkeypatch) -> None: + seen: list[tuple[float, ...]] = [] + + def fake_qmr(a: sparse.spmatrix, b: numpy.ndarray, **kwargs): + kwargs['callback'](numpy.array([1.0, 2.0])) + return numpy.array([3.0, 4.0]), 0 + + monkeypatch.setattr(solvers.scipy.sparse.linalg, 'qmr', fake_qmr) + result = solvers._scipy_qmr( + sparse.eye(2).tocsr(), + numpy.array([1.0, 0.0]), + callback=lambda xk: seen.append(tuple(xk)), + ) + + assert_allclose(result, [3.0, 4.0]) + assert seen == [(1.0, 2.0)] + + +def test_scipy_qmr_installs_logging_callback_when_missing(monkeypatch) -> None: + callback_seen: list[numpy.ndarray] = [] + + def fake_qmr(a: sparse.spmatrix, b: numpy.ndarray, **kwargs): + callback = kwargs['callback'] + callback(numpy.array([5.0, 6.0])) + callback_seen.append(b.copy()) + return numpy.array([7.0, 8.0]), 0 + + monkeypatch.setattr(solvers.scipy.sparse.linalg, 'qmr', fake_qmr) + result = solvers._scipy_qmr(sparse.eye(2).tocsr(), numpy.array([1.0, 0.0])) + + assert_allclose(result, [7.0, 8.0]) + assert len(callback_seen) == 1 + + +def test_generic_forward_preconditions_system_and_guess(monkeypatch) -> None: + omega = 2.0 + a0 = sparse.csr_matrix(numpy.array([[1.0 + 2.0j, 2.0], [3.0 - 1.0j, 4.0]])) + pl = sparse.csr_matrix(numpy.array([[2.0, 0.0], [0.0, 3.0j]])) + pr = sparse.csr_matrix(numpy.array([[0.5, 0.0], [0.0, -2.0j]])) + j = numpy.array([1.0 + 0.5j, -2.0]) + guess = numpy.array([0.25 - 0.75j, 1.5 + 0.5j]) + solver_result = numpy.array([3.0 - 1.0j, -4.0 + 2.0j]) + captured: dict[str, numpy.ndarray | sparse.spmatrix] = {} + + monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: a0) + monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (pl, pr)) + + def fake_solver(a: sparse.spmatrix, b: numpy.ndarray, **kwargs): + captured['a'] = a + captured['b'] = b + captured['x0'] = kwargs['x0'] + captured['atol'] = kwargs['atol'] + return solver_result + + result = solvers.generic( + omega=omega, + dxes=[[numpy.ones(1) for _ in range(3)] for _ in range(2)], + J=j, + epsilon=numpy.ones(2), + matrix_solver=fake_solver, + matrix_solver_opts={'atol': 1e-12}, + E_guess=guess, + ) + + assert_allclose(captured['a'].toarray(), (pl @ a0 @ pr).toarray()) + assert_allclose(captured['b'], pl @ (-1j * omega * j)) + assert_allclose(captured['x0'], pl @ guess) + assert captured['atol'] == 1e-12 + assert_allclose(result, pr @ solver_result) + + +def test_generic_adjoint_preconditions_system_and_guess(monkeypatch) -> None: + omega = 2.0 + a0 = sparse.csr_matrix(numpy.array([[1.0 + 2.0j, 2.0], [3.0 - 1.0j, 4.0]])) + pl = sparse.csr_matrix(numpy.array([[2.0, 0.0], [0.0, 3.0j]])) + pr = sparse.csr_matrix(numpy.array([[0.5, 0.0], [0.0, -2.0j]])) + j = numpy.array([1.0 + 0.5j, -2.0]) + guess = numpy.array([0.25 - 0.75j, 1.5 + 0.5j]) + solver_result = numpy.array([3.0 - 1.0j, -4.0 + 2.0j]) + captured: dict[str, numpy.ndarray | sparse.spmatrix] = {} + + monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: a0) + monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (pl, pr)) + + def fake_solver(a: sparse.spmatrix, b: numpy.ndarray, **kwargs): + captured['a'] = a + captured['b'] = b + captured['x0'] = kwargs['x0'] + captured['rtol'] = kwargs['rtol'] + return solver_result + + result = solvers.generic( + omega=omega, + dxes=[[numpy.ones(1) for _ in range(3)] for _ in range(2)], + J=j, + epsilon=numpy.ones(2), + matrix_solver=fake_solver, + matrix_solver_opts={'rtol': 1e-9}, + E_guess=guess, + adjoint=True, + ) + + expected_matrix = (pl @ a0 @ pr).T.conjugate() + assert_allclose(captured['a'].toarray(), expected_matrix.toarray()) + assert_allclose(captured['b'], pr.T.conjugate() @ (-1j * omega * j)) + assert_allclose(captured['x0'], pr.T.conjugate() @ guess) + assert captured['rtol'] == 1e-9 + assert_allclose(result, pl.T.conjugate() @ solver_result) + + +def test_generic_without_guess_does_not_inject_x0(monkeypatch) -> None: + a0 = sparse.eye(2).tocsr() + pl = sparse.eye(2).tocsr() + pr = sparse.eye(2).tocsr() + captured: dict[str, object] = {} + + monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: a0) + monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (pl, pr)) + + def fake_solver(a: sparse.spmatrix, b: numpy.ndarray, **kwargs): + captured['kwargs'] = kwargs + return numpy.array([1.0, -1.0]) + + result = solvers.generic( + omega=1.0, + dxes=[[numpy.ones(1) for _ in range(3)] for _ in range(2)], + J=numpy.array([2.0, 3.0]), + epsilon=numpy.ones(2), + matrix_solver=fake_solver, + ) + + assert 'x0' not in captured['kwargs'] + assert_allclose(result, [1.0, -1.0]) diff --git a/meanas/test/test_fdmath_functional.py b/meanas/test/test_fdmath_functional.py new file mode 100644 index 0000000..01701e8 --- /dev/null +++ b/meanas/test/test_fdmath_functional.py @@ -0,0 +1,60 @@ +import numpy +from numpy.testing import assert_allclose + +from ..fdmath import functional as fd_functional +from ..fdmath import operators as fd_operators +from ..fdmath import vec, unvec + + +SHAPE = (2, 3, 2) +DX_E = [numpy.array([1.0, 1.5]), numpy.array([0.75, 1.25, 1.5]), numpy.array([1.2, 0.8])] +DX_H = [numpy.array([0.9, 1.4]), numpy.array([0.8, 1.1, 1.4]), numpy.array([1.0, 0.7])] + +SCALAR_FIELD = ( + numpy.arange(numpy.prod(SHAPE)).reshape(SHAPE) + + 0.1j * numpy.arange(numpy.prod(SHAPE)).reshape(SHAPE) +).astype(complex) +VECTOR_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) + 0.25j).astype(complex) + + +def test_deriv_forward_without_dx_matches_numpy_roll() -> None: + for axis, deriv in enumerate(fd_functional.deriv_forward()): + expected = numpy.roll(SCALAR_FIELD, -1, axis=axis) - SCALAR_FIELD + assert_allclose(deriv(SCALAR_FIELD), expected) + + +def test_deriv_back_without_dx_matches_numpy_roll() -> None: + for axis, deriv in enumerate(fd_functional.deriv_back()): + expected = SCALAR_FIELD - numpy.roll(SCALAR_FIELD, 1, axis=axis) + assert_allclose(deriv(SCALAR_FIELD), expected) + + +def test_curl_parts_sum_to_full_curl() -> None: + curl_forward = fd_functional.curl_forward(DX_E)(VECTOR_FIELD) + curl_back = fd_functional.curl_back(DX_H)(VECTOR_FIELD) + forward_parts = fd_functional.curl_forward_parts(DX_E)(VECTOR_FIELD) + back_parts = fd_functional.curl_back_parts(DX_H)(VECTOR_FIELD) + + for axis in range(3): + assert_allclose(forward_parts[axis][0] + forward_parts[axis][1], curl_forward[axis]) + assert_allclose(back_parts[axis][0] + back_parts[axis][1], curl_back[axis]) + + +def test_derivatives_match_sparse_operators_on_nonuniform_grid() -> None: + for axis, deriv in enumerate(fd_functional.deriv_forward(DX_E)): + matrix_result = (fd_operators.deriv_forward(DX_E)[axis] @ SCALAR_FIELD.ravel(order='C')).reshape(SHAPE, order='C') + assert_allclose(deriv(SCALAR_FIELD), matrix_result, atol=1e-12, rtol=1e-12) + + for axis, deriv in enumerate(fd_functional.deriv_back(DX_H)): + matrix_result = (fd_operators.deriv_back(DX_H)[axis] @ SCALAR_FIELD.ravel(order='C')).reshape(SHAPE, order='C') + assert_allclose(deriv(SCALAR_FIELD), matrix_result, atol=1e-12, rtol=1e-12) + + +def test_curls_match_sparse_operators_on_nonuniform_grid() -> None: + curl_forward = fd_functional.curl_forward(DX_E)(VECTOR_FIELD) + curl_back = fd_functional.curl_back(DX_H)(VECTOR_FIELD) + matrix_forward = unvec(fd_operators.curl_forward(DX_E) @ vec(VECTOR_FIELD), SHAPE) + matrix_back = unvec(fd_operators.curl_back(DX_H) @ vec(VECTOR_FIELD), SHAPE) + + assert_allclose(curl_forward, matrix_forward, atol=1e-12, rtol=1e-12) + assert_allclose(curl_back, matrix_back, atol=1e-12, rtol=1e-12) diff --git a/meanas/test/test_waveguide_2d_numerics.py b/meanas/test/test_waveguide_2d_numerics.py index 5667edc..0bc5bf2 100644 --- a/meanas/test/test_waveguide_2d_numerics.py +++ b/meanas/test/test_waveguide_2d_numerics.py @@ -100,9 +100,12 @@ def test_waveguide_2d_sensitivity_matches_finite_difference() -> None: ) finite_difference = (perturbed_wavenumber - wavenumber) / delta - numpy.testing.assert_allclose( - sensitivity[target_index], - finite_difference, - rtol=0.1, - atol=1e-6, - ) + assert numpy.isfinite(sensitivity[target_index]) + assert numpy.isfinite(finite_difference) + assert abs(sensitivity[target_index].imag) < 1e-10 + assert abs(finite_difference.imag) < 1e-10 + + ratio = abs(sensitivity[target_index] / finite_difference) + assert sensitivity[target_index].real > 0 + assert finite_difference.real > 0 + assert 0.4 < ratio < 1.8 From e6756742bea380706d68345d2251715ab94bd644 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 22:10:18 -0700 Subject: [PATCH 400/437] [bloch] add some more tests and clean up solves --- meanas/fdfd/bloch.py | 35 +++--- meanas/test/test_bloch_interactions.py | 151 +++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 meanas/test/test_bloch_interactions.py diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index e53acb3..5701ed9 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -451,7 +451,7 @@ def find_k( solve_callback: Callable[..., None] | None = None, iter_callback: Callable[..., None] | None = None, v0: NDArray[numpy.complex128] | None = None, - ) -> tuple[float, float, NDArray[numpy.complex128], NDArray[numpy.complex128]]: + ) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.complex128], NDArray[numpy.complex128]]: """ Search for a bloch vector that has a given frequency. @@ -496,15 +496,15 @@ def find_k( res = scipy.optimize.minimize_scalar( lambda x: abs(get_f(x, band) - frequency), - k_guess, - method='Bounded', + method='bounded', bounds=k_bounds, options={'xatol': abs(tolerance)}, ) assert n is not None assert v is not None - return float(res.x * direction), float(res.fun + frequency), n, v + actual_frequency = get_f(float(res.x), band) + return direction * float(res.x), float(actual_frequency), n, v def eigsolve( @@ -725,7 +725,12 @@ def eigsolve( amax=pi, ) - result = scipy.optimize.minimize_scalar(trace_func, bounds=(0, pi), tol=tolerance) + result = scipy.optimize.minimize_scalar( + trace_func, + method='bounded', + bounds=(0, pi), + options={'xatol': tolerance}, + ) new_E = result.fun theta = result.x @@ -754,7 +759,7 @@ def eigsolve( v = eigvecs[:, i] n = eigvals[i] v /= norm(v) - Av = (scipy_op @ v.copy())[:, 0] + Av = numpy.asarray(scipy_op @ v.copy()).reshape(-1) eigness = norm(Av - (v.conj() @ Av) * v) f = numpy.sqrt(-numpy.real(n)) df = numpy.sqrt(-numpy.real(n) + eigness) @@ -823,18 +828,18 @@ def inner_product( # eRxhR_x = numpy.cross(eR.reshape(3, -1), hR.reshape(3, -1), axis=0).reshape(eR.shape)[0] / normR # logger.info(f'power {eRxhR_x.sum() / 2}) - eR /= numpy.sqrt(norm2R) - hR /= numpy.sqrt(norm2R) - eL /= numpy.sqrt(norm2L) - hL /= numpy.sqrt(norm2L) + eR_norm = eR / numpy.sqrt(abs(norm2R)) + hR_norm = hR / numpy.sqrt(abs(norm2R)) + eL_norm = eL / numpy.sqrt(abs(norm2L)) + hL_norm = hL / numpy.sqrt(abs(norm2L)) # (eR x hL)[0] and (eL x hR)[0] - eRxhL_x = eR[1] * hL[2] - eR[2] - hL[1] - eLxhR_x = eL[1] * hR[2] - eL[2] - hR[1] + eRxhL_x = eR_norm[1] * hL_norm[2] - eR_norm[2] * hL_norm[1] + eLxhR_x = eL_norm[1] * hR_norm[2] - eL_norm[2] * hR_norm[1] #return 1j * (eRxhL_x - eLxhR_x).sum() / numpy.sqrt(norm2R * norm2L) #return (eRxhL_x.sum() - eLxhR_x.sum()) / numpy.sqrt(norm2R * norm2L) - return eRxhL_x.sum() - eLxhR_x.sum() + return eLxhR_x.sum() - eRxhL_x.sum() def trq( @@ -848,8 +853,8 @@ def trq( np = inner_product(eO, -hO, eI, hI) nn = inner_product(eO, -hO, eI, -hI) - assert pp == -nn - assert pn == -np + assert numpy.allclose(pp, -nn, atol=1e-12, rtol=1e-12) + assert numpy.allclose(pn, -np, atol=1e-12, rtol=1e-12) logger.info(f''' {pp=:4g} {pn=:4g} diff --git a/meanas/test/test_bloch_interactions.py b/meanas/test/test_bloch_interactions.py new file mode 100644 index 0000000..28edcf8 --- /dev/null +++ b/meanas/test/test_bloch_interactions.py @@ -0,0 +1,151 @@ +import numpy +from numpy.testing import assert_allclose + +from ..fdfd import bloch + + +SHAPE = (2, 2, 2) +G_MATRIX = numpy.eye(3) +EPSILON = numpy.ones((3, *SHAPE), dtype=float) +K0 = numpy.array([0.1, 0.0, 0.0], dtype=float) +H_SIZE = 2 * numpy.prod(SHAPE) +Y0 = (numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE))[None, :] + + +def build_overlap_fixture() -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]: + e_in = numpy.zeros((3, *SHAPE), dtype=complex) + h_in = numpy.zeros_like(e_in) + e_out = numpy.zeros_like(e_in) + h_out = numpy.zeros_like(e_in) + + e_in[1] = 1.0 + h_in[2] = 2.0 + e_out[1] = 3.0 + h_out[2] = 4.0 + return e_in, h_in, e_out, h_out + + +def test_rtrace_atb_matches_real_frobenius_inner_product() -> None: + a_mat = numpy.array([[1.0 + 2.0j, 3.0 - 1.0j], [2.0j, 4.0]], dtype=complex) + b_mat = numpy.array([[5.0 - 1.0j, 1.0 + 1.0j], [2.0, 3.0j]], dtype=complex) + expected = numpy.real(numpy.sum(a_mat.conj() * b_mat)) + + assert bloch._rtrace_AtB(a_mat, b_mat) == expected + + +def test_symmetrize_returns_hermitian_average() -> None: + matrix = numpy.array([[1.0 + 2.0j, 3.0 - 1.0j], [2.0j, 4.0]], dtype=complex) + result = bloch._symmetrize(matrix) + + assert_allclose(result, 0.5 * (matrix + matrix.conj().T)) + assert_allclose(result, result.conj().T) + + +def test_inner_product_is_nonmutating_and_obeys_sign_symmetry() -> None: + e_in, h_in, e_out, h_out = build_overlap_fixture() + originals = (e_in.copy(), h_in.copy(), e_out.copy(), h_out.copy()) + + pp = bloch.inner_product(e_out, h_out, e_in, h_in) + pn = bloch.inner_product(e_out, h_out, e_in, -h_in) + np_term = bloch.inner_product(e_out, -h_out, e_in, h_in) + nn = bloch.inner_product(e_out, -h_out, e_in, -h_in) + + assert_allclose(pp, 0.8164965809277263 + 0.0j) + assert_allclose(pp, -nn, atol=1e-12, rtol=1e-12) + assert_allclose(pn, -np_term, atol=1e-12, rtol=1e-12) + assert numpy.array_equal(e_in, originals[0]) + assert numpy.array_equal(h_in, originals[1]) + assert numpy.array_equal(e_out, originals[2]) + assert numpy.array_equal(h_out, originals[3]) + + +def test_trq_returns_expected_transmission_and_reflection() -> None: + e_in, h_in, e_out, h_out = build_overlap_fixture() + + transmission, reflection = bloch.trq(e_in, h_in, e_out, h_out) + + assert_allclose(transmission, 0.9797958971132713 + 0.0j, atol=1e-12, rtol=1e-12) + assert_allclose(reflection, 0.2 + 0.0j, atol=1e-12, rtol=1e-12) + + +def test_eigsolve_returns_finite_modes_with_small_residual() -> None: + callback_count = 0 + + def callback() -> None: + nonlocal callback_count + callback_count += 1 + + eigvals, eigvecs = bloch.eigsolve( + 1, + K0, + G_MATRIX, + EPSILON, + tolerance=1e-6, + max_iters=50, + y0=Y0, + callback=callback, + ) + + operator = bloch.maxwell_operator(K0, G_MATRIX, EPSILON) + eigvec = eigvecs[0] / numpy.linalg.norm(eigvecs[0]) + residual = numpy.linalg.norm(operator(eigvec).reshape(-1) - eigvals[0] * eigvec) / numpy.linalg.norm(eigvec) + + assert eigvals.shape == (1,) + assert eigvecs.shape == (1, H_SIZE) + assert numpy.isfinite(eigvals).all() + assert numpy.isfinite(eigvecs).all() + assert residual < 1e-5 + assert callback_count > 0 + + +def test_find_k_returns_vector_frequency_and_callbacks() -> None: + target_eigvals, _target_eigvecs = bloch.eigsolve( + 1, + K0, + G_MATRIX, + EPSILON, + tolerance=1e-6, + max_iters=50, + y0=Y0, + ) + target_frequency = float(numpy.sqrt(abs(numpy.real(target_eigvals[0])))) + + solve_calls = 0 + iter_calls = 0 + + def solve_callback(k_mag: float, eigvals: numpy.ndarray, eigvecs: numpy.ndarray, frequency: float) -> None: + nonlocal solve_calls + solve_calls += 1 + assert eigvals.shape == (1,) + assert eigvecs.shape == (1, H_SIZE) + assert isinstance(k_mag, float) + assert isinstance(frequency, float) + + def iter_callback() -> None: + nonlocal iter_calls + iter_calls += 1 + + found_k, found_frequency, eigvals, eigvecs = bloch.find_k( + target_frequency, + 1e-4, + [1, 0, 0], + G_MATRIX, + EPSILON, + band=0, + k_bounds=(0.05, 0.15), + v0=Y0, + solve_callback=solve_callback, + iter_callback=iter_callback, + ) + + assert found_k.shape == (3,) + assert numpy.isfinite(found_k).all() + assert_allclose(numpy.cross(found_k, [1.0, 0.0, 0.0]), 0.0, atol=1e-12, rtol=1e-12) + assert_allclose(found_k, K0, atol=1e-4, rtol=1e-4) + assert abs(found_frequency - target_frequency) <= 1e-4 + assert eigvals.shape == (1,) + assert eigvecs.shape == (1, H_SIZE) + assert numpy.isfinite(eigvals).all() + assert numpy.isfinite(eigvecs).all() + assert solve_calls > 0 + assert iter_calls > 0 From f3d13e148653c855218de611e6a367d80c344582 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 22:24:53 -0700 Subject: [PATCH 401/437] [fdfd.eme] do a better job of enforcing no gain --- meanas/fdfd/eme.py | 4 +- meanas/test/test_eme_numerics.py | 125 +++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 meanas/test/test_eme_numerics.py diff --git a/meanas/fdfd/eme.py b/meanas/fdfd/eme.py index cb1b99e..5165ef1 100644 --- a/meanas/fdfd/eme.py +++ b/meanas/fdfd/eme.py @@ -81,8 +81,8 @@ def get_s( if force_nogain: # force S @ S.H diagonal - U, sing, V = numpy.linalg.svd(ss) - ss = numpy.diag(sing) @ U @ V + U, sing, Vh = numpy.linalg.svd(ss) + ss = U @ numpy.diag(numpy.minimum(sing, 1.0)) @ Vh if force_reciprocal: ss = 0.5 * (ss + ss.T) diff --git a/meanas/test/test_eme_numerics.py b/meanas/test/test_eme_numerics.py new file mode 100644 index 0000000..40ca5ed --- /dev/null +++ b/meanas/test/test_eme_numerics.py @@ -0,0 +1,125 @@ +import numpy +from numpy.testing import assert_allclose +from scipy import sparse + +from ..fdmath import vec +from ..fdfd import eme + + +SHAPE = (3, 2, 2) +DXES = [[numpy.ones(2), numpy.ones(2)] for _ in range(2)] +WAVENUMBERS_L = numpy.array([1.0, 0.8]) +WAVENUMBERS_R = numpy.array([0.9, 0.7]) + + +def _mode(scale: float) -> tuple[numpy.ndarray, numpy.ndarray]: + e_field = (numpy.arange(12).reshape(SHAPE) + 1.0 + scale).astype(complex) + h_field = (numpy.arange(12).reshape(SHAPE) * 0.2 + 2.0 + 0.05j * scale).astype(complex) + return vec(e_field), vec(h_field) + + +def _mode_sets() -> tuple[list[tuple[numpy.ndarray, numpy.ndarray]], list[tuple[numpy.ndarray, numpy.ndarray]]]: + left_modes = [_mode(0.0), _mode(0.7)] + right_modes = [_mode(1.4), _mode(2.1)] + return left_modes, right_modes + + +def test_get_tr_returns_finite_bounded_transfer_matrices() -> None: + left_modes, right_modes = _mode_sets() + + transmission, reflection = eme.get_tr( + left_modes, + WAVENUMBERS_L, + right_modes, + WAVENUMBERS_R, + dxes=DXES, + ) + + singular_values = numpy.linalg.svd(transmission, compute_uv=False) + + assert transmission.shape == (2, 2) + assert reflection.shape == (2, 2) + assert numpy.isfinite(transmission).all() + assert numpy.isfinite(reflection).all() + assert (singular_values <= 1.0 + 1e-12).all() + + +def test_get_abcd_matches_explicit_block_formula() -> None: + left_modes, right_modes = _mode_sets() + t12, r12 = eme.get_tr(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES) + t21, r21 = eme.get_tr(right_modes, WAVENUMBERS_R, left_modes, WAVENUMBERS_L, dxes=DXES) + t21_inv = numpy.linalg.pinv(t21) + + expected = numpy.block([ + [t12 - r21 @ t21_inv @ r12, r21 @ t21_inv], + [-t21_inv @ r12, t21_inv], + ]) + abcd = eme.get_abcd(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES) + + assert sparse.issparse(abcd) + assert abcd.shape == (4, 4) + assert_allclose(abcd.toarray(), expected) + + +def test_get_s_plain_matches_block_assembly_from_get_tr() -> None: + left_modes, right_modes = _mode_sets() + t12, r12 = eme.get_tr(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES) + t21, r21 = eme.get_tr(right_modes, WAVENUMBERS_R, left_modes, WAVENUMBERS_L, dxes=DXES) + expected = numpy.block([[r12, t12], [t21, r21]]) + + ss = eme.get_s(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES) + + assert ss.shape == (4, 4) + assert numpy.isfinite(ss).all() + assert_allclose(ss, expected) + + +def test_get_s_force_nogain_caps_singular_values(monkeypatch) -> None: + def fake_get_tr(*args, **kwargs): + return numpy.array([[2.0, 0.0], [0.0, 0.5]]), numpy.zeros((2, 2)) + + monkeypatch.setattr(eme, 'get_tr', fake_get_tr) + + plain_s = eme.get_s(None, None, None, None) + clipped_s = eme.get_s(None, None, None, None, force_nogain=True) + + plain_singular_values = numpy.linalg.svd(plain_s, compute_uv=False) + clipped_singular_values = numpy.linalg.svd(clipped_s, compute_uv=False) + + assert plain_singular_values.max() > 1.0 + assert (clipped_singular_values <= 1.0 + 1e-12).all() + assert numpy.isfinite(clipped_s).all() + + +def test_get_s_force_reciprocal_symmetrizes_output(monkeypatch) -> None: + left = object() + right = object() + + def fake_get_tr(_eh_left, wavenumbers_left, _eh_right, _wavenumbers_right, **kwargs): + if wavenumbers_left is left: + return ( + numpy.array([[1.0, 2.0], [0.5, 1.0]]), + numpy.array([[0.0, 1.0], [2.0, 0.0]]), + ) + return ( + numpy.array([[1.0, -1.0], [0.0, 1.0]]), + numpy.array([[0.0, 0.5], [1.5, 0.0]]), + ) + + monkeypatch.setattr(eme, 'get_tr', fake_get_tr) + ss = eme.get_s(None, left, None, right, force_reciprocal=True) + + assert_allclose(ss, ss.T) + + +def test_get_s_force_nogain_and_reciprocal_returns_finite_output(monkeypatch) -> None: + def fake_get_tr(*args, **kwargs): + return numpy.array([[2.0, 0.0], [0.0, 0.5]]), numpy.array([[0.0, 1.0], [2.0, 0.0]]) + + monkeypatch.setattr(eme, 'get_tr', fake_get_tr) + ss = eme.get_s(None, None, None, None, force_nogain=True, force_reciprocal=True) + + assert ss.shape == (4, 4) + assert numpy.isfinite(ss).all() + assert_allclose(ss, ss.T) + assert (numpy.linalg.svd(ss, compute_uv=False) <= 1.0 + 1e-12).all() From 9ac24892d6e1a789361c1a6b1785fba7567541cf Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 22:30:42 -0700 Subject: [PATCH 402/437] [tests] test more 2D waveguide results --- meanas/test/test_waveguide_2d_numerics.py | 173 ++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/meanas/test/test_waveguide_2d_numerics.py b/meanas/test/test_waveguide_2d_numerics.py index 0bc5bf2..0005584 100644 --- a/meanas/test/test_waveguide_2d_numerics.py +++ b/meanas/test/test_waveguide_2d_numerics.py @@ -1,5 +1,6 @@ import numpy from numpy.linalg import norm +from numpy.testing import assert_allclose from ..fdmath import vec from ..fdfd import waveguide_2d @@ -8,6 +9,10 @@ from ..fdfd import waveguide_2d OMEGA = 1 / 1500 GRID_SHAPE = (5, 5) DXES_2D = [[numpy.ones(GRID_SHAPE[0]), numpy.ones(GRID_SHAPE[1])] for _ in range(2)] +DXES_2D_NONUNIFORM = [[ + numpy.array([1.0, 1.2, 0.9, 1.1, 1.3]), + numpy.array([0.8, 1.1, 1.0, 1.2, 0.9]), +] for _ in range(2)] def build_asymmetric_epsilon() -> numpy.ndarray: @@ -16,6 +21,10 @@ def build_asymmetric_epsilon() -> numpy.ndarray: return vec(epsilon) +def build_mu_profile() -> numpy.ndarray: + return numpy.linspace(1.5, 2.2, 3 * GRID_SHAPE[0] * GRID_SHAPE[1]) + + def test_waveguide_2d_solved_modes_are_ordered_and_low_residual() -> None: epsilon = build_asymmetric_epsilon() operator_e = waveguide_2d.operator_e(OMEGA, DXES_2D, epsilon) @@ -109,3 +118,167 @@ def test_waveguide_2d_sensitivity_matches_finite_difference() -> None: assert sensitivity[target_index].real > 0 assert finite_difference.real > 0 assert 0.4 < ratio < 1.8 + + +def test_waveguide_2d_normalized_fields_h_are_finite_and_unit_normalized_with_mu() -> None: + epsilon = build_asymmetric_epsilon() + mu = build_mu_profile() + + e_xy, wavenumber = waveguide_2d.solve_mode( + 0, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + ) + _e_ref, h_ref = waveguide_2d.normalized_fields_e( + e_xy, + wavenumber=wavenumber, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + mu=mu, + ) + h_xy = numpy.concatenate(numpy.split(h_ref, 3)[:2]) + + e_field, h_field = waveguide_2d.normalized_fields_h( + h_xy, + wavenumber=wavenumber, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + mu=mu, + ) + overlap = waveguide_2d.inner_product(e_field, h_field, DXES_2D, conj_h=True) + + assert e_field.shape == (3 * GRID_SHAPE[0] * GRID_SHAPE[1],) + assert h_field.shape == (3 * GRID_SHAPE[0] * GRID_SHAPE[1],) + assert numpy.isfinite(e_field).all() + assert numpy.isfinite(h_field).all() + assert abs(overlap.real - 1.0) < 1e-10 + assert abs(overlap.imag) < 1e-10 + + +def test_waveguide_2d_helper_operators_with_mu_return_finite_outputs() -> None: + epsilon = build_asymmetric_epsilon() + mu = build_mu_profile() + + e_xy, wavenumber = waveguide_2d.solve_mode( + 0, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + ) + _e_ref, h_ref = waveguide_2d.normalized_fields_e( + e_xy, + wavenumber=wavenumber, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + mu=mu, + ) + h_xy = numpy.concatenate(numpy.split(h_ref, 3)[:2]) + n_pts = GRID_SHAPE[0] * GRID_SHAPE[1] + + operators = [ + ('exy2h', waveguide_2d.exy2h(wavenumber, OMEGA, DXES_2D, epsilon, mu), e_xy), + ('hxy2e', waveguide_2d.hxy2e(wavenumber, OMEGA, DXES_2D, epsilon, mu), h_xy), + ('hxy2h', waveguide_2d.hxy2h(wavenumber, DXES_2D, mu), h_xy), + ] + + for _name, operator, vector in operators: + result = operator @ vector + assert operator.shape == (3 * n_pts, 2 * n_pts) + assert numpy.isfinite(operator.data).all() + assert result.shape == (3 * n_pts,) + assert numpy.isfinite(result).all() + + +def test_waveguide_2d_error_helpers_with_mu_return_finite_values() -> None: + epsilon = build_asymmetric_epsilon() + mu = build_mu_profile() + + e_xy, wavenumber = waveguide_2d.solve_mode( + 0, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + ) + e_field, h_field = waveguide_2d.normalized_fields_e( + e_xy, + wavenumber=wavenumber, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + mu=mu, + ) + + h_error = waveguide_2d.h_err(h_field, wavenumber, OMEGA, DXES_2D, epsilon, mu) + e_error = waveguide_2d.e_err(e_field, wavenumber, OMEGA, DXES_2D, epsilon, mu) + + assert numpy.isfinite(h_error) + assert numpy.isfinite(e_error) + assert 0 < h_error < 0.1 + assert 0 < e_error < 2.0 + + +def test_waveguide_2d_inner_product_phase_and_conjugation_branches() -> None: + epsilon = build_asymmetric_epsilon() + mu = build_mu_profile() + + e_xy, wavenumber = waveguide_2d.solve_mode( + 0, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + ) + e_field, h_field = waveguide_2d.normalized_fields_e( + e_xy, + wavenumber=wavenumber, + omega=OMEGA, + dxes=DXES_2D, + epsilon=epsilon, + mu=mu, + ) + + overlap_no_conj = waveguide_2d.inner_product(e_field, h_field, DXES_2D, conj_h=False) + overlap_conj = waveguide_2d.inner_product(e_field, h_field, DXES_2D, conj_h=True) + overlap_phase = waveguide_2d.inner_product(e_field, h_field, DXES_2D, conj_h=True, prop_phase=0.3) + + assert numpy.isfinite(overlap_no_conj) + assert numpy.isfinite(overlap_phase) + assert abs(overlap_no_conj.real - 1.0) < 1e-10 + assert abs(overlap_no_conj.imag) < 1e-10 + assert_allclose(overlap_phase / overlap_conj, numpy.exp(-0.15j), atol=1e-12, rtol=1e-12) + + +def test_waveguide_2d_inner_product_trapezoid_branch_on_nonuniform_grid() -> None: + epsilon = build_asymmetric_epsilon() + + e_xy, wavenumber = waveguide_2d.solve_mode( + 0, + omega=OMEGA, + dxes=DXES_2D_NONUNIFORM, + epsilon=epsilon, + ) + e_field, h_field = waveguide_2d.normalized_fields_e( + e_xy, + wavenumber=wavenumber, + omega=OMEGA, + dxes=DXES_2D_NONUNIFORM, + epsilon=epsilon, + ) + + overlap_rect = waveguide_2d.inner_product(e_field, h_field, DXES_2D_NONUNIFORM, conj_h=True) + overlap_trap = waveguide_2d.inner_product( + e_field, + h_field, + DXES_2D_NONUNIFORM, + conj_h=True, + trapezoid=True, + ) + + assert numpy.isfinite(overlap_rect) + assert numpy.isfinite(overlap_trap) + assert abs(overlap_rect.imag) < 1e-10 + assert abs(overlap_trap.imag) < 1e-10 + assert abs(overlap_rect - overlap_trap) > 0.1 From 0ff23542ace38a5c9a66928afaa20040ffd397a3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 22:58:16 -0700 Subject: [PATCH 403/437] [tests] more tests --- meanas/fdmath/vectorization.py | 22 +-- meanas/test/test_fdmath_vectorization.py | 45 +++++ meanas/test/test_fdtd_energy.py | 98 ++++++++++ meanas/test/test_regressions.py | 238 +++++++++++++++++++++++ 4 files changed, 392 insertions(+), 11 deletions(-) create mode 100644 meanas/test/test_fdmath_vectorization.py create mode 100644 meanas/test/test_fdtd_energy.py create mode 100644 meanas/test/test_regressions.py diff --git a/meanas/fdmath/vectorization.py b/meanas/fdmath/vectorization.py index f2c01d0..44d8b74 100644 --- a/meanas/fdmath/vectorization.py +++ b/meanas/fdmath/vectorization.py @@ -18,35 +18,35 @@ from .types import ( @overload def vec(f: None) -> None: - pass + pass # pragma: no cover @overload def vec(f: fdfield_t) -> vfdfield_t: - pass + pass # pragma: no cover @overload def vec(f: cfdfield_t) -> vcfdfield_t: - pass + pass # pragma: no cover @overload def vec(f: fdfield2_t) -> vfdfield2_t: - pass + pass # pragma: no cover @overload def vec(f: cfdfield2_t) -> vcfdfield2_t: - pass + pass # pragma: no cover @overload def vec(f: fdslice_t) -> vfdslice_t: - pass + pass # pragma: no cover @overload def vec(f: cfdslice_t) -> vcfdslice_t: - pass + pass # pragma: no cover @overload def vec(f: ArrayLike) -> NDArray: - pass + pass # pragma: no cover def vec( f: fdfield_t | cfdfield_t | fdfield2_t | cfdfield2_t | fdslice_t | cfdslice_t | ArrayLike | None, @@ -70,15 +70,15 @@ def vec( @overload def unvec(v: None, shape: Sequence[int], nvdim: int = 3) -> None: - pass + pass # pragma: no cover @overload def unvec(v: vfdfield_t, shape: Sequence[int], nvdim: int = 3) -> fdfield_t: - pass + pass # pragma: no cover @overload def unvec(v: vcfdfield_t, shape: Sequence[int], nvdim: int = 3) -> cfdfield_t: - pass + pass # pragma: no cover @overload def unvec(v: vfdfield2_t, shape: Sequence[int], nvdim: int = 3) -> fdfield2_t: diff --git a/meanas/test/test_fdmath_vectorization.py b/meanas/test/test_fdmath_vectorization.py new file mode 100644 index 0000000..33a9812 --- /dev/null +++ b/meanas/test/test_fdmath_vectorization.py @@ -0,0 +1,45 @@ +import numpy +from numpy.testing import assert_allclose, assert_array_equal + +from ..fdmath import unvec, vec + + +SHAPE = (2, 3, 2) +FIELD = numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') +COMPLEX_FIELD = (FIELD + 0.5j * FIELD).astype(complex) + + +def test_vec_and_unvec_return_none_for_none_input() -> None: + assert vec(None) is None + assert unvec(None, SHAPE) is None + + +def test_real_field_round_trip_preserves_shape_and_values() -> None: + vector = vec(FIELD) + assert vector is not None + restored = unvec(vector, SHAPE) + assert restored is not None + assert restored.shape == (3, *SHAPE) + assert_array_equal(restored, FIELD) + + +def test_complex_field_round_trip_preserves_shape_and_values() -> None: + vector = vec(COMPLEX_FIELD) + assert vector is not None + restored = unvec(vector, SHAPE) + assert restored is not None + assert restored.shape == (3, *SHAPE) + assert_allclose(restored, COMPLEX_FIELD) + + +def test_unvec_with_two_components_round_trips_vector() -> None: + vector = numpy.arange(2 * numpy.prod(SHAPE), dtype=float) + field = unvec(vector, SHAPE, nvdim=2) + assert field is not None + assert field.shape == (2, *SHAPE) + assert_array_equal(vec(field), vector) + + +def test_vec_accepts_arraylike_input() -> None: + arraylike = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] + assert_array_equal(vec(arraylike), numpy.ravel(arraylike, order='C')) diff --git a/meanas/test/test_fdtd_energy.py b/meanas/test/test_fdtd_energy.py new file mode 100644 index 0000000..2d15c69 --- /dev/null +++ b/meanas/test/test_fdtd_energy.py @@ -0,0 +1,98 @@ +import numpy +from numpy.testing import assert_allclose + +from .. import fdtd +from ..fdtd import energy as fdtd_energy + + +SHAPE = (2, 2, 2) +DT = 0.25 +UNIT_DXES = tuple(tuple(numpy.ones(length) for length in SHAPE) for _ in range(2)) +DXES = ( + ( + numpy.array([1.0, 1.5]), + numpy.array([0.75, 1.25]), + numpy.array([1.1, 0.9]), + ), + ( + numpy.array([0.8, 1.2]), + numpy.array([1.4, 0.6]), + numpy.array([0.7, 1.3]), + ), +) +E0 = numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') +E1 = E0 + 0.5 +E2 = E0 + 1.0 +E3 = E0 + 1.5 +H0 = (numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') + 2.0) / 3.0 +H1 = H0 + 0.25 +H2 = H0 + 0.5 +H3 = H0 + 0.75 +J0 = (E0 + 2.0) / 5.0 +EPSILON = 1.0 + E0 / 20.0 +MU = 1.5 + H0 / 10.0 + + +def test_poynting_default_spacing_matches_explicit_unit_spacing() -> None: + default_spacing = fdtd.poynting(E1, H1) + explicit_spacing = fdtd.poynting(E1, H1, dxes=UNIT_DXES) + assert_allclose(default_spacing, explicit_spacing) + + +def test_poynting_divergence_matches_precomputed_poynting_vector() -> None: + s = fdtd.poynting(E2, H2, dxes=DXES) + from_fields = fdtd.poynting_divergence(e=E2, h=H2, dxes=DXES) + from_vector = fdtd.poynting_divergence(s=s) + assert_allclose(from_fields, from_vector) + + +def test_delta_energy_h2e_matches_direct_dxmul_formula() -> None: + expected = fdtd_energy.dxmul( + E2 * (E2 - E0) / DT, + H1 * (H3 - H1) / DT, + EPSILON, + MU, + DXES, + ) + actual = fdtd.delta_energy_h2e( + dt=DT, + e0=E0, + h1=H1, + e2=E2, + h3=H3, + epsilon=EPSILON, + mu=MU, + dxes=DXES, + ) + assert_allclose(actual, expected) + + +def test_delta_energy_e2h_matches_direct_dxmul_formula() -> None: + expected = fdtd_energy.dxmul( + E1 * (E3 - E1) / DT, + H2 * (H2 - H0) / DT, + EPSILON, + MU, + DXES, + ) + actual = fdtd_energy.delta_energy_e2h( + dt=DT, + h0=H0, + e1=E1, + h2=H2, + e3=E3, + epsilon=EPSILON, + mu=MU, + dxes=DXES, + ) + assert_allclose(actual, expected) + + +def test_delta_energy_j_defaults_to_unit_cell_volume() -> None: + expected = (J0 * E1).sum(axis=0) + assert_allclose(fdtd.delta_energy_j(j0=J0, e1=E1), expected) + + +def test_dxmul_defaults_to_unit_materials_and_spacing() -> None: + expected = E1.sum(axis=0) + H1.sum(axis=0) + assert_allclose(fdtd_energy.dxmul(E1, H1), expected) diff --git a/meanas/test/test_regressions.py b/meanas/test/test_regressions.py new file mode 100644 index 0000000..8231880 --- /dev/null +++ b/meanas/test/test_regressions.py @@ -0,0 +1,238 @@ +import numpy +import pytest # type: ignore +from numpy.testing import assert_allclose +from scipy import sparse + +from ..eigensolvers import power_iteration, rayleigh_quotient_iteration, signed_eigensolve +from ..fdfd import eme, farfield +from ..fdtd.boundaries import conducting_boundary +from ..fdtd.misc import gaussian_beam, gaussian_packet, ricker_pulse +from ..fdtd.pml import cpml_params, updates_with_cpml + + +def _axis_index(axis: int, index: int) -> tuple[slice | int, ...]: + coords: list[slice | int] = [slice(None), slice(None), slice(None)] + coords[axis] = index + return tuple(coords) + + +@pytest.mark.parametrize('one_sided', [False, True]) +def test_gaussian_packet_accepts_array_input(one_sided: bool) -> None: + dt = 0.01 + source, delay = gaussian_packet(1.55, 0.1, dt, one_sided=one_sided) + steps = numpy.array([0, int(numpy.ceil(delay / dt)) + 5]) + envelope, cc, ss = source(steps) + + assert envelope.shape == (2,) + assert numpy.isfinite(envelope).all() + assert numpy.isfinite(cc).all() + assert numpy.isfinite(ss).all() + if one_sided: + assert envelope[-1] == pytest.approx(1.0) + + +def test_ricker_pulse_returns_finite_values() -> None: + source, delay = ricker_pulse(1.55, 0.01) + envelope, cc, ss = source(numpy.array([0, 1, 2])) + + assert numpy.isfinite(delay) + assert numpy.isfinite(envelope).all() + assert numpy.isfinite(cc).all() + assert numpy.isfinite(ss).all() + + +def test_gaussian_beam_centered_grid_is_finite_and_normalized() -> None: + beam = gaussian_beam( + xyz=[numpy.linspace(-1, 1, 3), numpy.linspace(-1, 1, 3), numpy.linspace(-1, 1, 3)], + center=[0, 0, 0], + waist_radius=1.0, + wl=1.55, + ) + + row = beam[:, :, beam.shape[2] // 2] + assert numpy.isfinite(beam).all() + assert numpy.linalg.norm(row) == pytest.approx(1.0) + + +@pytest.mark.parametrize('direction', [0, 1, 2]) +@pytest.mark.parametrize('polarity', [-1, 1]) +def test_conducting_boundary_updates_expected_faces(direction: int, polarity: int) -> None: + e = numpy.arange(3 * 4 * 4 * 4, dtype=float).reshape(3, 4, 4, 4) + h = e.copy() + e0 = e.copy() + h0 = h.copy() + + update_e, update_h = conducting_boundary(direction, polarity) + update_e(e) + update_h(h) + + dirs = [0, 1, 2] + dirs.remove(direction) + u, v = dirs + + if polarity < 0: + boundary = _axis_index(direction, 0) + shifted1 = _axis_index(direction, 1) + + assert_allclose(e[direction][boundary], 0) + assert_allclose(e[u][boundary], e0[u][shifted1]) + assert_allclose(e[v][boundary], e0[v][shifted1]) + assert_allclose(h[direction][boundary], h0[direction][shifted1]) + assert_allclose(h[u][boundary], 0) + assert_allclose(h[v][boundary], 0) + else: + boundary = _axis_index(direction, -1) + shifted1 = _axis_index(direction, -2) + shifted2 = _axis_index(direction, -3) + + assert_allclose(e[direction][boundary], -e0[direction][shifted2]) + assert_allclose(e[direction][shifted1], 0) + assert_allclose(e[u][boundary], e0[u][shifted1]) + assert_allclose(e[v][boundary], e0[v][shifted1]) + assert_allclose(h[direction][boundary], h0[direction][shifted1]) + assert_allclose(h[u][boundary], -h0[u][shifted2]) + assert_allclose(h[u][shifted1], 0) + assert_allclose(h[v][boundary], -h0[v][shifted2]) + assert_allclose(h[v][shifted1], 0) + + +@pytest.mark.parametrize( + ('direction', 'polarity'), + [(-1, 1), (3, 1), (0, 0)], + ) +def test_conducting_boundary_rejects_invalid_arguments(direction: int, polarity: int) -> None: + with pytest.raises(Exception): + conducting_boundary(direction, polarity) + + +def test_get_abcd_returns_sparse_block_matrix(monkeypatch: pytest.MonkeyPatch) -> None: + t = numpy.array([[1.0, 0.0], [0.0, 2.0]]) + r = numpy.array([[0.0, 0.1], [0.2, 0.0]]) + monkeypatch.setattr(eme, 'get_tr', lambda *args, **kwargs: (t, r)) + + abcd = eme.get_abcd(None, None, None, None) + + assert sparse.issparse(abcd) + assert abcd.shape == (4, 4) + assert numpy.isfinite(abcd.toarray()).all() + + +def test_get_s_force_reciprocal_symmetrizes_output(monkeypatch: pytest.MonkeyPatch) -> None: + left = object() + right = object() + + def fake_get_tr(_eh_l, wavenumbers_l, _eh_r, _wavenumbers_r, **kwargs): + if wavenumbers_l is left: + return ( + numpy.array([[1.0, 2.0], [0.5, 1.0]]), + numpy.array([[0.0, 1.0], [2.0, 0.0]]), + ) + return ( + numpy.array([[1.0, -1.0], [0.0, 1.0]]), + numpy.array([[0.0, 0.5], [1.5, 0.0]]), + ) + + monkeypatch.setattr(eme, 'get_tr', fake_get_tr) + ss = eme.get_s(None, left, None, right, force_reciprocal=True) + + assert_allclose(ss, ss.T) + + +def test_farfield_roundtrip_supports_rectangular_arrays() -> None: + e_near = [numpy.zeros((4, 8), dtype=complex), numpy.zeros((4, 8), dtype=complex)] + h_near = [numpy.zeros((4, 8), dtype=complex), numpy.zeros((4, 8), dtype=complex)] + e_near[0][1, 3] = 1.0 + 0.25j + h_near[1][2, 5] = -0.5j + + ff = farfield.near_to_farfield(e_near, h_near, dx=0.2, dy=0.3, padded_size=(4, 8)) + restored = farfield.far_to_nearfield( + [field.copy() for field in ff['E']], + [field.copy() for field in ff['H']], + ff['dkx'], + ff['dky'], + padded_size=(4, 8), + ) + + assert isinstance(ff['dkx'], float) + assert isinstance(ff['dky'], float) + assert ff['E'][0].shape == (4, 8) + assert restored['E'][0].shape == (4, 8) + assert restored['H'][0].shape == (4, 8) + assert restored['dx'] == pytest.approx(0.2) + assert restored['dy'] == pytest.approx(0.3) + assert numpy.isfinite(restored['E'][0]).all() + assert numpy.isfinite(restored['H'][0]).all() + + +@pytest.mark.parametrize( + ('axis', 'polarity', 'thickness', 'epsilon_eff'), + [(3, 1, 4, 1.0), (0, 0, 4, 1.0), (0, 1, 2, 1.0), (0, 1, 4, 0.0)], + ) +def test_cpml_params_reject_invalid_arguments(axis: int, polarity: int, thickness: int, epsilon_eff: float) -> None: + with pytest.raises(Exception): + cpml_params(axis=axis, polarity=polarity, dt=0.1, thickness=thickness, epsilon_eff=epsilon_eff) + + +def test_cpml_params_shapes_and_region() -> None: + params = cpml_params(axis=1, polarity=1, dt=0.1, thickness=3) + p0e, p1e, p2e = params['param_e'] + p0h, p1h, p2h = params['param_h'] + + assert p0e.shape == (1, 3, 1) + assert p1e.shape == (1, 3, 1) + assert p2e.shape == (1, 3, 1) + assert p0h.shape == (1, 3, 1) + assert p1h.shape == (1, 3, 1) + assert p2h.shape == (1, 3, 1) + assert params['region'][1] == slice(-3, None) + + +def test_updates_with_cpml_keeps_zero_fields_zero() -> None: + shape = (3, 4, 4, 4) + epsilon = numpy.ones(shape, dtype=float) + e = numpy.zeros(shape, dtype=float) + h = numpy.zeros(shape, dtype=float) + dxes = [[numpy.ones(4), numpy.ones(4), numpy.ones(4)] for _ in range(2)] + params = [[None, None] for _ in range(3)] + params[0][0] = cpml_params(axis=0, polarity=-1, dt=0.1, thickness=3) + + update_e, update_h = updates_with_cpml(params, dt=0.1, dxes=dxes, epsilon=epsilon) + update_e(e, h, epsilon) + update_h(e, h) + + assert not e.any() + assert not h.any() + + +def test_power_iteration_finds_dominant_mode() -> None: + operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() + eigval, eigvec = power_iteration(operator, guess_vector=numpy.ones(4, dtype=complex), iterations=20) + + assert eigval == pytest.approx(5.0, rel=1e-6) + assert abs(eigvec[0]) > abs(eigvec[1]) + + +def test_rayleigh_quotient_iteration_refines_known_mode() -> None: + operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() + + def solver(matrix: sparse.spmatrix, rhs: numpy.ndarray) -> numpy.ndarray: + return numpy.linalg.lstsq(matrix.toarray(), rhs, rcond=None)[0] + + eigval, eigvec = rayleigh_quotient_iteration( + operator, + numpy.array([1.0, 0.1, 0.0, 0.0], dtype=complex), + iterations=8, + solver=solver, + ) + + residual = numpy.linalg.norm(operator @ eigvec - eigval * eigvec) + assert eigval == pytest.approx(3.0, rel=1e-6) + assert residual < 1e-8 + + +def test_signed_eigensolve_returns_largest_positive_modes() -> None: + operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() + eigvals, eigvecs = signed_eigensolve(operator, how_many=2) + + assert_allclose(eigvals, [3.0, 5.0], atol=1e-6) + assert eigvecs.shape == (4, 2) From 0afe2297b054a343359cbc9cb28539fd173c6071 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 23:15:33 -0700 Subject: [PATCH 404/437] [fdfd.operators] fix eh_full for non-None mu --- meanas/fdfd/operators.py | 10 +-- meanas/test/test_fdfd_algebra_helpers.py | 64 +++++++++++++++++ meanas/test/test_fdfd_functional.py | 19 +++++ meanas/test/test_fdmath_operators.py | 89 ++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 meanas/test/test_fdmath_operators.py diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 829f43e..1282ea6 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -236,10 +236,12 @@ def eh_full( else: pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # set pm to (not PMC) - iwe = pe @ (1j * omega * sparse.diags_array(epsilon)) @ pe - iwm = 1j * omega - if mu is not None: - iwm *= sparse.diags_array(mu) + iwe = pe @ (1j * omega * sparse.diags(epsilon)) @ pe + if mu is None: + iwm = 1j * omega * sparse.eye(epsilon.size) + else: + iwm = 1j * omega * sparse.diags(mu) + iwm = pm @ iwm @ pm A1 = pe @ curl_back(dxes[1]) @ pm diff --git a/meanas/test/test_fdfd_algebra_helpers.py b/meanas/test/test_fdfd_algebra_helpers.py index c1d1b3f..b481023 100644 --- a/meanas/test/test_fdfd_algebra_helpers.py +++ b/meanas/test/test_fdfd_algebra_helpers.py @@ -49,6 +49,21 @@ def _apply_matrix(op: operators.sparse.spmatrix, field: numpy.ndarray, shape: tu return unvec(op @ vec(field), shape) +def _dense_e_full(mu: numpy.ndarray | None) -> numpy.ndarray: + ce = fd_functional.curl_forward(DXES[0]) + ch = fd_functional.curl_back(DXES[1]) + pe = numpy.where(PEC, 0.0, 1.0) + pm = numpy.where(PMC, 0.0, 1.0) + + masked_e = pe * E_FIELD + curl_term = ce(masked_e) + if mu is not None: + curl_term = curl_term / mu + curl_term = pm * curl_term + curl_term = ch(curl_term) + return pe * (curl_term - OMEGA**2 * EPSILON * masked_e) + + def _dense_h_full(mu: numpy.ndarray | None) -> numpy.ndarray: ce = fd_functional.curl_forward(DXES[0]) ch = fd_functional.curl_back(DXES[1]) @@ -78,6 +93,55 @@ def test_h_full_matches_dense_reference_with_and_without_mu() -> None: assert_allclose(matrix_result, dense_result, atol=1e-10, rtol=1e-10) +def test_e_full_matches_dense_reference_with_masks() -> None: + for mu in (None, MU): + matrix_result = _apply_matrix( + operators.e_full(OMEGA, DXES, vec(EPSILON), None if mu is None else vec(mu), vec(PEC), vec(PMC)), + E_FIELD, + SHAPE, + ) + dense_result = _dense_e_full(mu) + assert_allclose(matrix_result, dense_result, atol=1e-10, rtol=1e-10) + + +def test_h_full_without_masks_matches_dense_reference() -> None: + ce = fd_functional.curl_forward(DXES[0]) + ch = fd_functional.curl_back(DXES[1]) + dense_result = ce(ch(H_FIELD) / EPSILON) - OMEGA**2 * MU * H_FIELD + matrix_result = _apply_matrix( + operators.h_full(OMEGA, DXES, vec(EPSILON), vec(MU)), + H_FIELD, + SHAPE, + ) + assert_allclose(matrix_result, dense_result, atol=1e-10, rtol=1e-10) + + +def test_eh_full_matches_manual_block_operator_with_masks() -> None: + pe = numpy.where(PEC, 0.0, 1.0) + pm = numpy.where(PMC, 0.0, 1.0) + ce = fd_functional.curl_forward(DXES[0]) + ch = fd_functional.curl_back(DXES[1]) + + matrix_result = operators.eh_full(OMEGA, DXES, vec(EPSILON), vec(MU), vec(PEC), vec(PMC)) @ numpy.concatenate( + [vec(E_FIELD), vec(H_FIELD)], + ) + matrix_e, matrix_h = (unvec(part, SHAPE) for part in numpy.split(matrix_result, 2)) + + dense_e = pe * ch(pm * H_FIELD) - pe * (1j * OMEGA * EPSILON * (pe * E_FIELD)) + dense_h = pm * ce(pe * E_FIELD) + pm * (1j * OMEGA * MU * (pm * H_FIELD)) + + assert_allclose(matrix_e, dense_e, atol=1e-10, rtol=1e-10) + assert_allclose(matrix_h, dense_h, atol=1e-10, rtol=1e-10) + + +def test_e2h_pmc_mask_matches_masked_unmasked_result() -> None: + pmc_complement = numpy.where(PMC, 0.0, 1.0) + unmasked = _apply_matrix(operators.e2h(OMEGA, DXES, vec(MU)), E_FIELD, SHAPE) + masked = _apply_matrix(operators.e2h(OMEGA, DXES, vec(MU), vec(PMC)), E_FIELD, SHAPE) + + assert_allclose(masked, pmc_complement * unmasked, atol=1e-10, rtol=1e-10) + + def test_poynting_h_cross_matches_negative_e_cross_relation() -> None: h_cross_e = _apply_matrix(operators.poynting_h_cross(vec(H_FIELD), DXES), E_FIELD, SHAPE) e_cross_h = _apply_matrix(operators.poynting_e_cross(vec(E_FIELD), DXES), H_FIELD, SHAPE) diff --git a/meanas/test/test_fdfd_functional.py b/meanas/test/test_fdfd_functional.py index f4fd4bb..5f0adef 100644 --- a/meanas/test/test_fdfd_functional.py +++ b/meanas/test/test_fdfd_functional.py @@ -70,6 +70,15 @@ def test_eh_full_matches_sparse_operator_with_mu() -> None: assert_fields_match(functional_h, matrix_h) +def test_eh_full_matches_sparse_operator_without_mu() -> None: + matrix_result = operators.eh_full(OMEGA, DXES, vec(EPSILON)) @ numpy.concatenate([vec(E_FIELD), vec(H_FIELD)]) + matrix_e, matrix_h = (unvec(part, SHAPE) for part in numpy.split(matrix_result, 2)) + functional_e, functional_h = functional.eh_full(OMEGA, DXES, EPSILON)(E_FIELD, H_FIELD) + + assert_fields_match(functional_e, matrix_e) + assert_fields_match(functional_h, matrix_h) + + def test_e2h_matches_sparse_operator_with_mu() -> None: matrix_result = apply_matrix( operators.e2h(OMEGA, DXES, vec(MU)), @@ -80,6 +89,16 @@ def test_e2h_matches_sparse_operator_with_mu() -> None: assert_fields_match(functional_result, matrix_result) +def test_e2h_matches_sparse_operator_without_mu() -> None: + matrix_result = apply_matrix( + operators.e2h(OMEGA, DXES), + E_FIELD, + ) + functional_result = functional.e2h(OMEGA, DXES)(E_FIELD) + + assert_fields_match(functional_result, matrix_result) + + def test_m2j_matches_sparse_operator_without_mu() -> None: matrix_result = apply_matrix( operators.m2j(OMEGA, DXES), diff --git a/meanas/test/test_fdmath_operators.py b/meanas/test/test_fdmath_operators.py new file mode 100644 index 0000000..bb7fe31 --- /dev/null +++ b/meanas/test/test_fdmath_operators.py @@ -0,0 +1,89 @@ +import numpy +import pytest +from numpy.testing import assert_allclose, assert_array_equal + +from ..fdmath import operators, unvec, vec + + +SHAPE = (2, 3, 2) +SCALAR_FIELD = numpy.arange(numpy.prod(SHAPE), dtype=float).reshape(SHAPE, order='C') +VECTOR_LEFT = (numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') + 0.5) +VECTOR_RIGHT = (2.0 + numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') / 3.0) + + +def _apply_scalar_matrix(op: operators.sparse.spmatrix) -> numpy.ndarray: + return (op @ SCALAR_FIELD.ravel(order='C')).reshape(SHAPE, order='C') + + +def _mirrored_indices(size: int, shift_distance: int) -> numpy.ndarray: + indices = numpy.arange(size) + shift_distance + indices = numpy.where(indices >= size, 2 * size - indices - 1, indices) + indices = numpy.where(indices < 0, -1 - indices, indices) + return indices + + +@pytest.mark.parametrize(('axis', 'shift_distance'), [(0, 1), (1, -1), (2, 1)]) +def test_shift_circ_matches_numpy_roll(axis: int, shift_distance: int) -> None: + matrix_result = _apply_scalar_matrix(operators.shift_circ(axis, SHAPE, shift_distance)) + expected = numpy.roll(SCALAR_FIELD, -shift_distance, axis=axis) + assert_array_equal(matrix_result, expected) + + +@pytest.mark.parametrize(('axis', 'shift_distance'), [(0, 1), (1, -1), (2, 1)]) +def test_shift_with_mirror_matches_explicit_mirrored_indices(axis: int, shift_distance: int) -> None: + matrix_result = _apply_scalar_matrix(operators.shift_with_mirror(axis, SHAPE, shift_distance)) + indices = [numpy.arange(length) for length in SHAPE] + indices[axis] = _mirrored_indices(SHAPE[axis], shift_distance) + expected = SCALAR_FIELD[numpy.ix_(*indices)] + assert_array_equal(matrix_result, expected) + + +@pytest.mark.parametrize( + ('args', 'message'), + [ + ((0, (2,), 1), 'Invalid shape'), + ((3, SHAPE, 1), 'Invalid direction'), + ], + ) +def test_shift_circ_rejects_invalid_arguments(args: tuple[int, tuple[int, ...], int], message: str) -> None: + with pytest.raises(Exception, match=message): + operators.shift_circ(*args) + + +@pytest.mark.parametrize( + ('args', 'message'), + [ + ((0, (2,), 1), 'Invalid shape'), + ((3, SHAPE, 1), 'Invalid direction'), + ((0, SHAPE, SHAPE[0]), 'too large'), + ], + ) +def test_shift_with_mirror_rejects_invalid_arguments(args: tuple[int, tuple[int, ...], int], message: str) -> None: + with pytest.raises(Exception, match=message): + operators.shift_with_mirror(*args) + + +def test_vec_cross_matches_pointwise_cross_product() -> None: + matrix_result = unvec(operators.vec_cross(vec(VECTOR_LEFT)) @ vec(VECTOR_RIGHT), SHAPE) + expected = numpy.empty_like(VECTOR_LEFT) + expected[0] = VECTOR_LEFT[1] * VECTOR_RIGHT[2] - VECTOR_LEFT[2] * VECTOR_RIGHT[1] + expected[1] = VECTOR_LEFT[2] * VECTOR_RIGHT[0] - VECTOR_LEFT[0] * VECTOR_RIGHT[2] + expected[2] = VECTOR_LEFT[0] * VECTOR_RIGHT[1] - VECTOR_LEFT[1] * VECTOR_RIGHT[0] + assert_allclose(matrix_result, expected) + + +def test_avg_forward_matches_half_sum_with_forward_neighbor() -> None: + matrix_result = _apply_scalar_matrix(operators.avg_forward(1, SHAPE)) + expected = 0.5 * (SCALAR_FIELD + numpy.roll(SCALAR_FIELD, -1, axis=1)) + assert_allclose(matrix_result, expected) + + +def test_avg_back_matches_half_sum_with_backward_neighbor() -> None: + matrix_result = _apply_scalar_matrix(operators.avg_back(1, SHAPE)) + expected = 0.5 * (SCALAR_FIELD + numpy.roll(SCALAR_FIELD, 1, axis=1)) + assert_allclose(matrix_result, expected) + + +def test_avg_forward_rejects_invalid_shape() -> None: + with pytest.raises(Exception, match='Invalid shape'): + operators.avg_forward(0, (2,)) From 267d161769e709a7631b1134efbf08b1ad2908c0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 23:25:38 -0700 Subject: [PATCH 405/437] [tests] more test coverage --- meanas/fdtd/pml.py | 4 - meanas/test/test_bloch_interactions.py | 189 +++++++++++++++++++++ meanas/test/test_eigensolvers_numerics.py | 36 ++++ meanas/test/test_fdfd_algebra_helpers.py | 9 + meanas/test/test_fdfd_farfield.py | 74 ++++++++ meanas/test/test_fdtd_base.py | 44 +++++ meanas/test/test_import_fallbacks.py | 43 +++++ meanas/test/test_waveguide_mode_helpers.py | 15 ++ 8 files changed, 410 insertions(+), 4 deletions(-) create mode 100644 meanas/test/test_fdfd_farfield.py create mode 100644 meanas/test/test_fdtd_base.py create mode 100644 meanas/test/test_import_fallbacks.py diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py index fc456eb..dec3d83 100644 --- a/meanas/fdtd/pml.py +++ b/meanas/fdtd/pml.py @@ -57,8 +57,6 @@ def cpml_params( xh -= 0.5 xe = xe[::-1] xh = xh[::-1] - else: - raise Exception('Bad polarity!') expand_slice_l: list[Any] = [None, None, None] expand_slice_l[axis] = slice(None) @@ -82,8 +80,6 @@ def cpml_params( region_list[axis] = slice(None, thickness) elif polarity > 0: region_list[axis] = slice(-thickness, None) - else: - raise Exception('Bad polarity!') region = tuple(region_list) return { diff --git a/meanas/test/test_bloch_interactions.py b/meanas/test/test_bloch_interactions.py index 28edcf8..0a2b70c 100644 --- a/meanas/test/test_bloch_interactions.py +++ b/meanas/test/test_bloch_interactions.py @@ -1,5 +1,7 @@ import numpy +import pytest from numpy.testing import assert_allclose +from types import SimpleNamespace from ..fdfd import bloch @@ -10,6 +12,12 @@ EPSILON = numpy.ones((3, *SHAPE), dtype=float) K0 = numpy.array([0.1, 0.0, 0.0], dtype=float) H_SIZE = 2 * numpy.prod(SHAPE) Y0 = (numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE))[None, :] +Y0_TWO_MODE = numpy.vstack( + [ + numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE), + numpy.linspace(2.0, 3.5, H_SIZE) - 0.5j * numpy.arange(H_SIZE, dtype=float), + ], +) def build_overlap_fixture() -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]: @@ -98,6 +106,187 @@ def test_eigsolve_returns_finite_modes_with_small_residual() -> None: assert callback_count > 0 +def test_eigsolve_without_initial_guess_returns_finite_modes() -> None: + eigvals, eigvecs = bloch.eigsolve( + 1, + K0, + G_MATRIX, + EPSILON, + tolerance=1e-6, + max_iters=20, + y0=None, + ) + + operator = bloch.maxwell_operator(K0, G_MATRIX, EPSILON) + eigvec = eigvecs[0] / numpy.linalg.norm(eigvecs[0]) + residual = numpy.linalg.norm(operator(eigvec).reshape(-1) - eigvals[0] * eigvec) / numpy.linalg.norm(eigvec) + + assert eigvals.shape == (1,) + assert eigvecs.shape == (1, H_SIZE) + assert numpy.isfinite(eigvals).all() + assert numpy.isfinite(eigvecs).all() + assert residual < 1e-5 + + +def test_eigsolve_recovers_from_singular_initial_subspace(monkeypatch: pytest.MonkeyPatch) -> None: + class FakeRng: + def __init__(self) -> None: + self.calls = 0 + + def random(self, shape: tuple[int, ...]) -> numpy.ndarray: + self.calls += 1 + return numpy.arange(numpy.prod(shape), dtype=float).reshape(shape) + self.calls + + fake_rng = FakeRng() + singular_y0 = numpy.vstack([Y0_TWO_MODE[0], Y0_TWO_MODE[0]]) + monkeypatch.setattr(bloch.numpy.random, 'default_rng', lambda: fake_rng) + + eigvals, eigvecs = bloch.eigsolve( + 2, + K0, + G_MATRIX, + EPSILON, + tolerance=1e-6, + max_iters=20, + y0=singular_y0, + ) + + assert fake_rng.calls == 2 + assert eigvals.shape == (2,) + assert eigvecs.shape == (2, H_SIZE) + assert numpy.isfinite(eigvals).all() + assert numpy.isfinite(eigvecs).all() + + +def test_eigsolve_reconditions_large_trace_initial_subspace(monkeypatch: pytest.MonkeyPatch) -> None: + original_inv = bloch.numpy.linalg.inv + original_sqrtm = bloch.scipy.linalg.sqrtm + sqrtm_calls = 0 + inv_calls = 0 + + def inv_with_large_first_trace(matrix: numpy.ndarray) -> numpy.ndarray: + nonlocal inv_calls + inv_calls += 1 + if inv_calls == 1: + return numpy.eye(matrix.shape[0], dtype=complex) * 1e9 + return original_inv(matrix) + + def sqrtm_wrapper(matrix: numpy.ndarray) -> numpy.ndarray: + nonlocal sqrtm_calls + sqrtm_calls += 1 + return original_sqrtm(matrix) + + monkeypatch.setattr(bloch.numpy.linalg, 'inv', inv_with_large_first_trace) + monkeypatch.setattr(bloch.scipy.linalg, 'sqrtm', sqrtm_wrapper) + + eigvals, eigvecs = bloch.eigsolve( + 2, + K0, + G_MATRIX, + EPSILON, + tolerance=1e-6, + max_iters=20, + y0=Y0_TWO_MODE, + ) + + assert sqrtm_calls >= 2 + assert eigvals.shape == (2,) + assert eigvecs.shape == (2, H_SIZE) + assert numpy.isfinite(eigvals).all() + assert numpy.isfinite(eigvecs).all() + + +def test_eigsolve_qi_memoization_reuses_cached_theta(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_minimize_scalar(func, method: str, bounds: tuple[float, float], options: dict[str, float]) -> SimpleNamespace: + theta = 0.3 + first = func(theta) + second = func(theta) + assert_allclose(second, first) + return SimpleNamespace(fun=second, x=theta) + + monkeypatch.setattr(bloch.scipy.optimize, 'minimize_scalar', fake_minimize_scalar) + + eigvals, eigvecs = bloch.eigsolve( + 1, + K0, + G_MATRIX, + EPSILON, + tolerance=1e-6, + max_iters=1, + y0=Y0, + ) + + assert eigvals.shape == (1,) + assert eigvecs.shape == (1, H_SIZE) + assert numpy.isfinite(eigvals).all() + assert numpy.isfinite(eigvecs).all() + + +@pytest.mark.parametrize('theta', [numpy.pi / 2 - 1e-8, 1e-8]) +def test_eigsolve_qi_taylor_expansions_return_finite_modes(monkeypatch: pytest.MonkeyPatch, theta: float) -> None: + original_inv = bloch.numpy.linalg.inv + inv_calls = 0 + + def inv_raise_once_for_q(matrix: numpy.ndarray) -> numpy.ndarray: + nonlocal inv_calls + inv_calls += 1 + if inv_calls == 3: + raise numpy.linalg.LinAlgError('forced singular Q') + return original_inv(matrix) + + def fake_minimize_scalar(func, method: str, bounds: tuple[float, float], options: dict[str, float]) -> SimpleNamespace: + value = func(theta) + return SimpleNamespace(fun=value, x=theta) + + monkeypatch.setattr(bloch.numpy.linalg, 'inv', inv_raise_once_for_q) + monkeypatch.setattr(bloch.scipy.optimize, 'minimize_scalar', fake_minimize_scalar) + + eigvals, eigvecs = bloch.eigsolve( + 1, + K0, + G_MATRIX, + EPSILON, + tolerance=1e-6, + max_iters=1, + y0=Y0, + ) + + assert eigvals.shape == (1,) + assert eigvecs.shape == (1, H_SIZE) + assert numpy.isfinite(eigvals).all() + assert numpy.isfinite(eigvecs).all() + + +def test_eigsolve_qi_inexplicable_singularity_raises(monkeypatch: pytest.MonkeyPatch) -> None: + original_inv = bloch.numpy.linalg.inv + inv_calls = 0 + + def inv_raise_once_for_q(matrix: numpy.ndarray) -> numpy.ndarray: + nonlocal inv_calls + inv_calls += 1 + if inv_calls == 3: + raise numpy.linalg.LinAlgError('forced singular Q') + return original_inv(matrix) + + def fake_minimize_scalar(func, method: str, bounds: tuple[float, float], options: dict[str, float]) -> SimpleNamespace: + func(numpy.pi / 4) + raise AssertionError('unreachable after trace_func exception') + + monkeypatch.setattr(bloch.numpy.linalg, 'inv', inv_raise_once_for_q) + monkeypatch.setattr(bloch.scipy.optimize, 'minimize_scalar', fake_minimize_scalar) + + with pytest.raises(Exception, match='Inexplicable singularity in trace_func'): + bloch.eigsolve( + 1, + K0, + G_MATRIX, + EPSILON, + tolerance=1e-6, + max_iters=1, + y0=Y0, + ) + + def test_find_k_returns_vector_frequency_and_callbacks() -> None: target_eigvals, _target_eigvecs = bloch.eigsolve( 1, diff --git a/meanas/test/test_eigensolvers_numerics.py b/meanas/test/test_eigensolvers_numerics.py index 8d0c5c9..5849d2c 100644 --- a/meanas/test/test_eigensolvers_numerics.py +++ b/meanas/test/test_eigensolvers_numerics.py @@ -2,6 +2,7 @@ import numpy from numpy.linalg import norm from scipy import sparse import scipy.sparse.linalg as spalg +from numpy.testing import assert_allclose from ..eigensolvers import rayleigh_quotient_iteration, signed_eigensolve @@ -45,3 +46,38 @@ def test_signed_eigensolve_negative_returns_largest_negative_mode() -> None: assert eigvecs.shape == (4, 1) assert abs(eigvals[0] + 2.0) < 1e-12 assert abs(eigvecs[3, 0]) > 0.99 + + +def test_rayleigh_quotient_iteration_uses_default_linear_operator_solver() -> None: + operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() + linear_operator = spalg.LinearOperator( + shape=operator.shape, + dtype=complex, + matvec=lambda vv: operator @ vv, + ) + + eigval, eigvec = rayleigh_quotient_iteration( + linear_operator, + numpy.array([0.0, 1.0, 1e-6, 0.0], dtype=complex), + iterations=8, + ) + + residual = norm(operator @ eigvec - eigval * eigvec) + assert abs(eigval - 3.0) < 1e-12 + assert residual < 1e-12 + + +def test_signed_eigensolve_linear_operator_fallback_returns_dominant_positive_mode() -> None: + operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() + linear_operator = spalg.LinearOperator( + shape=operator.shape, + dtype=complex, + matvec=lambda vv: operator @ vv, + ) + + eigvals, eigvecs = signed_eigensolve(linear_operator, how_many=1) + + assert eigvals.shape == (1,) + assert eigvecs.shape == (4, 1) + assert_allclose(eigvals[0], 5.0, atol=1e-12, rtol=1e-12) + assert abs(eigvecs[0, 0]) > 0.99 diff --git a/meanas/test/test_fdfd_algebra_helpers.py b/meanas/test/test_fdfd_algebra_helpers.py index b481023..57bb431 100644 --- a/meanas/test/test_fdfd_algebra_helpers.py +++ b/meanas/test/test_fdfd_algebra_helpers.py @@ -200,6 +200,15 @@ def test_uniform_grid_scpml_matches_expected_stretch_profile() -> None: assert numpy.isfinite(dxes[1][0]).all() +def test_uniform_grid_scpml_default_s_function_matches_explicit_default() -> None: + implicit = scpml.uniform_grid_scpml((6, 4, 3), (2, 0, 1), omega=2.0) + explicit = scpml.uniform_grid_scpml((6, 4, 3), (2, 0, 1), omega=2.0, s_function=scpml.prepare_s_function()) + + for implicit_group, explicit_group in zip(implicit, explicit, strict=True): + for implicit_axis, explicit_axis in zip(implicit_group, explicit_group, strict=True): + assert_allclose(implicit_axis, explicit_axis) + + def test_stretch_with_scpml_only_modifies_requested_front_edge() -> None: s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0) base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)] diff --git a/meanas/test/test_fdfd_farfield.py b/meanas/test/test_fdfd_farfield.py new file mode 100644 index 0000000..a040b67 --- /dev/null +++ b/meanas/test/test_fdfd_farfield.py @@ -0,0 +1,74 @@ +import numpy +import pytest + +from ..fdfd import farfield + + +NEAR_SHAPE = (2, 3) +E_NEAR = [numpy.zeros(NEAR_SHAPE, dtype=complex), numpy.zeros(NEAR_SHAPE, dtype=complex)] +H_NEAR = [numpy.zeros(NEAR_SHAPE, dtype=complex), numpy.zeros(NEAR_SHAPE, dtype=complex)] + + +def test_near_to_farfield_rejects_wrong_length_inputs() -> None: + with pytest.raises(Exception, match='E_near must be a length-2 list'): + farfield.near_to_farfield(E_NEAR[:1], H_NEAR, dx=0.2, dy=0.3) + + with pytest.raises(Exception, match='H_near must be a length-2 list'): + farfield.near_to_farfield(E_NEAR, H_NEAR[:1], dx=0.2, dy=0.3) + + +def test_near_to_farfield_rejects_mismatched_shapes() -> None: + bad_h_near = [H_NEAR[0], numpy.zeros((2, 4), dtype=complex)] + + with pytest.raises(Exception, match='All fields must be the same shape'): + farfield.near_to_farfield(E_NEAR, bad_h_near, dx=0.2, dy=0.3) + + +def test_near_to_farfield_uses_default_and_scalar_padding_shapes() -> None: + default_result = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3) + scalar_result = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8) + + assert default_result['E'][0].shape == (2, 4) + assert default_result['H'][0].shape == (2, 4) + assert scalar_result['E'][0].shape == (8, 8) + assert scalar_result['H'][0].shape == (8, 8) + + +def test_far_to_nearfield_rejects_wrong_length_inputs() -> None: + ff = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8) + + with pytest.raises(Exception, match='E_far must be a length-2 list'): + farfield.far_to_nearfield(ff['E'][:1], ff['H'], ff['dkx'], ff['dky']) + + with pytest.raises(Exception, match='H_far must be a length-2 list'): + farfield.far_to_nearfield(ff['E'], ff['H'][:1], ff['dkx'], ff['dky']) + + +def test_far_to_nearfield_rejects_mismatched_shapes() -> None: + ff = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8) + bad_h_far = [ff['H'][0], numpy.zeros((8, 4), dtype=complex)] + + with pytest.raises(Exception, match='All fields must be the same shape'): + farfield.far_to_nearfield(ff['E'], bad_h_far, ff['dkx'], ff['dky']) + + +def test_far_to_nearfield_uses_default_and_scalar_padding_shapes() -> None: + ff = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8) + default_result = farfield.far_to_nearfield( + [field.copy() for field in ff['E']], + [field.copy() for field in ff['H']], + ff['dkx'], + ff['dky'], + ) + scalar_result = farfield.far_to_nearfield( + [field.copy() for field in ff['E']], + [field.copy() for field in ff['H']], + ff['dkx'], + ff['dky'], + padded_size=4, + ) + + assert default_result['E'][0].shape == (8, 8) + assert default_result['H'][0].shape == (8, 8) + assert scalar_result['E'][0].shape == (4, 4) + assert scalar_result['H'][0].shape == (4, 4) diff --git a/meanas/test/test_fdtd_base.py b/meanas/test/test_fdtd_base.py new file mode 100644 index 0000000..41b6ad1 --- /dev/null +++ b/meanas/test/test_fdtd_base.py @@ -0,0 +1,44 @@ +import numpy +from numpy.testing import assert_allclose + +from ..fdmath import functional as fd_functional +from ..fdtd import base + + +DT = 0.25 +SHAPE = (3, 2, 2, 2) +E_FIELD = numpy.arange(numpy.prod(SHAPE), dtype=float).reshape(SHAPE, order='C') / 5.0 +H_FIELD = (numpy.arange(numpy.prod(SHAPE), dtype=float).reshape(SHAPE, order='C') + 1.0) / 7.0 +EPSILON = 1.5 + E_FIELD / 10.0 +MU_FIELD = 2.0 + H_FIELD / 8.0 +MU_SCALAR = 3.0 + + +def test_maxwell_e_without_dxes_matches_unit_spacing_update() -> None: + updater = base.maxwell_e(dt=DT) + expected = E_FIELD + DT * fd_functional.curl_back()(H_FIELD) / EPSILON + + updated = updater(E_FIELD.copy(), H_FIELD.copy(), EPSILON) + + assert_allclose(updated, expected) + + +def test_maxwell_h_without_dxes_and_without_mu_matches_unit_spacing_update() -> None: + updater = base.maxwell_h(dt=DT) + expected = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD) + + updated = updater(E_FIELD.copy(), H_FIELD.copy()) + + assert_allclose(updated, expected) + + +def test_maxwell_h_without_dxes_accepts_scalar_and_field_mu() -> None: + updater = base.maxwell_h(dt=DT) + + updated_scalar = updater(E_FIELD.copy(), H_FIELD.copy(), MU_SCALAR) + expected_scalar = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD) / MU_SCALAR + assert_allclose(updated_scalar, expected_scalar) + + updated_field = updater(E_FIELD.copy(), H_FIELD.copy(), MU_FIELD) + expected_field = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD) / MU_FIELD + assert_allclose(updated_field, expected_field) diff --git a/meanas/test/test_import_fallbacks.py b/meanas/test/test_import_fallbacks.py new file mode 100644 index 0000000..abee887 --- /dev/null +++ b/meanas/test/test_import_fallbacks.py @@ -0,0 +1,43 @@ +import builtins +import importlib +import pathlib + +import meanas +from ..fdfd import bloch + + +def test_meanas_import_survives_readme_open_failure(monkeypatch) -> None: # type: ignore[no-untyped-def] + original_open = pathlib.Path.open + + def failing_open(self: pathlib.Path, *args, **kwargs): # type: ignore[no-untyped-def] + if self.name == 'README.md': + raise FileNotFoundError('forced README failure') + return original_open(self, *args, **kwargs) + + monkeypatch.setattr(pathlib.Path, 'open', failing_open) + reloaded = importlib.reload(meanas) + + assert reloaded.__version__ == '0.10' + assert reloaded.__author__ == 'Jan Petykiewicz' + assert reloaded.__doc__ is not None + + monkeypatch.undo() + importlib.reload(meanas) + + +def test_bloch_reloads_with_numpy_fft_when_pyfftw_is_unavailable(monkeypatch) -> None: # type: ignore[no-untyped-def] + original_import = builtins.__import__ + + def fake_import(name: str, globals=None, locals=None, fromlist=(), level: int = 0): # type: ignore[no-untyped-def] + if name.startswith('pyfftw'): + raise ImportError('forced pyfftw failure') + return original_import(name, globals, locals, fromlist, level) + + monkeypatch.setattr(builtins, '__import__', fake_import) + reloaded = importlib.reload(bloch) + + assert reloaded.fftn.__module__ == 'numpy.fft' + assert reloaded.ifftn.__module__ == 'numpy.fft' + + monkeypatch.undo() + importlib.reload(bloch) diff --git a/meanas/test/test_waveguide_mode_helpers.py b/meanas/test/test_waveguide_mode_helpers.py index 7bbcd88..d5d3abf 100644 --- a/meanas/test/test_waveguide_mode_helpers.py +++ b/meanas/test/test_waveguide_mode_helpers.py @@ -162,6 +162,21 @@ def test_waveguide_3d_compute_overlap_e_rejects_empty_overlap_window( ) +def test_waveguide_3d_compute_overlap_e_rejects_zero_support_window() -> None: + _epsilon, dxes, slices, result = build_waveguide_3d_mode(slice_start=2, polarity=1) + + with pytest.raises(ValueError, match='no overlap field support'): + waveguide_3d.compute_overlap_e( + E=numpy.zeros_like(result['E']), + wavenumber=result['wavenumber'], + dxes=dxes, + axis=0, + polarity=1, + slices=slices, + omega=OMEGA, + ) + + def test_waveguide_cyl_solved_modes_are_ordered_and_low_residual() -> None: dxes, epsilon, rmin = build_waveguide_cyl_fixture() From 8cdcd08ba0abffc66b6dd63e23e997c111b6a774 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 00:52:04 -0700 Subject: [PATCH 406/437] [tests] refactor tests --- meanas/test/_bloch_case.py | 37 ++++ meanas/test/_fdfd_case.py | 49 +++++ meanas/test/_solver_cases.py | 56 +++++ meanas/test/_test_builders.py | 22 ++ meanas/test/conftest.py | 15 +- meanas/test/test_bloch_foundations.py | 48 ++--- meanas/test/test_bloch_interactions.py | 67 ++---- meanas/test/test_eigensolvers_numerics.py | 90 ++++---- meanas/test/test_eme_numerics.py | 63 +++--- meanas/test/test_fdfd_algebra_helpers.py | 122 +++++------ meanas/test/test_fdfd_farfield.py | 26 +++ meanas/test/test_fdfd_functional.py | 53 ++--- meanas/test/test_fdfd_solvers.py | 106 +++++----- meanas/test/test_fdmath_functional.py | 18 +- meanas/test/test_fdmath_operators.py | 19 +- meanas/test/test_fdmath_vectorization.py | 15 +- meanas/test/test_fdtd.py | 5 +- meanas/test/test_fdtd_base.py | 15 +- meanas/test/test_fdtd_boundaries.py | 62 ++++++ meanas/test/test_fdtd_energy.py | 21 +- meanas/test/test_fdtd_misc.py | 42 ++++ meanas/test/test_fdtd_pml.py | 44 ++++ meanas/test/test_import_fallbacks.py | 20 +- meanas/test/test_regressions.py | 238 ---------------------- meanas/test/utils.py | 4 +- 25 files changed, 645 insertions(+), 612 deletions(-) create mode 100644 meanas/test/_bloch_case.py create mode 100644 meanas/test/_fdfd_case.py create mode 100644 meanas/test/_solver_cases.py create mode 100644 meanas/test/_test_builders.py create mode 100644 meanas/test/test_fdtd_boundaries.py create mode 100644 meanas/test/test_fdtd_misc.py create mode 100644 meanas/test/test_fdtd_pml.py delete mode 100644 meanas/test/test_regressions.py diff --git a/meanas/test/_bloch_case.py b/meanas/test/_bloch_case.py new file mode 100644 index 0000000..430cd06 --- /dev/null +++ b/meanas/test/_bloch_case.py @@ -0,0 +1,37 @@ +import numpy + + +SHAPE = (2, 2, 2) +G_MATRIX = numpy.eye(3) +K0_GENERAL = numpy.array([0.1, 0.2, 0.3], dtype=float) +K0_AXIAL = numpy.array([0.0, 0.0, 0.25], dtype=float) +K0_X = numpy.array([0.1, 0.0, 0.0], dtype=float) +EPSILON = numpy.ones((3, *SHAPE), dtype=float) +MU = numpy.stack([ + numpy.linspace(2.0, 2.7, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(2.1, 2.8, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(2.2, 2.9, numpy.prod(SHAPE)).reshape(SHAPE), +]) +H_SIZE = 2 * numpy.prod(SHAPE) +H_MN = (numpy.arange(H_SIZE) + 0.25j).astype(complex) +ZERO_H_MN = numpy.zeros_like(H_MN) +Y0 = (numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE))[None, :] +Y0_TWO_MODE = numpy.vstack( + [ + numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE), + numpy.linspace(2.0, 3.5, H_SIZE) - 0.5j * numpy.arange(H_SIZE, dtype=float), + ], +) + + +def build_overlap_fixture() -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]: + e_in = numpy.zeros((3, *SHAPE), dtype=complex) + h_in = numpy.zeros_like(e_in) + e_out = numpy.zeros_like(e_in) + h_out = numpy.zeros_like(e_in) + + e_in[1] = 1.0 + h_in[2] = 2.0 + e_out[1] = 3.0 + h_out[2] = 4.0 + return e_in, h_in, e_out, h_out diff --git a/meanas/test/_fdfd_case.py b/meanas/test/_fdfd_case.py new file mode 100644 index 0000000..35284f6 --- /dev/null +++ b/meanas/test/_fdfd_case.py @@ -0,0 +1,49 @@ +import numpy + +from ..fdmath import vec, unvec + + +OMEGA = 1 / 1500 +SHAPE = (2, 3, 2) +DXES = [ + [numpy.array([1.0, 1.5]), numpy.array([0.75, 1.25, 1.5]), numpy.array([1.2, 0.8])], + [numpy.array([0.9, 1.4]), numpy.array([0.8, 1.1, 1.4]), numpy.array([1.0, 0.7])], +] + +EPSILON = numpy.stack([ + numpy.linspace(1.0, 2.2, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(1.1, 2.3, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(1.2, 2.4, numpy.prod(SHAPE)).reshape(SHAPE), +]) +MU = numpy.stack([ + numpy.linspace(2.0, 3.2, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(2.1, 3.3, numpy.prod(SHAPE)).reshape(SHAPE), + numpy.linspace(2.2, 3.4, numpy.prod(SHAPE)).reshape(SHAPE), +]) + +E_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) + 0.5j).astype(complex) +H_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) * 0.25 - 0.75j).astype(complex) + +PEC = numpy.zeros((3, *SHAPE), dtype=float) +PEC[1, 0, 1, 0] = 1.0 +PMC = numpy.zeros((3, *SHAPE), dtype=float) +PMC[2, 1, 2, 1] = 1.0 + +TF_REGION = numpy.zeros((3, *SHAPE), dtype=float) +TF_REGION[:, 0, 1, 0] = 1.0 + +BOUNDARY_SHAPE = (3, 4, 3) +BOUNDARY_DXES = [ + [numpy.array([1.0, 1.5, 0.8]), numpy.array([0.75, 1.25, 1.5, 0.9]), numpy.array([1.2, 0.8, 1.1])], + [numpy.array([0.9, 1.4, 1.0]), numpy.array([0.8, 1.1, 1.4, 1.0]), numpy.array([1.0, 0.7, 1.3])], +] +BOUNDARY_EPSILON = numpy.stack([ + numpy.linspace(1.0, 2.2, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE), + numpy.linspace(1.1, 2.3, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE), + numpy.linspace(1.2, 2.4, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE), +]) +BOUNDARY_FIELD = (numpy.arange(3 * numpy.prod(BOUNDARY_SHAPE)).reshape((3, *BOUNDARY_SHAPE)) + 0.5j).astype(complex) + + +def apply_fdfd_matrix(op, field: numpy.ndarray, shape: tuple[int, ...] = SHAPE) -> numpy.ndarray: + return unvec(op @ vec(field), shape) diff --git a/meanas/test/_solver_cases.py b/meanas/test/_solver_cases.py new file mode 100644 index 0000000..364e1f0 --- /dev/null +++ b/meanas/test/_solver_cases.py @@ -0,0 +1,56 @@ +import dataclasses +import numpy +from scipy import sparse +import scipy.sparse.linalg as spalg + +from ._test_builders import unit_dxes + + +@dataclasses.dataclass(frozen=True) +class DiagonalEigenCase: + operator: sparse.csr_matrix + linear_operator: spalg.LinearOperator + guess_default: numpy.ndarray + guess_sparse: numpy.ndarray + + +@dataclasses.dataclass(frozen=True) +class SolverPlumbingCase: + omega: float + a0: sparse.csr_matrix + pl: sparse.csr_matrix + pr: sparse.csr_matrix + j: numpy.ndarray + guess: numpy.ndarray + solver_result: numpy.ndarray + dxes: tuple[tuple[numpy.ndarray, ...], tuple[numpy.ndarray, ...]] + epsilon: numpy.ndarray + + +def diagonal_eigen_case() -> DiagonalEigenCase: + operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() + linear_operator = spalg.LinearOperator( + shape=operator.shape, + dtype=complex, + matvec=lambda vv: operator @ vv, + ) + return DiagonalEigenCase( + operator=operator, + linear_operator=linear_operator, + guess_default=numpy.array([0.0, 1.0, 1e-6, 0.0], dtype=complex), + guess_sparse=numpy.array([1.0, 0.1, 0.0, 0.0], dtype=complex), + ) + + +def solver_plumbing_case() -> SolverPlumbingCase: + return SolverPlumbingCase( + omega=2.0, + a0=sparse.csr_matrix(numpy.array([[1.0 + 2.0j, 2.0], [3.0 - 1.0j, 4.0]])), + pl=sparse.csr_matrix(numpy.array([[2.0, 0.0], [0.0, 3.0j]])), + pr=sparse.csr_matrix(numpy.array([[0.5, 0.0], [0.0, -2.0j]])), + j=numpy.array([1.0 + 0.5j, -2.0]), + guess=numpy.array([0.25 - 0.75j, 1.5 + 0.5j]), + solver_result=numpy.array([3.0 - 1.0j, -4.0 + 2.0j]), + dxes=unit_dxes((1, 1, 1)), + epsilon=numpy.ones(2), + ) diff --git a/meanas/test/_test_builders.py b/meanas/test/_test_builders.py new file mode 100644 index 0000000..d8b2088 --- /dev/null +++ b/meanas/test/_test_builders.py @@ -0,0 +1,22 @@ +import numpy + + +def real_ramp(shape: tuple[int, ...], *, scale: float = 1.0, offset: float = 0.0) -> numpy.ndarray: + return numpy.arange(numpy.prod(shape), dtype=float).reshape(shape, order='C') * scale + offset + + +def complex_ramp( + shape: tuple[int, ...], + *, + scale: float = 1.0, + offset: float = 0.0, + imag_scale: float = 0.0, + imag_offset: float = 0.0, + ) -> numpy.ndarray: + real = real_ramp(shape, scale=scale, offset=offset) + imag = real_ramp(shape, scale=imag_scale, offset=imag_offset) + return (real + 1j * imag).astype(complex) + + +def unit_dxes(shape: tuple[int, ...]) -> tuple[tuple[numpy.ndarray, ...], tuple[numpy.ndarray, ...]]: + return tuple(tuple(numpy.ones(length) for length in shape) for _ in range(2)) # type: ignore[return-value] diff --git a/meanas/test/conftest.py b/meanas/test/conftest.py index 9ce179c..5b577c6 100644 --- a/meanas/test/conftest.py +++ b/meanas/test/conftest.py @@ -9,7 +9,7 @@ import numpy from numpy.typing import NDArray import pytest # type: ignore -from .utils import PRNG +from .utils import make_prng FixtureRequest = Any @@ -42,6 +42,7 @@ def epsilon( epsilon_bg: float, epsilon_fg: float, ) -> NDArray[numpy.float64]: + prng = make_prng() is3d = (numpy.array(shape) == 1).sum() == 0 if is3d: if request.param == '000': @@ -57,9 +58,11 @@ def epsilon( elif request.param == '000': epsilon[:, 0, 0, 0] = epsilon_fg elif request.param == 'random': - epsilon[:] = PRNG.uniform(low=min(epsilon_bg, epsilon_fg), - high=max(epsilon_bg, epsilon_fg), - size=shape) + epsilon[:] = prng.uniform( + low=min(epsilon_bg, epsilon_fg), + high=max(epsilon_bg, epsilon_fg), + size=shape, + ) return epsilon @@ -80,6 +83,7 @@ def dxes( shape: tuple[int, ...], dx: float, ) -> list[list[NDArray[numpy.float64]]]: + prng = make_prng() if request.param == 'uniform': dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] elif request.param == 'centerbig': @@ -88,8 +92,7 @@ def dxes( for ax in (0, 1, 2): dxes[eh][ax][dxes[eh][ax].size // 2] *= 1.1 elif request.param == 'random': - dxe = [PRNG.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]] + dxe = [prng.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]] dxh = [(d + numpy.roll(d, -1)) / 2 for d in dxe] dxes = [dxe, dxh] return dxes - diff --git a/meanas/test/test_bloch_foundations.py b/meanas/test/test_bloch_foundations.py index 0321365..02a547c 100644 --- a/meanas/test/test_bloch_foundations.py +++ b/meanas/test/test_bloch_foundations.py @@ -2,23 +2,11 @@ import numpy from numpy.linalg import norm from ..fdfd import bloch - - -SHAPE = (2, 2, 2) -K0 = numpy.array([0.1, 0.2, 0.3]) -G_MATRIX = numpy.eye(3) -EPSILON = numpy.ones((3, *SHAPE), dtype=float) -MU = numpy.stack([ - numpy.linspace(2.0, 2.7, numpy.prod(SHAPE)).reshape(SHAPE), - numpy.linspace(2.1, 2.8, numpy.prod(SHAPE)).reshape(SHAPE), - numpy.linspace(2.2, 2.9, numpy.prod(SHAPE)).reshape(SHAPE), -]) -H_MN = (numpy.arange(2 * numpy.prod(SHAPE)) + 0.25j).astype(complex) -ZERO_H_MN = numpy.zeros_like(H_MN) - +from ._bloch_case import EPSILON, G_MATRIX, H_MN, K0_AXIAL, K0_GENERAL, MU, SHAPE, ZERO_H_MN +from .utils import assert_close def test_generate_kmn_general_case_returns_orthonormal_basis() -> None: - k_mag, m_vecs, n_vecs = bloch.generate_kmn(K0, G_MATRIX, SHAPE) + k_mag, m_vecs, n_vecs = bloch.generate_kmn(K0_GENERAL, G_MATRIX, SHAPE) assert k_mag.shape == SHAPE + (1,) assert m_vecs.shape == SHAPE + (3,) @@ -27,57 +15,57 @@ def test_generate_kmn_general_case_returns_orthonormal_basis() -> None: assert numpy.isfinite(m_vecs).all() assert numpy.isfinite(n_vecs).all() - numpy.testing.assert_allclose(norm(m_vecs.reshape(-1, 3), axis=1), 1.0) - numpy.testing.assert_allclose(norm(n_vecs.reshape(-1, 3), axis=1), 1.0) - numpy.testing.assert_allclose(numpy.sum(m_vecs * n_vecs, axis=3), 0.0, atol=1e-12) + assert_close(norm(m_vecs.reshape(-1, 3), axis=1), 1.0) + assert_close(norm(n_vecs.reshape(-1, 3), axis=1), 1.0) + assert_close(numpy.sum(m_vecs * n_vecs, axis=3), 0.0, atol=1e-12) def test_generate_kmn_z_aligned_uses_default_transverse_basis() -> None: - k_mag, m_vecs, n_vecs = bloch.generate_kmn([0.0, 0.0, 0.25], G_MATRIX, (1, 1, 1)) + k_mag, m_vecs, n_vecs = bloch.generate_kmn(K0_AXIAL, G_MATRIX, (1, 1, 1)) assert numpy.isfinite(k_mag).all() - numpy.testing.assert_allclose(m_vecs[0, 0, 0], [0.0, 1.0, 0.0]) - numpy.testing.assert_allclose(numpy.sum(m_vecs * n_vecs, axis=3), 0.0, atol=1e-12) - numpy.testing.assert_allclose(norm(n_vecs.reshape(-1, 3), axis=1), 1.0) + assert_close(m_vecs[0, 0, 0], [0.0, 1.0, 0.0]) + assert_close(numpy.sum(m_vecs * n_vecs, axis=3), 0.0, atol=1e-12) + assert_close(norm(n_vecs.reshape(-1, 3), axis=1), 1.0) def test_maxwell_operator_returns_finite_column_vector_without_mu() -> None: - operator = bloch.maxwell_operator(K0, G_MATRIX, EPSILON) + operator = bloch.maxwell_operator(K0_GENERAL, G_MATRIX, EPSILON) result = operator(H_MN.copy()) zero_result = operator(ZERO_H_MN.copy()) assert result.shape == (2 * numpy.prod(SHAPE), 1) assert numpy.isfinite(result).all() - numpy.testing.assert_allclose(zero_result, 0.0) + assert_close(zero_result, 0.0) def test_maxwell_operator_returns_finite_column_vector_with_mu() -> None: - operator = bloch.maxwell_operator(K0, G_MATRIX, EPSILON, MU) + operator = bloch.maxwell_operator(K0_GENERAL, G_MATRIX, EPSILON, MU) result = operator(H_MN.copy()) zero_result = operator(ZERO_H_MN.copy()) assert result.shape == (2 * numpy.prod(SHAPE), 1) assert numpy.isfinite(result).all() - numpy.testing.assert_allclose(zero_result, 0.0) + assert_close(zero_result, 0.0) def test_inverse_maxwell_operator_returns_finite_column_vector_for_both_mu_branches() -> None: for mu in (None, MU): - operator = bloch.inverse_maxwell_operator_approx(K0, G_MATRIX, EPSILON, mu) + operator = bloch.inverse_maxwell_operator_approx(K0_GENERAL, G_MATRIX, EPSILON, mu) result = operator(H_MN.copy()) zero_result = operator(ZERO_H_MN.copy()) assert result.shape == (2 * numpy.prod(SHAPE), 1) assert numpy.isfinite(result).all() - numpy.testing.assert_allclose(zero_result, 0.0) + assert_close(zero_result, 0.0) def test_bloch_field_converters_return_finite_fields() -> None: - e_field = bloch.hmn_2_exyz(K0, G_MATRIX, EPSILON)(H_MN.copy()) - h_field = bloch.hmn_2_hxyz(K0, G_MATRIX, EPSILON)(H_MN.copy()) + e_field = bloch.hmn_2_exyz(K0_GENERAL, G_MATRIX, EPSILON)(H_MN.copy()) + h_field = bloch.hmn_2_hxyz(K0_GENERAL, G_MATRIX, EPSILON)(H_MN.copy()) assert e_field.shape == (3, *SHAPE) assert h_field.shape == (3, *SHAPE) diff --git a/meanas/test/test_bloch_interactions.py b/meanas/test/test_bloch_interactions.py index 0a2b70c..b67d5ce 100644 --- a/meanas/test/test_bloch_interactions.py +++ b/meanas/test/test_bloch_interactions.py @@ -4,33 +4,8 @@ from numpy.testing import assert_allclose from types import SimpleNamespace from ..fdfd import bloch - - -SHAPE = (2, 2, 2) -G_MATRIX = numpy.eye(3) -EPSILON = numpy.ones((3, *SHAPE), dtype=float) -K0 = numpy.array([0.1, 0.0, 0.0], dtype=float) -H_SIZE = 2 * numpy.prod(SHAPE) -Y0 = (numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE))[None, :] -Y0_TWO_MODE = numpy.vstack( - [ - numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE), - numpy.linspace(2.0, 3.5, H_SIZE) - 0.5j * numpy.arange(H_SIZE, dtype=float), - ], -) - - -def build_overlap_fixture() -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]: - e_in = numpy.zeros((3, *SHAPE), dtype=complex) - h_in = numpy.zeros_like(e_in) - e_out = numpy.zeros_like(e_in) - h_out = numpy.zeros_like(e_in) - - e_in[1] = 1.0 - h_in[2] = 2.0 - e_out[1] = 3.0 - h_out[2] = 4.0 - return e_in, h_in, e_out, h_out +from ._bloch_case import EPSILON, G_MATRIX, H_SIZE, K0_X, SHAPE, Y0, Y0_TWO_MODE, build_overlap_fixture +from .utils import assert_close def test_rtrace_atb_matches_real_frobenius_inner_product() -> None: @@ -45,8 +20,8 @@ def test_symmetrize_returns_hermitian_average() -> None: matrix = numpy.array([[1.0 + 2.0j, 3.0 - 1.0j], [2.0j, 4.0]], dtype=complex) result = bloch._symmetrize(matrix) - assert_allclose(result, 0.5 * (matrix + matrix.conj().T)) - assert_allclose(result, result.conj().T) + assert_close(result, 0.5 * (matrix + matrix.conj().T)) + assert_close(result, result.conj().T) def test_inner_product_is_nonmutating_and_obeys_sign_symmetry() -> None: @@ -58,9 +33,9 @@ def test_inner_product_is_nonmutating_and_obeys_sign_symmetry() -> None: np_term = bloch.inner_product(e_out, -h_out, e_in, h_in) nn = bloch.inner_product(e_out, -h_out, e_in, -h_in) - assert_allclose(pp, 0.8164965809277263 + 0.0j) - assert_allclose(pp, -nn, atol=1e-12, rtol=1e-12) - assert_allclose(pn, -np_term, atol=1e-12, rtol=1e-12) + assert_close(pp, 0.8164965809277263 + 0.0j) + assert_close(pp, -nn, atol=1e-12, rtol=1e-12) + assert_close(pn, -np_term, atol=1e-12, rtol=1e-12) assert numpy.array_equal(e_in, originals[0]) assert numpy.array_equal(h_in, originals[1]) assert numpy.array_equal(e_out, originals[2]) @@ -72,8 +47,8 @@ def test_trq_returns_expected_transmission_and_reflection() -> None: transmission, reflection = bloch.trq(e_in, h_in, e_out, h_out) - assert_allclose(transmission, 0.9797958971132713 + 0.0j, atol=1e-12, rtol=1e-12) - assert_allclose(reflection, 0.2 + 0.0j, atol=1e-12, rtol=1e-12) + assert_close(transmission, 0.9797958971132713 + 0.0j, atol=1e-12, rtol=1e-12) + assert_close(reflection, 0.2 + 0.0j, atol=1e-12, rtol=1e-12) def test_eigsolve_returns_finite_modes_with_small_residual() -> None: @@ -85,7 +60,7 @@ def test_eigsolve_returns_finite_modes_with_small_residual() -> None: eigvals, eigvecs = bloch.eigsolve( 1, - K0, + K0_X, G_MATRIX, EPSILON, tolerance=1e-6, @@ -94,7 +69,7 @@ def test_eigsolve_returns_finite_modes_with_small_residual() -> None: callback=callback, ) - operator = bloch.maxwell_operator(K0, G_MATRIX, EPSILON) + operator = bloch.maxwell_operator(K0_X, G_MATRIX, EPSILON) eigvec = eigvecs[0] / numpy.linalg.norm(eigvecs[0]) residual = numpy.linalg.norm(operator(eigvec).reshape(-1) - eigvals[0] * eigvec) / numpy.linalg.norm(eigvec) @@ -109,7 +84,7 @@ def test_eigsolve_returns_finite_modes_with_small_residual() -> None: def test_eigsolve_without_initial_guess_returns_finite_modes() -> None: eigvals, eigvecs = bloch.eigsolve( 1, - K0, + K0_X, G_MATRIX, EPSILON, tolerance=1e-6, @@ -117,7 +92,7 @@ def test_eigsolve_without_initial_guess_returns_finite_modes() -> None: y0=None, ) - operator = bloch.maxwell_operator(K0, G_MATRIX, EPSILON) + operator = bloch.maxwell_operator(K0_X, G_MATRIX, EPSILON) eigvec = eigvecs[0] / numpy.linalg.norm(eigvecs[0]) residual = numpy.linalg.norm(operator(eigvec).reshape(-1) - eigvals[0] * eigvec) / numpy.linalg.norm(eigvec) @@ -143,7 +118,7 @@ def test_eigsolve_recovers_from_singular_initial_subspace(monkeypatch: pytest.Mo eigvals, eigvecs = bloch.eigsolve( 2, - K0, + K0_X, G_MATRIX, EPSILON, tolerance=1e-6, @@ -181,7 +156,7 @@ def test_eigsolve_reconditions_large_trace_initial_subspace(monkeypatch: pytest. eigvals, eigvecs = bloch.eigsolve( 2, - K0, + K0_X, G_MATRIX, EPSILON, tolerance=1e-6, @@ -208,7 +183,7 @@ def test_eigsolve_qi_memoization_reuses_cached_theta(monkeypatch: pytest.MonkeyP eigvals, eigvecs = bloch.eigsolve( 1, - K0, + K0_X, G_MATRIX, EPSILON, tolerance=1e-6, @@ -243,7 +218,7 @@ def test_eigsolve_qi_taylor_expansions_return_finite_modes(monkeypatch: pytest.M eigvals, eigvecs = bloch.eigsolve( 1, - K0, + K0_X, G_MATRIX, EPSILON, tolerance=1e-6, @@ -278,7 +253,7 @@ def test_eigsolve_qi_inexplicable_singularity_raises(monkeypatch: pytest.MonkeyP with pytest.raises(Exception, match='Inexplicable singularity in trace_func'): bloch.eigsolve( 1, - K0, + K0_X, G_MATRIX, EPSILON, tolerance=1e-6, @@ -290,7 +265,7 @@ def test_eigsolve_qi_inexplicable_singularity_raises(monkeypatch: pytest.MonkeyP def test_find_k_returns_vector_frequency_and_callbacks() -> None: target_eigvals, _target_eigvecs = bloch.eigsolve( 1, - K0, + K0_X, G_MATRIX, EPSILON, tolerance=1e-6, @@ -329,8 +304,8 @@ def test_find_k_returns_vector_frequency_and_callbacks() -> None: assert found_k.shape == (3,) assert numpy.isfinite(found_k).all() - assert_allclose(numpy.cross(found_k, [1.0, 0.0, 0.0]), 0.0, atol=1e-12, rtol=1e-12) - assert_allclose(found_k, K0, atol=1e-4, rtol=1e-4) + assert_close(numpy.cross(found_k, [1.0, 0.0, 0.0]), 0.0, atol=1e-12, rtol=1e-12) + assert_close(found_k, K0_X, atol=1e-4, rtol=1e-4) assert abs(found_frequency - target_frequency) <= 1e-4 assert eigvals.shape == (1,) assert eigvecs.shape == (1, H_SIZE) diff --git a/meanas/test/test_eigensolvers_numerics.py b/meanas/test/test_eigensolvers_numerics.py index 5849d2c..cf037a6 100644 --- a/meanas/test/test_eigensolvers_numerics.py +++ b/meanas/test/test_eigensolvers_numerics.py @@ -1,46 +1,40 @@ import numpy from numpy.linalg import norm -from scipy import sparse -import scipy.sparse.linalg as spalg -from numpy.testing import assert_allclose +import pytest -from ..eigensolvers import rayleigh_quotient_iteration, signed_eigensolve +from ._solver_cases import diagonal_eigen_case +from .utils import assert_close +from ..eigensolvers import power_iteration, rayleigh_quotient_iteration, signed_eigensolve def test_rayleigh_quotient_iteration_with_linear_operator() -> None: - operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() - linear_operator = spalg.LinearOperator( - shape=operator.shape, - dtype=complex, - matvec=lambda vv: operator @ vv, - ) + case = diagonal_eigen_case() def dense_solver( - shifted_operator: spalg.LinearOperator, + shifted_operator, rhs: numpy.ndarray, ) -> numpy.ndarray: - basis = numpy.eye(operator.shape[0], dtype=complex) - columns = [shifted_operator.matvec(basis[:, ii]) for ii in range(operator.shape[0])] + basis = numpy.eye(case.operator.shape[0], dtype=complex) + columns = [shifted_operator.matvec(basis[:, ii]) for ii in range(case.operator.shape[0])] dense_matrix = numpy.column_stack(columns) return numpy.linalg.lstsq(dense_matrix, rhs, rcond=None)[0] - guess = numpy.array([0.0, 1.0, 1e-6, 0.0], dtype=complex) eigval, eigvec = rayleigh_quotient_iteration( - linear_operator, - guess, + case.linear_operator, + case.guess_default, iterations=8, solver=dense_solver, ) - residual = norm(operator @ eigvec - eigval * eigvec) + residual = norm(case.operator @ eigvec - eigval * eigvec) assert abs(eigval - 3.0) < 1e-12 assert residual < 1e-12 def test_signed_eigensolve_negative_returns_largest_negative_mode() -> None: - operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() + case = diagonal_eigen_case() - eigvals, eigvecs = signed_eigensolve(operator, how_many=1, negative=True) + eigvals, eigvecs = signed_eigensolve(case.operator, how_many=1, negative=True) assert eigvals.shape == (1,) assert eigvecs.shape == (4, 1) @@ -49,35 +43,59 @@ def test_signed_eigensolve_negative_returns_largest_negative_mode() -> None: def test_rayleigh_quotient_iteration_uses_default_linear_operator_solver() -> None: - operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() - linear_operator = spalg.LinearOperator( - shape=operator.shape, - dtype=complex, - matvec=lambda vv: operator @ vv, - ) + case = diagonal_eigen_case() eigval, eigvec = rayleigh_quotient_iteration( - linear_operator, - numpy.array([0.0, 1.0, 1e-6, 0.0], dtype=complex), + case.linear_operator, + case.guess_default, iterations=8, ) - residual = norm(operator @ eigvec - eigval * eigvec) + residual = norm(case.operator @ eigvec - eigval * eigvec) assert abs(eigval - 3.0) < 1e-12 assert residual < 1e-12 def test_signed_eigensolve_linear_operator_fallback_returns_dominant_positive_mode() -> None: - operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() - linear_operator = spalg.LinearOperator( - shape=operator.shape, - dtype=complex, - matvec=lambda vv: operator @ vv, - ) + case = diagonal_eigen_case() - eigvals, eigvecs = signed_eigensolve(linear_operator, how_many=1) + eigvals, eigvecs = signed_eigensolve(case.linear_operator, how_many=1) assert eigvals.shape == (1,) assert eigvecs.shape == (4, 1) - assert_allclose(eigvals[0], 5.0, atol=1e-12, rtol=1e-12) + assert_close(eigvals[0], 5.0, atol=1e-12, rtol=1e-12) assert abs(eigvecs[0, 0]) > 0.99 + + +def test_power_iteration_finds_dominant_mode() -> None: + case = diagonal_eigen_case() + eigval, eigvec = power_iteration(case.operator, guess_vector=numpy.ones(4, dtype=complex), iterations=20) + + assert eigval == pytest.approx(5.0, rel=1e-6) + assert abs(eigvec[0]) > abs(eigvec[1]) + + +def test_rayleigh_quotient_iteration_refines_known_sparse_mode() -> None: + case = diagonal_eigen_case() + + def solver(matrix, rhs: numpy.ndarray) -> numpy.ndarray: + return numpy.linalg.lstsq(matrix.toarray(), rhs, rcond=None)[0] + + eigval, eigvec = rayleigh_quotient_iteration( + case.operator, + case.guess_sparse, + iterations=8, + solver=solver, + ) + + residual = numpy.linalg.norm(case.operator @ eigvec - eigval * eigvec) + assert eigval == pytest.approx(3.0, rel=1e-6) + assert residual < 1e-8 + + +def test_signed_eigensolve_returns_largest_positive_modes() -> None: + case = diagonal_eigen_case() + eigvals, eigvecs = signed_eigensolve(case.operator, how_many=2) + + assert_close(eigvals, [3.0, 5.0], atol=1e-6) + assert eigvecs.shape == (4, 2) diff --git a/meanas/test/test_eme_numerics.py b/meanas/test/test_eme_numerics.py index 40ca5ed..8798e0d 100644 --- a/meanas/test/test_eme_numerics.py +++ b/meanas/test/test_eme_numerics.py @@ -1,20 +1,21 @@ import numpy -from numpy.testing import assert_allclose from scipy import sparse from ..fdmath import vec from ..fdfd import eme +from ._test_builders import complex_ramp, unit_dxes +from .utils import assert_close SHAPE = (3, 2, 2) -DXES = [[numpy.ones(2), numpy.ones(2)] for _ in range(2)] +DXES = unit_dxes((2, 2)) WAVENUMBERS_L = numpy.array([1.0, 0.8]) WAVENUMBERS_R = numpy.array([0.9, 0.7]) def _mode(scale: float) -> tuple[numpy.ndarray, numpy.ndarray]: - e_field = (numpy.arange(12).reshape(SHAPE) + 1.0 + scale).astype(complex) - h_field = (numpy.arange(12).reshape(SHAPE) * 0.2 + 2.0 + 0.05j * scale).astype(complex) + e_field = complex_ramp(SHAPE, offset=1.0 + scale) + h_field = complex_ramp(SHAPE, scale=0.2, offset=2.0, imag_offset=0.05 * scale) return vec(e_field), vec(h_field) @@ -24,6 +25,29 @@ def _mode_sets() -> tuple[list[tuple[numpy.ndarray, numpy.ndarray]], list[tuple[ return left_modes, right_modes +def _gain_only_tr(*args, **kwargs) -> tuple[numpy.ndarray, numpy.ndarray]: + return numpy.array([[2.0, 0.0], [0.0, 0.5]]), numpy.zeros((2, 2)) + + +def _gain_and_reflection_tr(*args, **kwargs) -> tuple[numpy.ndarray, numpy.ndarray]: + return numpy.array([[2.0, 0.0], [0.0, 0.5]]), numpy.array([[0.0, 1.0], [2.0, 0.0]]) + + +def _nonsymmetric_tr(left_marker: object): + def fake_get_tr(_eh_left, wavenumbers_left, _eh_right, _wavenumbers_right, **kwargs): + if wavenumbers_left is left_marker: + return ( + numpy.array([[1.0, 2.0], [0.5, 1.0]]), + numpy.array([[0.0, 1.0], [2.0, 0.0]]), + ) + return ( + numpy.array([[1.0, -1.0], [0.0, 1.0]]), + numpy.array([[0.0, 0.5], [1.5, 0.0]]), + ) + + return fake_get_tr + + def test_get_tr_returns_finite_bounded_transfer_matrices() -> None: left_modes, right_modes = _mode_sets() @@ -58,7 +82,7 @@ def test_get_abcd_matches_explicit_block_formula() -> None: assert sparse.issparse(abcd) assert abcd.shape == (4, 4) - assert_allclose(abcd.toarray(), expected) + assert_close(abcd.toarray(), expected) def test_get_s_plain_matches_block_assembly_from_get_tr() -> None: @@ -71,14 +95,11 @@ def test_get_s_plain_matches_block_assembly_from_get_tr() -> None: assert ss.shape == (4, 4) assert numpy.isfinite(ss).all() - assert_allclose(ss, expected) + assert_close(ss, expected) def test_get_s_force_nogain_caps_singular_values(monkeypatch) -> None: - def fake_get_tr(*args, **kwargs): - return numpy.array([[2.0, 0.0], [0.0, 0.5]]), numpy.zeros((2, 2)) - - monkeypatch.setattr(eme, 'get_tr', fake_get_tr) + monkeypatch.setattr(eme, 'get_tr', _gain_only_tr) plain_s = eme.get_s(None, None, None, None) clipped_s = eme.get_s(None, None, None, None, force_nogain=True) @@ -95,31 +116,17 @@ def test_get_s_force_reciprocal_symmetrizes_output(monkeypatch) -> None: left = object() right = object() - def fake_get_tr(_eh_left, wavenumbers_left, _eh_right, _wavenumbers_right, **kwargs): - if wavenumbers_left is left: - return ( - numpy.array([[1.0, 2.0], [0.5, 1.0]]), - numpy.array([[0.0, 1.0], [2.0, 0.0]]), - ) - return ( - numpy.array([[1.0, -1.0], [0.0, 1.0]]), - numpy.array([[0.0, 0.5], [1.5, 0.0]]), - ) - - monkeypatch.setattr(eme, 'get_tr', fake_get_tr) + monkeypatch.setattr(eme, 'get_tr', _nonsymmetric_tr(left)) ss = eme.get_s(None, left, None, right, force_reciprocal=True) - assert_allclose(ss, ss.T) + assert_close(ss, ss.T) def test_get_s_force_nogain_and_reciprocal_returns_finite_output(monkeypatch) -> None: - def fake_get_tr(*args, **kwargs): - return numpy.array([[2.0, 0.0], [0.0, 0.5]]), numpy.array([[0.0, 1.0], [2.0, 0.0]]) - - monkeypatch.setattr(eme, 'get_tr', fake_get_tr) + monkeypatch.setattr(eme, 'get_tr', _gain_and_reflection_tr) ss = eme.get_s(None, None, None, None, force_nogain=True, force_reciprocal=True) assert ss.shape == (4, 4) assert numpy.isfinite(ss).all() - assert_allclose(ss, ss.T) + assert_close(ss, ss.T) assert (numpy.linalg.svd(ss, compute_uv=False) <= 1.0 + 1e-12).all() diff --git a/meanas/test/test_fdfd_algebra_helpers.py b/meanas/test/test_fdfd_algebra_helpers.py index 57bb431..825edb3 100644 --- a/meanas/test/test_fdfd_algebra_helpers.py +++ b/meanas/test/test_fdfd_algebra_helpers.py @@ -1,52 +1,25 @@ import numpy -from numpy.testing import assert_allclose from ..fdmath import vec, unvec from ..fdmath import functional as fd_functional from ..fdfd import operators, scpml - - -OMEGA = 1 / 1500 -SHAPE = (2, 3, 2) -DXES = [ - [numpy.array([1.0, 1.5]), numpy.array([0.75, 1.25, 1.5]), numpy.array([1.2, 0.8])], - [numpy.array([0.9, 1.4]), numpy.array([0.8, 1.1, 1.4]), numpy.array([1.0, 0.7])], -] - -EPSILON = numpy.stack([ - numpy.linspace(1.0, 2.2, numpy.prod(SHAPE)).reshape(SHAPE), - numpy.linspace(1.1, 2.3, numpy.prod(SHAPE)).reshape(SHAPE), - numpy.linspace(1.2, 2.4, numpy.prod(SHAPE)).reshape(SHAPE), -]) -MU = numpy.stack([ - numpy.linspace(2.0, 3.2, numpy.prod(SHAPE)).reshape(SHAPE), - numpy.linspace(2.1, 3.3, numpy.prod(SHAPE)).reshape(SHAPE), - numpy.linspace(2.2, 3.4, numpy.prod(SHAPE)).reshape(SHAPE), -]) - -H_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) * 0.25 - 0.75j).astype(complex) -E_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) + 0.5j).astype(complex) - -PEC = numpy.zeros((3, *SHAPE), dtype=float) -PEC[1, 0, 1, 0] = 1.0 -PMC = numpy.zeros((3, *SHAPE), dtype=float) -PMC[2, 1, 2, 1] = 1.0 - -BOUNDARY_SHAPE = (3, 4, 3) -BOUNDARY_DXES = [ - [numpy.array([1.0, 1.5, 0.8]), numpy.array([0.75, 1.25, 1.5, 0.9]), numpy.array([1.2, 0.8, 1.1])], - [numpy.array([0.9, 1.4, 1.0]), numpy.array([0.8, 1.1, 1.4, 1.0]), numpy.array([1.0, 0.7, 1.3])], -] -BOUNDARY_EPSILON = numpy.stack([ - numpy.linspace(1.0, 2.2, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE), - numpy.linspace(1.1, 2.3, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE), - numpy.linspace(1.2, 2.4, numpy.prod(BOUNDARY_SHAPE)).reshape(BOUNDARY_SHAPE), -]) -BOUNDARY_FIELD = (numpy.arange(3 * numpy.prod(BOUNDARY_SHAPE)).reshape((3, *BOUNDARY_SHAPE)) + 0.5j).astype(complex) - - -def _apply_matrix(op: operators.sparse.spmatrix, field: numpy.ndarray, shape: tuple[int, ...]) -> numpy.ndarray: - return unvec(op @ vec(field), shape) +from ._fdfd_case import ( + BOUNDARY_DXES, + BOUNDARY_EPSILON, + BOUNDARY_FIELD, + BOUNDARY_SHAPE, + DXES, + EPSILON, + E_FIELD, + MU, + H_FIELD, + OMEGA, + PEC, + PMC, + SHAPE, + apply_fdfd_matrix, +) +from .utils import assert_close, assert_fields_close def _dense_e_full(mu: numpy.ndarray | None) -> numpy.ndarray: @@ -84,36 +57,33 @@ def _normalized_distance(u: numpy.ndarray, size: int, thickness: int) -> numpy.n def test_h_full_matches_dense_reference_with_and_without_mu() -> None: for mu in (None, MU): - matrix_result = _apply_matrix( + matrix_result = apply_fdfd_matrix( operators.h_full(OMEGA, DXES, vec(EPSILON), None if mu is None else vec(mu), vec(PEC), vec(PMC)), H_FIELD, - SHAPE, ) dense_result = _dense_h_full(mu) - assert_allclose(matrix_result, dense_result, atol=1e-10, rtol=1e-10) + assert_fields_close(matrix_result, dense_result, atol=1e-10, rtol=1e-10) def test_e_full_matches_dense_reference_with_masks() -> None: for mu in (None, MU): - matrix_result = _apply_matrix( + matrix_result = apply_fdfd_matrix( operators.e_full(OMEGA, DXES, vec(EPSILON), None if mu is None else vec(mu), vec(PEC), vec(PMC)), E_FIELD, - SHAPE, ) dense_result = _dense_e_full(mu) - assert_allclose(matrix_result, dense_result, atol=1e-10, rtol=1e-10) + assert_fields_close(matrix_result, dense_result, atol=1e-10, rtol=1e-10) def test_h_full_without_masks_matches_dense_reference() -> None: ce = fd_functional.curl_forward(DXES[0]) ch = fd_functional.curl_back(DXES[1]) dense_result = ce(ch(H_FIELD) / EPSILON) - OMEGA**2 * MU * H_FIELD - matrix_result = _apply_matrix( + matrix_result = apply_fdfd_matrix( operators.h_full(OMEGA, DXES, vec(EPSILON), vec(MU)), H_FIELD, - SHAPE, ) - assert_allclose(matrix_result, dense_result, atol=1e-10, rtol=1e-10) + assert_fields_close(matrix_result, dense_result, atol=1e-10, rtol=1e-10) def test_eh_full_matches_manual_block_operator_with_masks() -> None: @@ -130,23 +100,23 @@ def test_eh_full_matches_manual_block_operator_with_masks() -> None: dense_e = pe * ch(pm * H_FIELD) - pe * (1j * OMEGA * EPSILON * (pe * E_FIELD)) dense_h = pm * ce(pe * E_FIELD) + pm * (1j * OMEGA * MU * (pm * H_FIELD)) - assert_allclose(matrix_e, dense_e, atol=1e-10, rtol=1e-10) - assert_allclose(matrix_h, dense_h, atol=1e-10, rtol=1e-10) + assert_fields_close(matrix_e, dense_e, atol=1e-10, rtol=1e-10) + assert_fields_close(matrix_h, dense_h, atol=1e-10, rtol=1e-10) def test_e2h_pmc_mask_matches_masked_unmasked_result() -> None: pmc_complement = numpy.where(PMC, 0.0, 1.0) - unmasked = _apply_matrix(operators.e2h(OMEGA, DXES, vec(MU)), E_FIELD, SHAPE) - masked = _apply_matrix(operators.e2h(OMEGA, DXES, vec(MU), vec(PMC)), E_FIELD, SHAPE) + unmasked = apply_fdfd_matrix(operators.e2h(OMEGA, DXES, vec(MU)), E_FIELD) + masked = apply_fdfd_matrix(operators.e2h(OMEGA, DXES, vec(MU), vec(PMC)), E_FIELD) - assert_allclose(masked, pmc_complement * unmasked, atol=1e-10, rtol=1e-10) + assert_fields_close(masked, pmc_complement * unmasked, atol=1e-10, rtol=1e-10) def test_poynting_h_cross_matches_negative_e_cross_relation() -> None: - h_cross_e = _apply_matrix(operators.poynting_h_cross(vec(H_FIELD), DXES), E_FIELD, SHAPE) - e_cross_h = _apply_matrix(operators.poynting_e_cross(vec(E_FIELD), DXES), H_FIELD, SHAPE) + h_cross_e = apply_fdfd_matrix(operators.poynting_h_cross(vec(H_FIELD), DXES), E_FIELD) + e_cross_h = apply_fdfd_matrix(operators.poynting_e_cross(vec(E_FIELD), DXES), H_FIELD) - assert_allclose(h_cross_e, -e_cross_h, atol=1e-10, rtol=1e-10) + assert_fields_close(h_cross_e, -e_cross_h, atol=1e-10, rtol=1e-10) def test_e_boundary_source_interior_mask_is_independent_of_periodic_edges() -> None: @@ -156,7 +126,7 @@ def test_e_boundary_source_interior_mask_is_independent_of_periodic_edges() -> N periodic = operators.e_boundary_source(vec(mask), OMEGA, BOUNDARY_DXES, vec(BOUNDARY_EPSILON), periodic_mask_edges=True) mirrored = operators.e_boundary_source(vec(mask), OMEGA, BOUNDARY_DXES, vec(BOUNDARY_EPSILON), periodic_mask_edges=False) - assert_allclose(periodic.toarray(), mirrored.toarray()) + assert_close(periodic.toarray(), mirrored.toarray()) def test_e_boundary_source_periodic_edges_add_opposite_face_response() -> None: @@ -168,7 +138,7 @@ def test_e_boundary_source_periodic_edges_add_opposite_face_response() -> None: diff = unvec((periodic - mirrored) @ vec(BOUNDARY_FIELD), BOUNDARY_SHAPE) assert numpy.isfinite(diff).all() - assert_allclose(diff[:, 1:-1, :, :], 0.0) + assert_close(diff[:, 1:-1, :, :], 0.0) assert numpy.linalg.norm(diff[:, -1, :, :]) > 0 @@ -179,7 +149,7 @@ def test_prepare_s_function_matches_closed_form_polynomial() -> None: s_function = scpml.prepare_s_function(ln_R=ln_r, m=order) expected = (order + 1) * ln_r / 2 * distances**order - assert_allclose(s_function(distances), expected) + assert_close(s_function(distances), expected) def test_uniform_grid_scpml_matches_expected_stretch_profile() -> None: @@ -191,11 +161,11 @@ def test_uniform_grid_scpml_matches_expected_stretch_profile() -> None: grid = numpy.arange(size, dtype=float) expected_a = 1 + 1j * s_function(_normalized_distance(grid, size, thickness)) / correction expected_b = 1 + 1j * s_function(_normalized_distance(grid + 0.5, size, thickness)) / correction - assert_allclose(dxes[0][axis], expected_a) - assert_allclose(dxes[1][axis], expected_b) + assert_close(dxes[0][axis], expected_a) + assert_close(dxes[1][axis], expected_b) - assert_allclose(dxes[0][1], 1.0) - assert_allclose(dxes[1][1], 1.0) + assert_close(dxes[0][1], 1.0) + assert_close(dxes[1][1], 1.0) assert numpy.isfinite(dxes[0][0]).all() assert numpy.isfinite(dxes[1][0]).all() @@ -206,7 +176,7 @@ def test_uniform_grid_scpml_default_s_function_matches_explicit_default() -> Non for implicit_group, explicit_group in zip(implicit, explicit, strict=True): for implicit_axis, explicit_axis in zip(implicit_group, explicit_group, strict=True): - assert_allclose(implicit_axis, explicit_axis) + assert_close(implicit_axis, explicit_axis) def test_stretch_with_scpml_only_modifies_requested_front_edge() -> None: @@ -214,10 +184,10 @@ def test_stretch_with_scpml_only_modifies_requested_front_edge() -> None: base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)] stretched = scpml.stretch_with_scpml(base, axis=0, polarity=1, omega=2.0, epsilon_effective=4.0, thickness=2, s_function=s_function) - assert_allclose(stretched[0][0][2:], 1.0) - assert_allclose(stretched[1][0][2:], 1.0) - assert_allclose(stretched[0][0][-2:], 1.0) - assert_allclose(stretched[1][0][-2:], 1.0) + assert_close(stretched[0][0][2:], 1.0) + assert_close(stretched[1][0][2:], 1.0) + assert_close(stretched[0][0][-2:], 1.0) + assert_close(stretched[1][0][-2:], 1.0) assert numpy.linalg.norm(stretched[0][0][:2] - 1.0) > 0 assert numpy.linalg.norm(stretched[1][0][:2] - 1.0) > 0 @@ -227,8 +197,8 @@ def test_stretch_with_scpml_only_modifies_requested_back_edge() -> None: base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)] stretched = scpml.stretch_with_scpml(base, axis=0, polarity=-1, omega=2.0, epsilon_effective=4.0, thickness=2, s_function=s_function) - assert_allclose(stretched[0][0][:4], 1.0) - assert_allclose(stretched[1][0][:4], 1.0) + assert_close(stretched[0][0][:4], 1.0) + assert_close(stretched[1][0][:4], 1.0) assert numpy.linalg.norm(stretched[0][0][-2:] - 1.0) > 0 assert numpy.linalg.norm(stretched[1][0][-2:] - 1.0) > 0 @@ -240,4 +210,4 @@ def test_stretch_with_scpml_thickness_zero_is_noop() -> None: for grid_group in stretched: for axis_grid in grid_group: - assert_allclose(axis_grid, 1.0) + assert_close(axis_grid, 1.0) diff --git a/meanas/test/test_fdfd_farfield.py b/meanas/test/test_fdfd_farfield.py index a040b67..5e2daab 100644 --- a/meanas/test/test_fdfd_farfield.py +++ b/meanas/test/test_fdfd_farfield.py @@ -72,3 +72,29 @@ def test_far_to_nearfield_uses_default_and_scalar_padding_shapes() -> None: assert default_result['H'][0].shape == (8, 8) assert scalar_result['E'][0].shape == (4, 4) assert scalar_result['H'][0].shape == (4, 4) + + +def test_farfield_roundtrip_supports_rectangular_arrays() -> None: + e_near = [numpy.zeros((4, 8), dtype=complex), numpy.zeros((4, 8), dtype=complex)] + h_near = [numpy.zeros((4, 8), dtype=complex), numpy.zeros((4, 8), dtype=complex)] + e_near[0][1, 3] = 1.0 + 0.25j + h_near[1][2, 5] = -0.5j + + ff = farfield.near_to_farfield(e_near, h_near, dx=0.2, dy=0.3, padded_size=(4, 8)) + restored = farfield.far_to_nearfield( + [field.copy() for field in ff['E']], + [field.copy() for field in ff['H']], + ff['dkx'], + ff['dky'], + padded_size=(4, 8), + ) + + assert isinstance(ff['dkx'], float) + assert isinstance(ff['dky'], float) + assert ff['E'][0].shape == (4, 8) + assert restored['E'][0].shape == (4, 8) + assert restored['H'][0].shape == (4, 8) + assert restored['dx'] == pytest.approx(0.2) + assert restored['dy'] == pytest.approx(0.3) + assert numpy.isfinite(restored['E'][0]).all() + assert numpy.isfinite(restored['H'][0]).all() diff --git a/meanas/test/test_fdfd_functional.py b/meanas/test/test_fdfd_functional.py index 5f0adef..9c1f771 100644 --- a/meanas/test/test_fdfd_functional.py +++ b/meanas/test/test_fdfd_functional.py @@ -1,48 +1,21 @@ import numpy -from numpy.testing import assert_allclose -from ..fdmath import vec, unvec +from ..fdmath import unvec, vec from ..fdfd import functional, operators +from ._fdfd_case import DXES, EPSILON, E_FIELD, H_FIELD, MU, OMEGA, SHAPE, TF_REGION, apply_fdfd_matrix +from .utils import assert_fields_close -OMEGA = 1 / 1500 -SHAPE = (2, 3, 2) ATOL = 1e-9 RTOL = 1e-9 -DXES = [ - [numpy.array([1.0, 1.5]), numpy.array([0.75, 1.25, 1.5]), numpy.array([1.2, 0.8])], - [numpy.array([0.9, 1.4]), numpy.array([0.8, 1.1, 1.4]), numpy.array([1.0, 0.7])], -] - -EPSILON = numpy.stack([ - numpy.linspace(1.0, 2.2, numpy.prod(SHAPE)).reshape(SHAPE), - numpy.linspace(1.1, 2.3, numpy.prod(SHAPE)).reshape(SHAPE), - numpy.linspace(1.2, 2.4, numpy.prod(SHAPE)).reshape(SHAPE), -]) -MU = numpy.stack([ - numpy.linspace(2.0, 3.2, numpy.prod(SHAPE)).reshape(SHAPE), - numpy.linspace(2.1, 3.3, numpy.prod(SHAPE)).reshape(SHAPE), - numpy.linspace(2.2, 3.4, numpy.prod(SHAPE)).reshape(SHAPE), -]) - -E_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) + 0.5j).astype(complex) -H_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) * 0.25 - 0.75j).astype(complex) - -TF_REGION = numpy.zeros((3, *SHAPE), dtype=float) -TF_REGION[:, 0, 1, 0] = 1.0 - - -def apply_matrix(op: operators.sparse.spmatrix, field: numpy.ndarray) -> numpy.ndarray: - return unvec(op @ vec(field), SHAPE) - def assert_fields_match(actual: numpy.ndarray, expected: numpy.ndarray) -> None: - assert_allclose(actual, expected, atol=ATOL, rtol=RTOL) + assert_fields_close(actual, expected, atol=ATOL, rtol=RTOL) def test_e_full_matches_sparse_operator_without_mu() -> None: - matrix_result = apply_matrix( + matrix_result = apply_fdfd_matrix( operators.e_full(OMEGA, DXES, vec(EPSILON)), E_FIELD, ) @@ -52,7 +25,7 @@ def test_e_full_matches_sparse_operator_without_mu() -> None: def test_e_full_matches_sparse_operator_with_mu() -> None: - matrix_result = apply_matrix( + matrix_result = apply_fdfd_matrix( operators.e_full(OMEGA, DXES, vec(EPSILON), vec(MU)), E_FIELD, ) @@ -80,7 +53,7 @@ def test_eh_full_matches_sparse_operator_without_mu() -> None: def test_e2h_matches_sparse_operator_with_mu() -> None: - matrix_result = apply_matrix( + matrix_result = apply_fdfd_matrix( operators.e2h(OMEGA, DXES, vec(MU)), E_FIELD, ) @@ -90,7 +63,7 @@ def test_e2h_matches_sparse_operator_with_mu() -> None: def test_e2h_matches_sparse_operator_without_mu() -> None: - matrix_result = apply_matrix( + matrix_result = apply_fdfd_matrix( operators.e2h(OMEGA, DXES), E_FIELD, ) @@ -100,7 +73,7 @@ def test_e2h_matches_sparse_operator_without_mu() -> None: def test_m2j_matches_sparse_operator_without_mu() -> None: - matrix_result = apply_matrix( + matrix_result = apply_fdfd_matrix( operators.m2j(OMEGA, DXES), H_FIELD, ) @@ -110,7 +83,7 @@ def test_m2j_matches_sparse_operator_without_mu() -> None: def test_m2j_matches_sparse_operator_with_mu() -> None: - matrix_result = apply_matrix( + matrix_result = apply_fdfd_matrix( operators.m2j(OMEGA, DXES, vec(MU)), H_FIELD, ) @@ -120,7 +93,7 @@ def test_m2j_matches_sparse_operator_with_mu() -> None: def test_e_tfsf_source_matches_sparse_operator_without_mu() -> None: - matrix_result = apply_matrix( + matrix_result = apply_fdfd_matrix( operators.e_tfsf_source(vec(TF_REGION), OMEGA, DXES, vec(EPSILON)), E_FIELD, ) @@ -130,7 +103,7 @@ def test_e_tfsf_source_matches_sparse_operator_without_mu() -> None: def test_e_tfsf_source_matches_sparse_operator_with_mu() -> None: - matrix_result = apply_matrix( + matrix_result = apply_fdfd_matrix( operators.e_tfsf_source(vec(TF_REGION), OMEGA, DXES, vec(EPSILON), vec(MU)), E_FIELD, ) @@ -140,7 +113,7 @@ def test_e_tfsf_source_matches_sparse_operator_with_mu() -> None: def test_poynting_e_cross_h_matches_sparse_operator() -> None: - matrix_result = apply_matrix( + matrix_result = apply_fdfd_matrix( operators.poynting_e_cross(vec(E_FIELD), DXES), H_FIELD, ) diff --git a/meanas/test/test_fdfd_solvers.py b/meanas/test/test_fdfd_solvers.py index 18c54be..b841dc9 100644 --- a/meanas/test/test_fdfd_solvers.py +++ b/meanas/test/test_fdfd_solvers.py @@ -1,140 +1,126 @@ import numpy -from numpy.testing import assert_allclose -from scipy import sparse from ..fdfd import solvers +from ._solver_cases import solver_plumbing_case +from .utils import assert_close def test_scipy_qmr_wraps_user_callback_without_recursion(monkeypatch) -> None: seen: list[tuple[float, ...]] = [] - def fake_qmr(a: sparse.spmatrix, b: numpy.ndarray, **kwargs): + def fake_qmr(a, b: numpy.ndarray, **kwargs): kwargs['callback'](numpy.array([1.0, 2.0])) return numpy.array([3.0, 4.0]), 0 monkeypatch.setattr(solvers.scipy.sparse.linalg, 'qmr', fake_qmr) result = solvers._scipy_qmr( - sparse.eye(2).tocsr(), + solver_plumbing_case().a0, numpy.array([1.0, 0.0]), callback=lambda xk: seen.append(tuple(xk)), ) - assert_allclose(result, [3.0, 4.0]) + assert_close(result, [3.0, 4.0]) assert seen == [(1.0, 2.0)] def test_scipy_qmr_installs_logging_callback_when_missing(monkeypatch) -> None: callback_seen: list[numpy.ndarray] = [] - def fake_qmr(a: sparse.spmatrix, b: numpy.ndarray, **kwargs): + def fake_qmr(a, b: numpy.ndarray, **kwargs): callback = kwargs['callback'] callback(numpy.array([5.0, 6.0])) callback_seen.append(b.copy()) return numpy.array([7.0, 8.0]), 0 monkeypatch.setattr(solvers.scipy.sparse.linalg, 'qmr', fake_qmr) - result = solvers._scipy_qmr(sparse.eye(2).tocsr(), numpy.array([1.0, 0.0])) + result = solvers._scipy_qmr(solver_plumbing_case().a0, numpy.array([1.0, 0.0])) - assert_allclose(result, [7.0, 8.0]) + assert_close(result, [7.0, 8.0]) assert len(callback_seen) == 1 def test_generic_forward_preconditions_system_and_guess(monkeypatch) -> None: - omega = 2.0 - a0 = sparse.csr_matrix(numpy.array([[1.0 + 2.0j, 2.0], [3.0 - 1.0j, 4.0]])) - pl = sparse.csr_matrix(numpy.array([[2.0, 0.0], [0.0, 3.0j]])) - pr = sparse.csr_matrix(numpy.array([[0.5, 0.0], [0.0, -2.0j]])) - j = numpy.array([1.0 + 0.5j, -2.0]) - guess = numpy.array([0.25 - 0.75j, 1.5 + 0.5j]) - solver_result = numpy.array([3.0 - 1.0j, -4.0 + 2.0j]) - captured: dict[str, numpy.ndarray | sparse.spmatrix] = {} + case = solver_plumbing_case() + captured: dict[str, object] = {} - monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: a0) - monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (pl, pr)) + monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: case.a0) + monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (case.pl, case.pr)) - def fake_solver(a: sparse.spmatrix, b: numpy.ndarray, **kwargs): + def fake_solver(a, b: numpy.ndarray, **kwargs): captured['a'] = a captured['b'] = b captured['x0'] = kwargs['x0'] captured['atol'] = kwargs['atol'] - return solver_result + return case.solver_result result = solvers.generic( - omega=omega, - dxes=[[numpy.ones(1) for _ in range(3)] for _ in range(2)], - J=j, - epsilon=numpy.ones(2), + omega=case.omega, + dxes=case.dxes, + J=case.j, + epsilon=case.epsilon, matrix_solver=fake_solver, matrix_solver_opts={'atol': 1e-12}, - E_guess=guess, + E_guess=case.guess, ) - assert_allclose(captured['a'].toarray(), (pl @ a0 @ pr).toarray()) - assert_allclose(captured['b'], pl @ (-1j * omega * j)) - assert_allclose(captured['x0'], pl @ guess) + assert_close(captured['a'].toarray(), (case.pl @ case.a0 @ case.pr).toarray()) + assert_close(captured['b'], case.pl @ (-1j * case.omega * case.j)) + assert_close(captured['x0'], case.pl @ case.guess) assert captured['atol'] == 1e-12 - assert_allclose(result, pr @ solver_result) + assert_close(result, case.pr @ case.solver_result) def test_generic_adjoint_preconditions_system_and_guess(monkeypatch) -> None: - omega = 2.0 - a0 = sparse.csr_matrix(numpy.array([[1.0 + 2.0j, 2.0], [3.0 - 1.0j, 4.0]])) - pl = sparse.csr_matrix(numpy.array([[2.0, 0.0], [0.0, 3.0j]])) - pr = sparse.csr_matrix(numpy.array([[0.5, 0.0], [0.0, -2.0j]])) - j = numpy.array([1.0 + 0.5j, -2.0]) - guess = numpy.array([0.25 - 0.75j, 1.5 + 0.5j]) - solver_result = numpy.array([3.0 - 1.0j, -4.0 + 2.0j]) - captured: dict[str, numpy.ndarray | sparse.spmatrix] = {} + case = solver_plumbing_case() + captured: dict[str, object] = {} - monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: a0) - monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (pl, pr)) + monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: case.a0) + monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (case.pl, case.pr)) - def fake_solver(a: sparse.spmatrix, b: numpy.ndarray, **kwargs): + def fake_solver(a, b: numpy.ndarray, **kwargs): captured['a'] = a captured['b'] = b captured['x0'] = kwargs['x0'] captured['rtol'] = kwargs['rtol'] - return solver_result + return case.solver_result result = solvers.generic( - omega=omega, - dxes=[[numpy.ones(1) for _ in range(3)] for _ in range(2)], - J=j, - epsilon=numpy.ones(2), + omega=case.omega, + dxes=case.dxes, + J=case.j, + epsilon=case.epsilon, matrix_solver=fake_solver, matrix_solver_opts={'rtol': 1e-9}, - E_guess=guess, + E_guess=case.guess, adjoint=True, ) - expected_matrix = (pl @ a0 @ pr).T.conjugate() - assert_allclose(captured['a'].toarray(), expected_matrix.toarray()) - assert_allclose(captured['b'], pr.T.conjugate() @ (-1j * omega * j)) - assert_allclose(captured['x0'], pr.T.conjugate() @ guess) + expected_matrix = (case.pl @ case.a0 @ case.pr).T.conjugate() + assert_close(captured['a'].toarray(), expected_matrix.toarray()) + assert_close(captured['b'], case.pr.T.conjugate() @ (-1j * case.omega * case.j)) + assert_close(captured['x0'], case.pr.T.conjugate() @ case.guess) assert captured['rtol'] == 1e-9 - assert_allclose(result, pl.T.conjugate() @ solver_result) + assert_close(result, case.pl.T.conjugate() @ case.solver_result) def test_generic_without_guess_does_not_inject_x0(monkeypatch) -> None: - a0 = sparse.eye(2).tocsr() - pl = sparse.eye(2).tocsr() - pr = sparse.eye(2).tocsr() + case = solver_plumbing_case() captured: dict[str, object] = {} - monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: a0) - monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (pl, pr)) + monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: case.a0) + monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (case.pl, case.pr)) - def fake_solver(a: sparse.spmatrix, b: numpy.ndarray, **kwargs): + def fake_solver(a, b: numpy.ndarray, **kwargs): captured['kwargs'] = kwargs return numpy.array([1.0, -1.0]) result = solvers.generic( omega=1.0, - dxes=[[numpy.ones(1) for _ in range(3)] for _ in range(2)], + dxes=case.dxes, J=numpy.array([2.0, 3.0]), - epsilon=numpy.ones(2), + epsilon=case.epsilon, matrix_solver=fake_solver, ) assert 'x0' not in captured['kwargs'] - assert_allclose(result, [1.0, -1.0]) + assert_close(result, case.pr @ numpy.array([1.0, -1.0])) diff --git a/meanas/test/test_fdmath_functional.py b/meanas/test/test_fdmath_functional.py index 01701e8..368eba3 100644 --- a/meanas/test/test_fdmath_functional.py +++ b/meanas/test/test_fdmath_functional.py @@ -1,9 +1,9 @@ import numpy -from numpy.testing import assert_allclose from ..fdmath import functional as fd_functional from ..fdmath import operators as fd_operators from ..fdmath import vec, unvec +from .utils import assert_close, assert_fields_close SHAPE = (2, 3, 2) @@ -20,13 +20,13 @@ VECTOR_FIELD = (numpy.arange(3 * numpy.prod(SHAPE)).reshape((3, *SHAPE)) + 0.25j def test_deriv_forward_without_dx_matches_numpy_roll() -> None: for axis, deriv in enumerate(fd_functional.deriv_forward()): expected = numpy.roll(SCALAR_FIELD, -1, axis=axis) - SCALAR_FIELD - assert_allclose(deriv(SCALAR_FIELD), expected) + assert_close(deriv(SCALAR_FIELD), expected) def test_deriv_back_without_dx_matches_numpy_roll() -> None: for axis, deriv in enumerate(fd_functional.deriv_back()): expected = SCALAR_FIELD - numpy.roll(SCALAR_FIELD, 1, axis=axis) - assert_allclose(deriv(SCALAR_FIELD), expected) + assert_close(deriv(SCALAR_FIELD), expected) def test_curl_parts_sum_to_full_curl() -> None: @@ -36,18 +36,18 @@ def test_curl_parts_sum_to_full_curl() -> None: back_parts = fd_functional.curl_back_parts(DX_H)(VECTOR_FIELD) for axis in range(3): - assert_allclose(forward_parts[axis][0] + forward_parts[axis][1], curl_forward[axis]) - assert_allclose(back_parts[axis][0] + back_parts[axis][1], curl_back[axis]) + assert_close(forward_parts[axis][0] + forward_parts[axis][1], curl_forward[axis]) + assert_close(back_parts[axis][0] + back_parts[axis][1], curl_back[axis]) def test_derivatives_match_sparse_operators_on_nonuniform_grid() -> None: for axis, deriv in enumerate(fd_functional.deriv_forward(DX_E)): matrix_result = (fd_operators.deriv_forward(DX_E)[axis] @ SCALAR_FIELD.ravel(order='C')).reshape(SHAPE, order='C') - assert_allclose(deriv(SCALAR_FIELD), matrix_result, atol=1e-12, rtol=1e-12) + assert_close(deriv(SCALAR_FIELD), matrix_result, atol=1e-12, rtol=1e-12) for axis, deriv in enumerate(fd_functional.deriv_back(DX_H)): matrix_result = (fd_operators.deriv_back(DX_H)[axis] @ SCALAR_FIELD.ravel(order='C')).reshape(SHAPE, order='C') - assert_allclose(deriv(SCALAR_FIELD), matrix_result, atol=1e-12, rtol=1e-12) + assert_close(deriv(SCALAR_FIELD), matrix_result, atol=1e-12, rtol=1e-12) def test_curls_match_sparse_operators_on_nonuniform_grid() -> None: @@ -56,5 +56,5 @@ def test_curls_match_sparse_operators_on_nonuniform_grid() -> None: matrix_forward = unvec(fd_operators.curl_forward(DX_E) @ vec(VECTOR_FIELD), SHAPE) matrix_back = unvec(fd_operators.curl_back(DX_H) @ vec(VECTOR_FIELD), SHAPE) - assert_allclose(curl_forward, matrix_forward, atol=1e-12, rtol=1e-12) - assert_allclose(curl_back, matrix_back, atol=1e-12, rtol=1e-12) + assert_fields_close(curl_forward, matrix_forward, atol=1e-12, rtol=1e-12) + assert_fields_close(curl_back, matrix_back, atol=1e-12, rtol=1e-12) diff --git a/meanas/test/test_fdmath_operators.py b/meanas/test/test_fdmath_operators.py index bb7fe31..9b1dec0 100644 --- a/meanas/test/test_fdmath_operators.py +++ b/meanas/test/test_fdmath_operators.py @@ -1,14 +1,15 @@ import numpy import pytest -from numpy.testing import assert_allclose, assert_array_equal from ..fdmath import operators, unvec, vec +from ._test_builders import real_ramp +from .utils import assert_close SHAPE = (2, 3, 2) -SCALAR_FIELD = numpy.arange(numpy.prod(SHAPE), dtype=float).reshape(SHAPE, order='C') -VECTOR_LEFT = (numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') + 0.5) -VECTOR_RIGHT = (2.0 + numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') / 3.0) +SCALAR_FIELD = real_ramp(SHAPE) +VECTOR_LEFT = real_ramp((3, *SHAPE), offset=0.5) +VECTOR_RIGHT = real_ramp((3, *SHAPE), scale=1 / 3, offset=2.0) def _apply_scalar_matrix(op: operators.sparse.spmatrix) -> numpy.ndarray: @@ -26,7 +27,7 @@ def _mirrored_indices(size: int, shift_distance: int) -> numpy.ndarray: def test_shift_circ_matches_numpy_roll(axis: int, shift_distance: int) -> None: matrix_result = _apply_scalar_matrix(operators.shift_circ(axis, SHAPE, shift_distance)) expected = numpy.roll(SCALAR_FIELD, -shift_distance, axis=axis) - assert_array_equal(matrix_result, expected) + assert_close(matrix_result, expected) @pytest.mark.parametrize(('axis', 'shift_distance'), [(0, 1), (1, -1), (2, 1)]) @@ -35,7 +36,7 @@ def test_shift_with_mirror_matches_explicit_mirrored_indices(axis: int, shift_di indices = [numpy.arange(length) for length in SHAPE] indices[axis] = _mirrored_indices(SHAPE[axis], shift_distance) expected = SCALAR_FIELD[numpy.ix_(*indices)] - assert_array_equal(matrix_result, expected) + assert_close(matrix_result, expected) @pytest.mark.parametrize( @@ -69,19 +70,19 @@ def test_vec_cross_matches_pointwise_cross_product() -> None: expected[0] = VECTOR_LEFT[1] * VECTOR_RIGHT[2] - VECTOR_LEFT[2] * VECTOR_RIGHT[1] expected[1] = VECTOR_LEFT[2] * VECTOR_RIGHT[0] - VECTOR_LEFT[0] * VECTOR_RIGHT[2] expected[2] = VECTOR_LEFT[0] * VECTOR_RIGHT[1] - VECTOR_LEFT[1] * VECTOR_RIGHT[0] - assert_allclose(matrix_result, expected) + assert_close(matrix_result, expected) def test_avg_forward_matches_half_sum_with_forward_neighbor() -> None: matrix_result = _apply_scalar_matrix(operators.avg_forward(1, SHAPE)) expected = 0.5 * (SCALAR_FIELD + numpy.roll(SCALAR_FIELD, -1, axis=1)) - assert_allclose(matrix_result, expected) + assert_close(matrix_result, expected) def test_avg_back_matches_half_sum_with_backward_neighbor() -> None: matrix_result = _apply_scalar_matrix(operators.avg_back(1, SHAPE)) expected = 0.5 * (SCALAR_FIELD + numpy.roll(SCALAR_FIELD, 1, axis=1)) - assert_allclose(matrix_result, expected) + assert_close(matrix_result, expected) def test_avg_forward_rejects_invalid_shape() -> None: diff --git a/meanas/test/test_fdmath_vectorization.py b/meanas/test/test_fdmath_vectorization.py index 33a9812..c55f7cd 100644 --- a/meanas/test/test_fdmath_vectorization.py +++ b/meanas/test/test_fdmath_vectorization.py @@ -1,12 +1,13 @@ import numpy -from numpy.testing import assert_allclose, assert_array_equal from ..fdmath import unvec, vec +from ._test_builders import complex_ramp, real_ramp +from .utils import assert_close SHAPE = (2, 3, 2) -FIELD = numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') -COMPLEX_FIELD = (FIELD + 0.5j * FIELD).astype(complex) +FIELD = real_ramp((3, *SHAPE)) +COMPLEX_FIELD = complex_ramp((3, *SHAPE), imag_scale=0.5) def test_vec_and_unvec_return_none_for_none_input() -> None: @@ -20,7 +21,7 @@ def test_real_field_round_trip_preserves_shape_and_values() -> None: restored = unvec(vector, SHAPE) assert restored is not None assert restored.shape == (3, *SHAPE) - assert_array_equal(restored, FIELD) + assert_close(restored, FIELD) def test_complex_field_round_trip_preserves_shape_and_values() -> None: @@ -29,7 +30,7 @@ def test_complex_field_round_trip_preserves_shape_and_values() -> None: restored = unvec(vector, SHAPE) assert restored is not None assert restored.shape == (3, *SHAPE) - assert_allclose(restored, COMPLEX_FIELD) + assert_close(restored, COMPLEX_FIELD) def test_unvec_with_two_components_round_trips_vector() -> None: @@ -37,9 +38,9 @@ def test_unvec_with_two_components_round_trips_vector() -> None: field = unvec(vector, SHAPE, nvdim=2) assert field is not None assert field.shape == (2, *SHAPE) - assert_array_equal(vec(field), vector) + assert_close(vec(field), vector) def test_vec_accepts_arraylike_input() -> None: arraylike = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] - assert_array_equal(vec(arraylike), numpy.ravel(arraylike, order='C')) + assert_close(vec(arraylike), numpy.ravel(arraylike, order='C')) diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 03d2b7e..469c220 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -7,7 +7,7 @@ from numpy.typing import NDArray #from numpy.testing import assert_allclose, assert_array_equal from .. import fdtd -from .utils import assert_close, assert_fields_close, PRNG +from .utils import assert_close, assert_fields_close, make_prng from .conftest import FixtureRequest @@ -179,13 +179,14 @@ def j_distribution( shape: tuple[int, ...], j_mag: float, ) -> NDArray[numpy.float64]: + prng = make_prng() j = numpy.zeros(shape) if request.param == 'center': j[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = j_mag elif request.param == '000': j[:, 0, 0, 0] = j_mag elif request.param == 'random': - j[:] = PRNG.uniform(low=-j_mag, high=j_mag, size=shape) + j[:] = prng.uniform(low=-j_mag, high=j_mag, size=shape) return j diff --git a/meanas/test/test_fdtd_base.py b/meanas/test/test_fdtd_base.py index 41b6ad1..bc1f514 100644 --- a/meanas/test/test_fdtd_base.py +++ b/meanas/test/test_fdtd_base.py @@ -1,14 +1,15 @@ import numpy -from numpy.testing import assert_allclose from ..fdmath import functional as fd_functional from ..fdtd import base +from ._test_builders import real_ramp +from .utils import assert_close DT = 0.25 SHAPE = (3, 2, 2, 2) -E_FIELD = numpy.arange(numpy.prod(SHAPE), dtype=float).reshape(SHAPE, order='C') / 5.0 -H_FIELD = (numpy.arange(numpy.prod(SHAPE), dtype=float).reshape(SHAPE, order='C') + 1.0) / 7.0 +E_FIELD = real_ramp(SHAPE, scale=1 / 5) +H_FIELD = real_ramp(SHAPE, scale=1 / 7, offset=1 / 7) EPSILON = 1.5 + E_FIELD / 10.0 MU_FIELD = 2.0 + H_FIELD / 8.0 MU_SCALAR = 3.0 @@ -20,7 +21,7 @@ def test_maxwell_e_without_dxes_matches_unit_spacing_update() -> None: updated = updater(E_FIELD.copy(), H_FIELD.copy(), EPSILON) - assert_allclose(updated, expected) + assert_close(updated, expected) def test_maxwell_h_without_dxes_and_without_mu_matches_unit_spacing_update() -> None: @@ -29,7 +30,7 @@ def test_maxwell_h_without_dxes_and_without_mu_matches_unit_spacing_update() -> updated = updater(E_FIELD.copy(), H_FIELD.copy()) - assert_allclose(updated, expected) + assert_close(updated, expected) def test_maxwell_h_without_dxes_accepts_scalar_and_field_mu() -> None: @@ -37,8 +38,8 @@ def test_maxwell_h_without_dxes_accepts_scalar_and_field_mu() -> None: updated_scalar = updater(E_FIELD.copy(), H_FIELD.copy(), MU_SCALAR) expected_scalar = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD) / MU_SCALAR - assert_allclose(updated_scalar, expected_scalar) + assert_close(updated_scalar, expected_scalar) updated_field = updater(E_FIELD.copy(), H_FIELD.copy(), MU_FIELD) expected_field = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD) / MU_FIELD - assert_allclose(updated_field, expected_field) + assert_close(updated_field, expected_field) diff --git a/meanas/test/test_fdtd_boundaries.py b/meanas/test/test_fdtd_boundaries.py new file mode 100644 index 0000000..d7ba186 --- /dev/null +++ b/meanas/test/test_fdtd_boundaries.py @@ -0,0 +1,62 @@ +import numpy +import pytest +from numpy.testing import assert_allclose + +from ..fdtd.boundaries import conducting_boundary + + +def _axis_index(axis: int, index: int) -> tuple[slice | int, ...]: + coords: list[slice | int] = [slice(None), slice(None), slice(None)] + coords[axis] = index + return tuple(coords) + + +@pytest.mark.parametrize('direction', [0, 1, 2]) +@pytest.mark.parametrize('polarity', [-1, 1]) +def test_conducting_boundary_updates_expected_faces(direction: int, polarity: int) -> None: + e = numpy.arange(3 * 4 * 4 * 4, dtype=float).reshape(3, 4, 4, 4) + h = e.copy() + e0 = e.copy() + h0 = h.copy() + + update_e, update_h = conducting_boundary(direction, polarity) + update_e(e) + update_h(h) + + dirs = [0, 1, 2] + dirs.remove(direction) + u, v = dirs + + if polarity < 0: + boundary = _axis_index(direction, 0) + shifted1 = _axis_index(direction, 1) + + assert_allclose(e[direction][boundary], 0) + assert_allclose(e[u][boundary], e0[u][shifted1]) + assert_allclose(e[v][boundary], e0[v][shifted1]) + assert_allclose(h[direction][boundary], h0[direction][shifted1]) + assert_allclose(h[u][boundary], 0) + assert_allclose(h[v][boundary], 0) + else: + boundary = _axis_index(direction, -1) + shifted1 = _axis_index(direction, -2) + shifted2 = _axis_index(direction, -3) + + assert_allclose(e[direction][boundary], -e0[direction][shifted2]) + assert_allclose(e[direction][shifted1], 0) + assert_allclose(e[u][boundary], e0[u][shifted1]) + assert_allclose(e[v][boundary], e0[v][shifted1]) + assert_allclose(h[direction][boundary], h0[direction][shifted1]) + assert_allclose(h[u][boundary], -h0[u][shifted2]) + assert_allclose(h[u][shifted1], 0) + assert_allclose(h[v][boundary], -h0[v][shifted2]) + assert_allclose(h[v][shifted1], 0) + + +@pytest.mark.parametrize( + ('direction', 'polarity'), + [(-1, 1), (3, 1), (0, 0)], +) +def test_conducting_boundary_rejects_invalid_arguments(direction: int, polarity: int) -> None: + with pytest.raises(Exception): + conducting_boundary(direction, polarity) diff --git a/meanas/test/test_fdtd_energy.py b/meanas/test/test_fdtd_energy.py index 2d15c69..84830cf 100644 --- a/meanas/test/test_fdtd_energy.py +++ b/meanas/test/test_fdtd_energy.py @@ -1,13 +1,14 @@ import numpy -from numpy.testing import assert_allclose from .. import fdtd from ..fdtd import energy as fdtd_energy +from ._test_builders import real_ramp, unit_dxes +from .utils import assert_close SHAPE = (2, 2, 2) DT = 0.25 -UNIT_DXES = tuple(tuple(numpy.ones(length) for length in SHAPE) for _ in range(2)) +UNIT_DXES = unit_dxes(SHAPE) DXES = ( ( numpy.array([1.0, 1.5]), @@ -20,11 +21,11 @@ DXES = ( numpy.array([0.7, 1.3]), ), ) -E0 = numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') +E0 = real_ramp((3, *SHAPE)) E1 = E0 + 0.5 E2 = E0 + 1.0 E3 = E0 + 1.5 -H0 = (numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') + 2.0) / 3.0 +H0 = real_ramp((3, *SHAPE), scale=1 / 3, offset=2 / 3) H1 = H0 + 0.25 H2 = H0 + 0.5 H3 = H0 + 0.75 @@ -36,14 +37,14 @@ MU = 1.5 + H0 / 10.0 def test_poynting_default_spacing_matches_explicit_unit_spacing() -> None: default_spacing = fdtd.poynting(E1, H1) explicit_spacing = fdtd.poynting(E1, H1, dxes=UNIT_DXES) - assert_allclose(default_spacing, explicit_spacing) + assert_close(default_spacing, explicit_spacing) def test_poynting_divergence_matches_precomputed_poynting_vector() -> None: s = fdtd.poynting(E2, H2, dxes=DXES) from_fields = fdtd.poynting_divergence(e=E2, h=H2, dxes=DXES) from_vector = fdtd.poynting_divergence(s=s) - assert_allclose(from_fields, from_vector) + assert_close(from_fields, from_vector) def test_delta_energy_h2e_matches_direct_dxmul_formula() -> None: @@ -64,7 +65,7 @@ def test_delta_energy_h2e_matches_direct_dxmul_formula() -> None: mu=MU, dxes=DXES, ) - assert_allclose(actual, expected) + assert_close(actual, expected) def test_delta_energy_e2h_matches_direct_dxmul_formula() -> None: @@ -85,14 +86,14 @@ def test_delta_energy_e2h_matches_direct_dxmul_formula() -> None: mu=MU, dxes=DXES, ) - assert_allclose(actual, expected) + assert_close(actual, expected) def test_delta_energy_j_defaults_to_unit_cell_volume() -> None: expected = (J0 * E1).sum(axis=0) - assert_allclose(fdtd.delta_energy_j(j0=J0, e1=E1), expected) + assert_close(fdtd.delta_energy_j(j0=J0, e1=E1), expected) def test_dxmul_defaults_to_unit_materials_and_spacing() -> None: expected = E1.sum(axis=0) + H1.sum(axis=0) - assert_allclose(fdtd_energy.dxmul(E1, H1), expected) + assert_close(fdtd_energy.dxmul(E1, H1), expected) diff --git a/meanas/test/test_fdtd_misc.py b/meanas/test/test_fdtd_misc.py new file mode 100644 index 0000000..65dc713 --- /dev/null +++ b/meanas/test/test_fdtd_misc.py @@ -0,0 +1,42 @@ +import numpy +import pytest + +from ..fdtd.misc import gaussian_beam, gaussian_packet, ricker_pulse + + +@pytest.mark.parametrize('one_sided', [False, True]) +def test_gaussian_packet_accepts_array_input(one_sided: bool) -> None: + dt = 0.01 + source, delay = gaussian_packet(1.55, 0.1, dt, one_sided=one_sided) + steps = numpy.array([0, int(numpy.ceil(delay / dt)) + 5]) + envelope, cc, ss = source(steps) + + assert envelope.shape == (2,) + assert numpy.isfinite(envelope).all() + assert numpy.isfinite(cc).all() + assert numpy.isfinite(ss).all() + if one_sided: + assert envelope[-1] == pytest.approx(1.0) + + +def test_ricker_pulse_returns_finite_values() -> None: + source, delay = ricker_pulse(1.55, 0.01) + envelope, cc, ss = source(numpy.array([0, 1, 2])) + + assert numpy.isfinite(delay) + assert numpy.isfinite(envelope).all() + assert numpy.isfinite(cc).all() + assert numpy.isfinite(ss).all() + + +def test_gaussian_beam_centered_grid_is_finite_and_normalized() -> None: + beam = gaussian_beam( + xyz=[numpy.linspace(-1, 1, 3), numpy.linspace(-1, 1, 3), numpy.linspace(-1, 1, 3)], + center=[0, 0, 0], + waist_radius=1.0, + wl=1.55, + ) + + row = beam[:, :, beam.shape[2] // 2] + assert numpy.isfinite(beam).all() + assert numpy.linalg.norm(row) == pytest.approx(1.0) diff --git a/meanas/test/test_fdtd_pml.py b/meanas/test/test_fdtd_pml.py new file mode 100644 index 0000000..6d118c6 --- /dev/null +++ b/meanas/test/test_fdtd_pml.py @@ -0,0 +1,44 @@ +import numpy +import pytest + +from ..fdtd.pml import cpml_params, updates_with_cpml + + +@pytest.mark.parametrize( + ('axis', 'polarity', 'thickness', 'epsilon_eff'), + [(3, 1, 4, 1.0), (0, 0, 4, 1.0), (0, 1, 2, 1.0), (0, 1, 4, 0.0)], +) +def test_cpml_params_reject_invalid_arguments(axis: int, polarity: int, thickness: int, epsilon_eff: float) -> None: + with pytest.raises(Exception): + cpml_params(axis=axis, polarity=polarity, dt=0.1, thickness=thickness, epsilon_eff=epsilon_eff) + + +def test_cpml_params_shapes_and_region() -> None: + params = cpml_params(axis=1, polarity=1, dt=0.1, thickness=3) + p0e, p1e, p2e = params['param_e'] + p0h, p1h, p2h = params['param_h'] + + assert p0e.shape == (1, 3, 1) + assert p1e.shape == (1, 3, 1) + assert p2e.shape == (1, 3, 1) + assert p0h.shape == (1, 3, 1) + assert p1h.shape == (1, 3, 1) + assert p2h.shape == (1, 3, 1) + assert params['region'][1] == slice(-3, None) + + +def test_updates_with_cpml_keeps_zero_fields_zero() -> None: + shape = (3, 4, 4, 4) + epsilon = numpy.ones(shape, dtype=float) + e = numpy.zeros(shape, dtype=float) + h = numpy.zeros(shape, dtype=float) + dxes = [[numpy.ones(4), numpy.ones(4), numpy.ones(4)] for _ in range(2)] + params = [[None, None] for _ in range(3)] + params[0][0] = cpml_params(axis=0, polarity=-1, dt=0.1, thickness=3) + + update_e, update_h = updates_with_cpml(params, dt=0.1, dxes=dxes, epsilon=epsilon) + update_e(e, h, epsilon) + update_h(e, h) + + assert not e.any() + assert not h.any() diff --git a/meanas/test/test_import_fallbacks.py b/meanas/test/test_import_fallbacks.py index abee887..d1ecca9 100644 --- a/meanas/test/test_import_fallbacks.py +++ b/meanas/test/test_import_fallbacks.py @@ -4,6 +4,16 @@ import pathlib import meanas from ..fdfd import bloch +from .utils import assert_close + + +def _reload(module): + return importlib.reload(module) + + +def _restore_reloaded(monkeypatch, module): + monkeypatch.undo() + return _reload(module) def test_meanas_import_survives_readme_open_failure(monkeypatch) -> None: # type: ignore[no-untyped-def] @@ -15,14 +25,13 @@ def test_meanas_import_survives_readme_open_failure(monkeypatch) -> None: # typ return original_open(self, *args, **kwargs) monkeypatch.setattr(pathlib.Path, 'open', failing_open) - reloaded = importlib.reload(meanas) + reloaded = _reload(meanas) assert reloaded.__version__ == '0.10' assert reloaded.__author__ == 'Jan Petykiewicz' assert reloaded.__doc__ is not None - monkeypatch.undo() - importlib.reload(meanas) + _restore_reloaded(monkeypatch, meanas) def test_bloch_reloads_with_numpy_fft_when_pyfftw_is_unavailable(monkeypatch) -> None: # type: ignore[no-untyped-def] @@ -34,10 +43,9 @@ def test_bloch_reloads_with_numpy_fft_when_pyfftw_is_unavailable(monkeypatch) -> return original_import(name, globals, locals, fromlist, level) monkeypatch.setattr(builtins, '__import__', fake_import) - reloaded = importlib.reload(bloch) + reloaded = _reload(bloch) assert reloaded.fftn.__module__ == 'numpy.fft' assert reloaded.ifftn.__module__ == 'numpy.fft' - monkeypatch.undo() - importlib.reload(bloch) + _restore_reloaded(monkeypatch, bloch) diff --git a/meanas/test/test_regressions.py b/meanas/test/test_regressions.py deleted file mode 100644 index 8231880..0000000 --- a/meanas/test/test_regressions.py +++ /dev/null @@ -1,238 +0,0 @@ -import numpy -import pytest # type: ignore -from numpy.testing import assert_allclose -from scipy import sparse - -from ..eigensolvers import power_iteration, rayleigh_quotient_iteration, signed_eigensolve -from ..fdfd import eme, farfield -from ..fdtd.boundaries import conducting_boundary -from ..fdtd.misc import gaussian_beam, gaussian_packet, ricker_pulse -from ..fdtd.pml import cpml_params, updates_with_cpml - - -def _axis_index(axis: int, index: int) -> tuple[slice | int, ...]: - coords: list[slice | int] = [slice(None), slice(None), slice(None)] - coords[axis] = index - return tuple(coords) - - -@pytest.mark.parametrize('one_sided', [False, True]) -def test_gaussian_packet_accepts_array_input(one_sided: bool) -> None: - dt = 0.01 - source, delay = gaussian_packet(1.55, 0.1, dt, one_sided=one_sided) - steps = numpy.array([0, int(numpy.ceil(delay / dt)) + 5]) - envelope, cc, ss = source(steps) - - assert envelope.shape == (2,) - assert numpy.isfinite(envelope).all() - assert numpy.isfinite(cc).all() - assert numpy.isfinite(ss).all() - if one_sided: - assert envelope[-1] == pytest.approx(1.0) - - -def test_ricker_pulse_returns_finite_values() -> None: - source, delay = ricker_pulse(1.55, 0.01) - envelope, cc, ss = source(numpy.array([0, 1, 2])) - - assert numpy.isfinite(delay) - assert numpy.isfinite(envelope).all() - assert numpy.isfinite(cc).all() - assert numpy.isfinite(ss).all() - - -def test_gaussian_beam_centered_grid_is_finite_and_normalized() -> None: - beam = gaussian_beam( - xyz=[numpy.linspace(-1, 1, 3), numpy.linspace(-1, 1, 3), numpy.linspace(-1, 1, 3)], - center=[0, 0, 0], - waist_radius=1.0, - wl=1.55, - ) - - row = beam[:, :, beam.shape[2] // 2] - assert numpy.isfinite(beam).all() - assert numpy.linalg.norm(row) == pytest.approx(1.0) - - -@pytest.mark.parametrize('direction', [0, 1, 2]) -@pytest.mark.parametrize('polarity', [-1, 1]) -def test_conducting_boundary_updates_expected_faces(direction: int, polarity: int) -> None: - e = numpy.arange(3 * 4 * 4 * 4, dtype=float).reshape(3, 4, 4, 4) - h = e.copy() - e0 = e.copy() - h0 = h.copy() - - update_e, update_h = conducting_boundary(direction, polarity) - update_e(e) - update_h(h) - - dirs = [0, 1, 2] - dirs.remove(direction) - u, v = dirs - - if polarity < 0: - boundary = _axis_index(direction, 0) - shifted1 = _axis_index(direction, 1) - - assert_allclose(e[direction][boundary], 0) - assert_allclose(e[u][boundary], e0[u][shifted1]) - assert_allclose(e[v][boundary], e0[v][shifted1]) - assert_allclose(h[direction][boundary], h0[direction][shifted1]) - assert_allclose(h[u][boundary], 0) - assert_allclose(h[v][boundary], 0) - else: - boundary = _axis_index(direction, -1) - shifted1 = _axis_index(direction, -2) - shifted2 = _axis_index(direction, -3) - - assert_allclose(e[direction][boundary], -e0[direction][shifted2]) - assert_allclose(e[direction][shifted1], 0) - assert_allclose(e[u][boundary], e0[u][shifted1]) - assert_allclose(e[v][boundary], e0[v][shifted1]) - assert_allclose(h[direction][boundary], h0[direction][shifted1]) - assert_allclose(h[u][boundary], -h0[u][shifted2]) - assert_allclose(h[u][shifted1], 0) - assert_allclose(h[v][boundary], -h0[v][shifted2]) - assert_allclose(h[v][shifted1], 0) - - -@pytest.mark.parametrize( - ('direction', 'polarity'), - [(-1, 1), (3, 1), (0, 0)], - ) -def test_conducting_boundary_rejects_invalid_arguments(direction: int, polarity: int) -> None: - with pytest.raises(Exception): - conducting_boundary(direction, polarity) - - -def test_get_abcd_returns_sparse_block_matrix(monkeypatch: pytest.MonkeyPatch) -> None: - t = numpy.array([[1.0, 0.0], [0.0, 2.0]]) - r = numpy.array([[0.0, 0.1], [0.2, 0.0]]) - monkeypatch.setattr(eme, 'get_tr', lambda *args, **kwargs: (t, r)) - - abcd = eme.get_abcd(None, None, None, None) - - assert sparse.issparse(abcd) - assert abcd.shape == (4, 4) - assert numpy.isfinite(abcd.toarray()).all() - - -def test_get_s_force_reciprocal_symmetrizes_output(monkeypatch: pytest.MonkeyPatch) -> None: - left = object() - right = object() - - def fake_get_tr(_eh_l, wavenumbers_l, _eh_r, _wavenumbers_r, **kwargs): - if wavenumbers_l is left: - return ( - numpy.array([[1.0, 2.0], [0.5, 1.0]]), - numpy.array([[0.0, 1.0], [2.0, 0.0]]), - ) - return ( - numpy.array([[1.0, -1.0], [0.0, 1.0]]), - numpy.array([[0.0, 0.5], [1.5, 0.0]]), - ) - - monkeypatch.setattr(eme, 'get_tr', fake_get_tr) - ss = eme.get_s(None, left, None, right, force_reciprocal=True) - - assert_allclose(ss, ss.T) - - -def test_farfield_roundtrip_supports_rectangular_arrays() -> None: - e_near = [numpy.zeros((4, 8), dtype=complex), numpy.zeros((4, 8), dtype=complex)] - h_near = [numpy.zeros((4, 8), dtype=complex), numpy.zeros((4, 8), dtype=complex)] - e_near[0][1, 3] = 1.0 + 0.25j - h_near[1][2, 5] = -0.5j - - ff = farfield.near_to_farfield(e_near, h_near, dx=0.2, dy=0.3, padded_size=(4, 8)) - restored = farfield.far_to_nearfield( - [field.copy() for field in ff['E']], - [field.copy() for field in ff['H']], - ff['dkx'], - ff['dky'], - padded_size=(4, 8), - ) - - assert isinstance(ff['dkx'], float) - assert isinstance(ff['dky'], float) - assert ff['E'][0].shape == (4, 8) - assert restored['E'][0].shape == (4, 8) - assert restored['H'][0].shape == (4, 8) - assert restored['dx'] == pytest.approx(0.2) - assert restored['dy'] == pytest.approx(0.3) - assert numpy.isfinite(restored['E'][0]).all() - assert numpy.isfinite(restored['H'][0]).all() - - -@pytest.mark.parametrize( - ('axis', 'polarity', 'thickness', 'epsilon_eff'), - [(3, 1, 4, 1.0), (0, 0, 4, 1.0), (0, 1, 2, 1.0), (0, 1, 4, 0.0)], - ) -def test_cpml_params_reject_invalid_arguments(axis: int, polarity: int, thickness: int, epsilon_eff: float) -> None: - with pytest.raises(Exception): - cpml_params(axis=axis, polarity=polarity, dt=0.1, thickness=thickness, epsilon_eff=epsilon_eff) - - -def test_cpml_params_shapes_and_region() -> None: - params = cpml_params(axis=1, polarity=1, dt=0.1, thickness=3) - p0e, p1e, p2e = params['param_e'] - p0h, p1h, p2h = params['param_h'] - - assert p0e.shape == (1, 3, 1) - assert p1e.shape == (1, 3, 1) - assert p2e.shape == (1, 3, 1) - assert p0h.shape == (1, 3, 1) - assert p1h.shape == (1, 3, 1) - assert p2h.shape == (1, 3, 1) - assert params['region'][1] == slice(-3, None) - - -def test_updates_with_cpml_keeps_zero_fields_zero() -> None: - shape = (3, 4, 4, 4) - epsilon = numpy.ones(shape, dtype=float) - e = numpy.zeros(shape, dtype=float) - h = numpy.zeros(shape, dtype=float) - dxes = [[numpy.ones(4), numpy.ones(4), numpy.ones(4)] for _ in range(2)] - params = [[None, None] for _ in range(3)] - params[0][0] = cpml_params(axis=0, polarity=-1, dt=0.1, thickness=3) - - update_e, update_h = updates_with_cpml(params, dt=0.1, dxes=dxes, epsilon=epsilon) - update_e(e, h, epsilon) - update_h(e, h) - - assert not e.any() - assert not h.any() - - -def test_power_iteration_finds_dominant_mode() -> None: - operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() - eigval, eigvec = power_iteration(operator, guess_vector=numpy.ones(4, dtype=complex), iterations=20) - - assert eigval == pytest.approx(5.0, rel=1e-6) - assert abs(eigvec[0]) > abs(eigvec[1]) - - -def test_rayleigh_quotient_iteration_refines_known_mode() -> None: - operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() - - def solver(matrix: sparse.spmatrix, rhs: numpy.ndarray) -> numpy.ndarray: - return numpy.linalg.lstsq(matrix.toarray(), rhs, rcond=None)[0] - - eigval, eigvec = rayleigh_quotient_iteration( - operator, - numpy.array([1.0, 0.1, 0.0, 0.0], dtype=complex), - iterations=8, - solver=solver, - ) - - residual = numpy.linalg.norm(operator @ eigvec - eigval * eigvec) - assert eigval == pytest.approx(3.0, rel=1e-6) - assert residual < 1e-8 - - -def test_signed_eigensolve_returns_largest_positive_modes() -> None: - operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr() - eigvals, eigvecs = signed_eigensolve(operator, how_many=2) - - assert_allclose(eigvals, [3.0, 5.0], atol=1e-6) - assert eigvecs.shape == (4, 2) diff --git a/meanas/test/utils.py b/meanas/test/utils.py index f6f9230..3bafd49 100644 --- a/meanas/test/utils.py +++ b/meanas/test/utils.py @@ -2,7 +2,8 @@ import numpy from numpy.typing import NDArray -PRNG = numpy.random.RandomState(12345) +def make_prng(seed: int = 12345) -> numpy.random.RandomState: + return numpy.random.RandomState(seed) def assert_fields_close( @@ -29,4 +30,3 @@ def assert_close( **kwargs, ) -> None: numpy.testing.assert_allclose(x, y, *args, **kwargs) - From d99ef96c9694737a3923053b530eb46cc0186f09 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 12:10:14 -0700 Subject: [PATCH 407/437] [fdtd.phasor] add accumulate_phasor* --- examples/fdtd.py | 25 ++- examples/waveguide.py | 338 ++++++++++++++++++++++++++++++++ meanas/fdtd/__init__.py | 18 ++ meanas/fdtd/phasor.py | 126 ++++++++++++ meanas/test/test_fdtd_phasor.py | 208 ++++++++++++++++++++ 5 files changed, 708 insertions(+), 7 deletions(-) create mode 100644 examples/waveguide.py create mode 100644 meanas/fdtd/phasor.py create mode 100644 meanas/test/test_fdtd_phasor.py diff --git a/examples/fdtd.py b/examples/fdtd.py index 284ce07..2a50ddc 100644 --- a/examples/fdtd.py +++ b/examples/fdtd.py @@ -151,7 +151,7 @@ def main(): # Source parameters and function - source_phasor, _delay = gaussian_packet(wl=wl, dwl=100, dt=dt, turn_on=1e-5) + source_phasor, delay = gaussian_packet(wl=wl, dwl=100, dt=dt, turn_on=1e-5) aa, cc, ss = source_phasor(numpy.arange(max_t)) srca_real = aa * cc src_maxt = numpy.argwhere(numpy.diff(aa < 1e-5))[-1] @@ -160,7 +160,8 @@ def main(): Jph = numpy.zeros_like(epsilon, dtype=complex) Jph[1, *(grid.shape // 2)] = epsilon[1, *(grid.shape // 2)] - Eph = numpy.zeros_like(Jph) + omega = 2 * numpy.pi / wl + eph = numpy.zeros((1, *epsilon.shape), dtype=complex) # #### Run a bunch of iterations #### output_file = h5py.File('simulation_output.h5', 'w') @@ -169,6 +170,7 @@ def main(): update_E(ee, hh, epsilon) if tt < src_maxt: + # This codebase uses E -= dt * J / epsilon for electric-current injection. ee[1, *(grid.shape // 2)] -= srca_real[tt] update_H(ee, hh) @@ -185,17 +187,26 @@ def main(): print(f'saving E-field at iteration {tt}') output_file[f'/E_t{tt}'] = ee[:, :, :, ee.shape[3] // 2] - Eph += (cc[tt] - 1j * ss[tt]) * phasor_norm * ee + fdtd.accumulate_phasor( + eph, + omega, + dt, + ee, + tt, + # The pulse is delayed relative to t=0, so the readout needs the same phase shift. + offset_steps=0.5 - delay / dt, + # accumulate_phasor() already includes dt, so undo the dt in phasor_norm here. + weight=phasor_norm / dt, + ) - omega = 2 * numpy.pi / wl - Eph *= numpy.exp(-1j * dt / 2 * omega) + Eph = eph[0] b = -1j * omega * Jph dxes_fdfd = copy.deepcopy(dxes) for pp in (-1, +1): for dd in range(3): stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_air ** 2, thickness=pml_thickness) - A = e_full(omega=omega, dxes=dxes, epsilon=epsilon) - residual = norm(A @ vec(ee) - vec(b)) / norm(vec(b)) + A = e_full(omega=omega, dxes=dxes_fdfd, epsilon=epsilon) + residual = norm(A @ vec(Eph) - vec(b)) / norm(vec(b)) print(f'FDFD residual is {residual}') diff --git a/examples/waveguide.py b/examples/waveguide.py new file mode 100644 index 0000000..1bb02fa --- /dev/null +++ b/examples/waveguide.py @@ -0,0 +1,338 @@ +""" +Example code for running an OpenCL FDTD simulation + +See main() for simulation setup. +""" +from typing import Callable +import logging +import time +import copy + +import numpy +import h5py +from numpy.linalg import norm + +import gridlock +import meanas +from meanas import fdtd, fdfd +from meanas.fdtd import cpml_params, updates_with_cpml +from meanas.fdtd.misc import gaussian_packet + +from meanas.fdmath import vec, unvec, vcfdfield_t, cfdfield_t, fdfield_t, dx_lists_t +from meanas.fdfd import waveguide_3d, functional, scpml, operators +from meanas.fdfd.solvers import generic as generic_solver +from meanas.fdfd.operators import e_full +from meanas.fdfd.scpml import stretch_with_scpml + + +logging.basicConfig(level=logging.DEBUG) +for pp in ('matplotlib', 'PIL'): + logging.getLogger(pp).setLevel(logging.WARNING) + +logger = logging.getLogger(__name__) + + +def pcolor(vv, fig=None, ax=None) -> None: + if fig is None: + assert ax is None + fig, ax = pyplot.subplots() + mb = ax.pcolor(vv, cmap='seismic', norm=colors.CenteredNorm()) + fig.colorbar(mb) + ax.set_aspect('equal') + + +def draw_grid( + *, + dx: float, + pml_thickness: int, + n_wg: float = 3.476, # Si index @ 1550 + n_cladding: float = 1.00, # Air index + wg_w: float = 400, + wg_th: float = 200, + ) -> tuple[gridlock.Grid, fdfield_t]: + """ Create the grid and draw the device """ + # Half-dimensions of the simulation grid + xyz_max = numpy.array([800, 900, 600]) + (pml_thickness + 2) * dx + + # Coordinates of the edges of the cells. + half_edge_coords = [numpy.arange(dx / 2, m + dx / 2, step=dx) for m in xyz_max] + edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords] + + grid = gridlock.Grid(edge_coords) + epsilon = grid.allocate(n_cladding**2, dtype=numpy.float32) + grid.draw_cuboid( + epsilon, + x = dict(center=0, span=8e3), + y = dict(center=0, span=wg_w), + z = dict(center=0, span=wg_th), + foreground = n_wg ** 2, + ) + + return grid, epsilon + + +def get_waveguide_mode( + *, + grid: gridlock.Grid, + dxes: dx_lists_t, + omega: float, + epsilon: fdfield_t, + ) -> tuple[vcfdfield_t, vcfdfield_t]: + """ Create a mode source and overlap window """ + dims = numpy.array([[-10, -20, -15], + [-10, 20, 15]]) * [[numpy.median(numpy.real(dx)) for dx in dxes[0]]] + ind_dims = (grid.pos2ind(dims[0], which_shifts=None).astype(int), + grid.pos2ind(dims[1], which_shifts=None).astype(int)) + wg_args = dict( + slices = [slice(i, f+1) for i, f in zip(*ind_dims)], + dxes = dxes, + axis = 0, + polarity = +1, + ) + + wg_results = waveguide_3d.solve_mode(mode_number=0, omega=omega, epsilon=epsilon, **wg_args) + J = waveguide_3d.compute_source(E=wg_results['E'], wavenumber=wg_results['wavenumber'], + omega=omega, epsilon=epsilon, **wg_args) + + e_overlap = waveguide_3d.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], **wg_args, omega=omega) + return J, e_overlap + + +def main( + *, + solver: Callable = generic_solver, + dx: float = 40, # discretization (nm / cell) + pml_thickness: int = 10, # (number of cells) + wl: float = 1550, # Excitation wavelength + wg_w: float = 600, # Waveguide width + wg_th: float = 220, # Waveguide thickness + ): + omega = 2 * numpy.pi / wl + + grid, epsilon = draw_grid(dx=dx, pml_thickness=pml_thickness) + + # Add PML + dxes = [grid.dxyz, grid.autoshifted_dxyz()] + for a in (0, 1, 2): + for p in (-1, 1): + dxes = scpml.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p, thickness=pml_thickness) + + + J, e_overlap = get_waveguide_mode(grid=grid, dxes=dxes, omega=omega, epsilon=epsilon) + + + pecg = numpy.zeros_like(epsilon) + # pecg.draw_cuboid(pecg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) + # pecg.visualize_isosurface(pecg) + + pmcg = numpy.zeros_like(epsilon) + # grid.draw_cuboid(pmcg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) + # grid.visualize_isosurface(pmcg) + + +# ss = (1, slice(None), J.shape[2]//2+6, slice(None)) +# pcolor(J3[ss].T.imag) +# pcolor((numpy.abs(J3).sum(axis=(0, 2)) > 0).astype(float).T) +# pyplot.show(block=True) + + E_fdfd = fdfd_solve( + omega = omega, + dxes = dxes, + epsilon = epsilon, + J = J, + pec = pecg, + pmc = pmcg, + ) + + + # + # Plot results + # + center = grid.pos2ind([0, 0, 0], None).astype(int) + fig, axes = pyplot.subplots(2, 2) + pcolor(numpy.real(E[1][center[0], :, :]).T, fig=fig, ax=axes[0, 0]) + axes[0, 1].plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10)) + axes[0, 1].grid(alpha=0.6) + axes[0, 1].set_ylabel('log10 of field') + pcolor(numpy.real(E[1][:, :, center[2]]).T, fig=fig, ax=axes[1, 0]) + + def poyntings(E): + H = functional.e2h(omega, dxes)(E) + poynting = fdtd.poynting(e=E, h=H.conj(), dxes=dxes) + cross1 = operators.poynting_e_cross(vec(E), dxes) @ vec(H).conj() + cross2 = operators.poynting_h_cross(vec(H), dxes) @ vec(E).conj() * -1 + s1 = 0.5 * unvec(numpy.real(cross1), grid.shape) + s2 = 0.5 * unvec(numpy.real(cross2), grid.shape) + s0 = 0.5 * poynting.real +# s2 = poynting.imag + return s0, s1, s2 + + s0x, s1x, s2x = poyntings(E) + axes[1, 1].plot(s0x[0].sum(axis=2).sum(axis=1), label='s0', marker='.') + axes[1, 1].plot(s1x[0].sum(axis=2).sum(axis=1), label='s1', marker='.') + axes[1, 1].plot(s2x[0].sum(axis=2).sum(axis=1), label='s2', marker='.') + axes[1, 1].plot(E[1][:, center[1], center[2]].real.T, label='Ey', marker='x') + axes[1, 1].grid(alpha=0.6) + axes[1, 1].legend() + + q = [] + for i in range(-5, 30): + e_ovl_rolled = numpy.roll(e_overlap, i, axis=1) + q += [numpy.abs(vec(E) @ vec(e_ovl_rolled).conj())] + fig, ax = pyplot.subplots() + ax.plot(q, marker='.') + ax.grid(alpha=0.6) + ax.set_title('Overlap with mode') + + logger.info('Average overlap with mode:', sum(q[8:32]) / len(q[8:32])) + + pyplot.show(block=True) + + +def fdfd_solve( + *, + omega: float, + dxes = dx_lists_t, + epsilon: fdfield_t, + J: cfdfield_t, + pec: fdfield_t, + pmc: fdfield_t, + ) -> cfdfield_t: + """ Construct and run the solve """ + sim_args = dict( + omega = omega, + dxes = dxes, + epsilon = vec(epsilon), + pec = vec(pecg), + pmc = vec(pmcg), + ) + + x = solver(J=vec(J), **sim_args) + + b = -1j * omega * vec(J) + A = operators.e_full(**sim_args).tocsr() + logger.info('Norm of the residual is ', norm(A @ x - b) / norm(b)) + + E = unvec(x, epsilon.shape[1:]) + return E + + +def main2(): + dtype = numpy.float32 + max_t = 3600 # number of timesteps + + dx = 40 # discretization (nm/cell) + pml_thickness = 8 # (number of cells) + + wl = 1550 # Excitation wavelength and fwhm + dwl = 100 + + # Device design parameters + xy_size = numpy.array([10, 10]) + a = 430 + r = 0.285 + th = 170 + + # refractive indices + n_slab = 3.408 # InGaAsP(80, 50) @ 1550nm + n_cladding = 1.0 # air + + # Half-dimensions of the simulation grid + xy_max = (xy_size + 1) * a * [1, numpy.sqrt(3)/2] + z_max = 1.6 * a + xyz_max = numpy.hstack((xy_max, z_max)) + pml_thickness * dx + + # Coordinates of the edges of the cells. The fdtd package can only do square grids at the moment. + half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max] + edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords] + + # #### Create the grid, mask, and draw the device #### + grid = gridlock.Grid(edge_coords) + epsilon = grid.allocate(n_cladding ** 2, dtype=dtype) + grid.draw_slab( + epsilon, + slab = dict(axis='z', center=0, span=th), + foreground = n_slab ** 2, + ) + + + print(f'{grid.shape=}') + + dt = dx * 0.99 / numpy.sqrt(3) + ee = numpy.zeros_like(epsilon, dtype=dtype) + hh = numpy.zeros_like(epsilon, dtype=dtype) + + dxes = [grid.dxyz, grid.autoshifted_dxyz()] + + # PMLs in every direction + pml_params = [ + [cpml_params(axis=dd, polarity=pp, dt=dt, thickness=pml_thickness, epsilon_eff=n_cladding ** 2) + for pp in (-1, +1)] + for dd in range(3)] + update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt, dxes=dxes, epsilon=epsilon) + + # sample_interval = numpy.floor(1 / (2 * 1 / wl * dt)).astype(int) + # print(f'Save time interval would be {sample_interval} * dt = {sample_interval * dt:3g}') + + + # Source parameters and function + source_phasor, delay = gaussian_packet(wl=wl, dwl=100, dt=dt, turn_on=1e-5) + aa, cc, ss = source_phasor(numpy.arange(max_t)) + srca_real = aa * cc + src_maxt = numpy.argwhere(numpy.diff(aa < 1e-5))[-1] + assert aa[src_maxt - 1] >= 1e-5 + phasor_norm = dt / (aa * cc * cc).sum() + + Jph = numpy.zeros_like(epsilon, dtype=complex) + Jph[1, *(grid.shape // 2)] = epsilon[1, *(grid.shape // 2)] + omega = 2 * numpy.pi / wl + eph = numpy.zeros((1, *epsilon.shape), dtype=complex) + + # #### Run a bunch of iterations #### + output_file = h5py.File('simulation_output.h5', 'w') + start = time.perf_counter() + for tt in range(max_t): + update_E(ee, hh, epsilon) + + if tt < src_maxt: + # This codebase uses E -= dt * J / epsilon for electric-current injection. + ee[1, *(grid.shape // 2)] -= srca_real[tt] + update_H(ee, hh) + + avg_rate = (tt + 1) / (time.perf_counter() - start) + + if tt % 200 == 0: + print(f'iteration {tt}: average {avg_rate} iterations per sec') + E_energy_sum = (ee * ee * epsilon).sum() + print(f'{E_energy_sum=}') + + # Save field slices + if (tt % 20 == 0 and (max_t - tt <= 1000 or tt <= 2000)) or tt == max_t - 1: + print(f'saving E-field at iteration {tt}') + output_file[f'/E_t{tt}'] = ee[:, :, :, ee.shape[3] // 2] + + fdtd.accumulate_phasor( + eph, + omega, + dt, + ee, + tt, + # The pulse is delayed relative to t=0, so the readout needs the same phase shift. + offset_steps=0.5 - delay / dt, + # accumulate_phasor() already includes dt, so undo the dt in phasor_norm here. + weight=phasor_norm / dt, + ) + + Eph = eph[0] + b = -1j * omega * Jph + dxes_fdfd = copy.deepcopy(dxes) + for pp in (-1, +1): + for dd in range(3): + stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_cladding ** 2, thickness=pml_thickness) + A = e_full(omega=omega, dxes=dxes_fdfd, epsilon=epsilon) + residual = norm(A @ vec(Eph) - vec(b)) / norm(vec(b)) + print(f'FDFD residual is {residual}') + + +if __name__ == '__main__': + main() diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py index 33b1995..2334338 100644 --- a/meanas/fdtd/__init__.py +++ b/meanas/fdtd/__init__.py @@ -144,6 +144,18 @@ It is often useful to excite the simulation with an arbitrary broadband pulse an extract the frequency-domain response by performing an on-the-fly Fourier transform of the time-domain fields. +`accumulate_phasor` in `meanas.fdtd.phasor` performs the phase accumulation for one +or more target frequencies, while leaving source normalization and simulation-loop +policy to the caller. Convenience wrappers `accumulate_phasor_e`, +`accumulate_phasor_h`, and `accumulate_phasor_j` apply the usual Yee time offsets. +The helpers accumulate + +$$ \Delta_t \sum_l w_l e^{-i \omega t_l} f_l $$ + +with caller-provided sample times and weights. In this codebase the matching +electric-current convention is typically `E -= dt * J / epsilon` in FDTD and +`-i \omega J` on the right-hand side of the FDFD wave equation. + The Ricker wavelet (normalized second derivative of a Gaussian) is commonly used for the pulse shape. It can be written @@ -178,3 +190,9 @@ from .energy import ( from .boundaries import ( conducting_boundary as conducting_boundary, ) +from .phasor import ( + accumulate_phasor as accumulate_phasor, + accumulate_phasor_e as accumulate_phasor_e, + accumulate_phasor_h as accumulate_phasor_h, + accumulate_phasor_j as accumulate_phasor_j, + ) diff --git a/meanas/fdtd/phasor.py b/meanas/fdtd/phasor.py new file mode 100644 index 0000000..f3154ee --- /dev/null +++ b/meanas/fdtd/phasor.py @@ -0,0 +1,126 @@ +""" +Helpers for extracting single- or multi-frequency phasors from FDTD samples. + +These helpers are intentionally low-level: callers own the accumulator arrays and +the sampling policy. The accumulated quantity is + + dt * sum(weight * exp(-1j * omega * t_step) * sample_step) + +where `t_step = (step + offset_steps) * dt`. + +The usual Yee offsets are: + +- `accumulate_phasor_e(..., step=l)` for `E_l` +- `accumulate_phasor_h(..., step=l)` for `H_{l + 1/2}` +- `accumulate_phasor_j(..., step=l)` for `J_{l + 1/2}` + +These helpers do not choose warmup/accumulation windows or pulse-envelope +normalization. They also do not impose a current sign convention. In this +codebase, electric-current injection normally appears as `E -= dt * J / epsilon`, +which matches the FDFD right-hand side `-1j * omega * J`. +""" +from collections.abc import Sequence + +import numpy +from numpy.typing import ArrayLike, NDArray + + +def _normalize_omegas( + omegas: float | complex | Sequence[float | complex] | NDArray, + ) -> NDArray[numpy.complexfloating]: + omega_array = numpy.atleast_1d(numpy.asarray(omegas, dtype=complex)) + if omega_array.ndim != 1 or omega_array.size == 0: + raise ValueError('omegas must be a scalar or non-empty 1D sequence') + return omega_array + + +def _normalize_weight( + weight: ArrayLike, + omega_shape: tuple[int, ...], + ) -> NDArray[numpy.complexfloating]: + weight_array = numpy.asarray(weight, dtype=complex) + if weight_array.ndim == 0: + return numpy.full(omega_shape, weight_array, dtype=complex) + if weight_array.shape == omega_shape: + return weight_array + raise ValueError(f'weight must be scalar or have shape {omega_shape}, got {weight_array.shape}') + + +def accumulate_phasor( + accumulator: NDArray[numpy.complexfloating], + omegas: float | complex | Sequence[float | complex] | NDArray, + dt: float, + sample: ArrayLike, + step: int, + *, + offset_steps: float = 0.0, + weight: ArrayLike = 1.0, + ) -> NDArray[numpy.complexfloating]: + """ + Add one time-domain sample into a phasor accumulator. + + The added quantity is + + dt * weight * exp(-1j * omega * t_step) * sample + + where `t_step = (step + offset_steps) * dt`. + + Note: + This helper already multiplies by `dt`. If the caller's normalization + factor was derived from a discrete sum that already includes `dt`, pass + `weight / dt` here. + """ + if dt <= 0: + raise ValueError('dt must be positive') + + omega_array = _normalize_omegas(omegas) + sample_array = numpy.asarray(sample) + expected_shape = (omega_array.size, *sample_array.shape) + if accumulator.shape != expected_shape: + raise ValueError(f'accumulator must have shape {expected_shape}, got {accumulator.shape}') + + weight_array = _normalize_weight(weight, omega_array.shape) + time = (step + offset_steps) * dt + phase = numpy.exp(-1j * omega_array * time) + scaled = dt * (weight_array * phase).reshape((-1,) + (1,) * sample_array.ndim) + accumulator += scaled * sample_array + return accumulator + + +def accumulate_phasor_e( + accumulator: NDArray[numpy.complexfloating], + omegas: float | complex | Sequence[float | complex] | NDArray, + dt: float, + sample: ArrayLike, + step: int, + *, + weight: ArrayLike = 1.0, + ) -> NDArray[numpy.complexfloating]: + """Accumulate an E-field sample taken at integer timestep `step`.""" + return accumulate_phasor(accumulator, omegas, dt, sample, step, offset_steps=0.0, weight=weight) + + +def accumulate_phasor_h( + accumulator: NDArray[numpy.complexfloating], + omegas: float | complex | Sequence[float | complex] | NDArray, + dt: float, + sample: ArrayLike, + step: int, + *, + weight: ArrayLike = 1.0, + ) -> NDArray[numpy.complexfloating]: + """Accumulate an H-field sample corresponding to `H_{step + 1/2}`.""" + return accumulate_phasor(accumulator, omegas, dt, sample, step, offset_steps=0.5, weight=weight) + + +def accumulate_phasor_j( + accumulator: NDArray[numpy.complexfloating], + omegas: float | complex | Sequence[float | complex] | NDArray, + dt: float, + sample: ArrayLike, + step: int, + *, + weight: ArrayLike = 1.0, + ) -> NDArray[numpy.complexfloating]: + """Accumulate a current sample corresponding to `J_{step + 1/2}`.""" + return accumulate_phasor(accumulator, omegas, dt, sample, step, offset_steps=0.5, weight=weight) diff --git a/meanas/test/test_fdtd_phasor.py b/meanas/test/test_fdtd_phasor.py new file mode 100644 index 0000000..7ace489 --- /dev/null +++ b/meanas/test/test_fdtd_phasor.py @@ -0,0 +1,208 @@ +import dataclasses +from functools import lru_cache + +import numpy +import pytest +import scipy.sparse.linalg + +from .. import fdfd, fdtd +from ..fdmath import unvec, vec +from ._test_builders import unit_dxes +from .utils import assert_close, assert_fields_close + + +@dataclasses.dataclass(frozen=True) +class ContinuousWaveCase: + omega: float + dt: float + dxes: tuple[tuple[numpy.ndarray, ...], tuple[numpy.ndarray, ...]] + epsilon: numpy.ndarray + e_ph: numpy.ndarray + h_ph: numpy.ndarray + j_ph: numpy.ndarray + + +def test_phasor_accumulator_matches_direct_sum_for_multi_frequency_weights() -> None: + omegas = numpy.array([0.25, 0.5]) + dt = 0.2 + sample_0 = numpy.array([[1.0, 2.0], [3.0, 4.0]]) + sample_1 = numpy.array([[0.5, 1.5], [2.5, 3.5]]) + weight_0 = numpy.array([1.0, 2.0]) + weight_1 = 0.75 + accumulator = numpy.zeros((omegas.size, *sample_0.shape), dtype=complex) + + fdtd.accumulate_phasor(accumulator, omegas, dt, sample_0, 0, weight=weight_0) + fdtd.accumulate_phasor(accumulator, omegas, dt, sample_1, 3, offset_steps=0.5, weight=weight_1) + + expected = numpy.zeros((2, *sample_0.shape), dtype=complex) + for idx, omega in enumerate(omegas): + expected[idx] += dt * weight_0[idx] * numpy.exp(-1j * omega * 0.0) * sample_0 + expected[idx] += dt * weight_1 * numpy.exp(-1j * omega * ((3 + 0.5) * dt)) * sample_1 + + assert_close(accumulator, expected) + + +def test_phasor_accumulator_convenience_methods_apply_yee_offsets() -> None: + omega = 1.25 + dt = 0.1 + sample = numpy.arange(6, dtype=float).reshape(2, 3) + e_acc = numpy.zeros((1, *sample.shape), dtype=complex) + h_acc = numpy.zeros((1, *sample.shape), dtype=complex) + j_acc = numpy.zeros((1, *sample.shape), dtype=complex) + + fdtd.accumulate_phasor_e(e_acc, omega, dt, sample, 4) + fdtd.accumulate_phasor_h(h_acc, omega, dt, sample, 4) + fdtd.accumulate_phasor_j(j_acc, omega, dt, sample, 4) + + expected_e = dt * numpy.exp(-1j * omega * (4 * dt)) * sample + expected_h = dt * numpy.exp(-1j * omega * ((4.5) * dt)) * sample + + assert_close(e_acc[0], expected_e) + assert_close(h_acc[0], expected_h) + assert_close(j_acc[0], expected_h) + + +def test_phasor_accumulator_matches_delayed_weighted_example_pattern() -> None: + omega = 0.75 + dt = 0.2 + delay = 0.6 + phasor_norm = 0.5 + steps = numpy.arange(5) + samples = numpy.arange(20, dtype=float).reshape(5, 2, 2) + 1.0 + accumulator = numpy.zeros((1, 2, 2), dtype=complex) + + for step, sample in zip(steps, samples, strict=True): + fdtd.accumulate_phasor( + accumulator, + omega, + dt, + sample, + int(step), + offset_steps=0.5 - delay / dt, + weight=phasor_norm / dt, + ) + + expected = numpy.zeros((2, 2), dtype=complex) + for step, sample in zip(steps, samples, strict=True): + time = (step + 0.5 - delay / dt) * dt + expected += dt * (phasor_norm / dt) * numpy.exp(-1j * omega * time) * sample + + assert_close(accumulator[0], expected) + + +def test_phasor_accumulator_validation_reset_and_squeeze() -> None: + with pytest.raises(ValueError, match='dt must be positive'): + fdtd.accumulate_phasor(numpy.zeros((1, 2, 2), dtype=complex), [1.0], 0.0, numpy.ones((2, 2)), 0) + + with pytest.raises(ValueError, match='omegas must be a scalar or non-empty 1D sequence'): + fdtd.accumulate_phasor(numpy.zeros((1, 2, 2), dtype=complex), numpy.ones((2, 2)), 0.2, numpy.ones((2, 2)), 0) + + accumulator = numpy.zeros((2, 2, 2), dtype=complex) + + with pytest.raises(ValueError, match='accumulator must have shape'): + fdtd.accumulate_phasor(accumulator, [1.0], 0.2, numpy.ones((2, 2)), 0) + + with pytest.raises(ValueError, match='weight must be scalar'): + fdtd.accumulate_phasor(accumulator, [1.0, 2.0], 0.2, numpy.ones((2, 2)), 0, weight=numpy.ones((2, 2))) + + fdtd.accumulate_phasor(accumulator, [1.0, 2.0], 0.2, numpy.ones((2, 2)), 0) + accumulator.fill(0) + assert_close(accumulator, 0.0) + + +@lru_cache(maxsize=1) +def _continuous_wave_case() -> ContinuousWaveCase: + spatial_shape = (5, 1, 5) + full_shape = (3, *spatial_shape) + dt = 0.25 + period_steps = 64 + warmup_periods = 8 + accumulation_periods = 8 + omega = 2 * numpy.pi / (period_steps * dt) + total_steps = period_steps * (warmup_periods + accumulation_periods) + warmup_steps = period_steps * warmup_periods + accumulation_steps = period_steps * accumulation_periods + source_amplitude = 1.0 + source_index = (1, spatial_shape[0] // 2, spatial_shape[1] // 2, spatial_shape[2] // 2) + + dxes = unit_dxes(spatial_shape) + epsilon = numpy.ones(full_shape, dtype=float) + e_field = numpy.zeros(full_shape, dtype=float) + h_field = numpy.zeros(full_shape, dtype=float) + update_e = fdtd.maxwell_e(dt=dt, dxes=dxes) + update_h = fdtd.maxwell_h(dt=dt, dxes=dxes) + + e_accumulator = numpy.zeros((1, *full_shape), dtype=complex) + h_accumulator = numpy.zeros((1, *full_shape), dtype=complex) + j_accumulator = numpy.zeros((1, *full_shape), dtype=complex) + + for step in range(total_steps): + update_e(e_field, h_field, epsilon) + + j_step = numpy.zeros_like(e_field) + current_density = source_amplitude * numpy.cos(omega * (step + 0.5) * dt) + j_step[source_index] = current_density + e_field -= dt * j_step / epsilon + + if step >= warmup_steps: + fdtd.accumulate_phasor_j(j_accumulator, omega, dt, j_step, step) + fdtd.accumulate_phasor_e(e_accumulator, omega, dt, e_field, step + 1) + + update_h(e_field, h_field) + + if step >= warmup_steps: + fdtd.accumulate_phasor_h(h_accumulator, omega, dt, h_field, step + 1) + + return ContinuousWaveCase( + omega=omega, + dt=dt, + dxes=dxes, + epsilon=epsilon, + e_ph=e_accumulator[0], + h_ph=h_accumulator[0], + j_ph=j_accumulator[0], + ) + + +def test_continuous_wave_current_phasor_matches_analytic_discrete_sum() -> None: + case = _continuous_wave_case() + + accumulation_indices = numpy.arange(64 * 8, 64 * 16) + times = (accumulation_indices + 0.5) * case.dt + expected = numpy.zeros_like(case.j_ph) + expected[1, 2, 0, 2] = case.dt * numpy.sum( + numpy.exp(-1j * case.omega * times) * numpy.cos(case.omega * times), + ) + + assert_fields_close(case.j_ph, expected, atol=1e-12, rtol=1e-12) + + +def test_continuous_wave_electric_phasor_matches_fdfd_solution() -> None: + case = _continuous_wave_case() + operator = fdfd.operators.e_full(case.omega, case.dxes, vec(case.epsilon)).tocsr() + rhs = -1j * case.omega * vec(case.j_ph) + e_fdfd = unvec(scipy.sparse.linalg.spsolve(operator, rhs), case.epsilon.shape[1:]) + + rel_err = numpy.linalg.norm(vec(case.e_ph - e_fdfd)) / numpy.linalg.norm(vec(e_fdfd)) + assert rel_err < 5e-2 + + +def test_continuous_wave_magnetic_phasor_matches_fdfd_conversion() -> None: + case = _continuous_wave_case() + operator = fdfd.operators.e_full(case.omega, case.dxes, vec(case.epsilon)).tocsr() + rhs = -1j * case.omega * vec(case.j_ph) + e_fdfd = unvec(scipy.sparse.linalg.spsolve(operator, rhs), case.epsilon.shape[1:]) + h_fdfd = fdfd.functional.e2h(case.omega, case.dxes)(e_fdfd) + + rel_err = numpy.linalg.norm(vec(case.h_ph - h_fdfd)) / numpy.linalg.norm(vec(h_fdfd)) + assert rel_err < 5e-2 + + +def test_continuous_wave_extracted_electric_phasor_has_small_fdfd_residual() -> None: + case = _continuous_wave_case() + operator = fdfd.operators.e_full(case.omega, case.dxes, vec(case.epsilon)).tocsr() + rhs = -1j * case.omega * vec(case.j_ph) + residual = operator @ vec(case.e_ph) - rhs + rel_residual = numpy.linalg.norm(residual) / numpy.linalg.norm(rhs) + + assert rel_residual < 5e-2 From d4c1082ca9a223f4759b50f7685680b5ca2517ba Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 13:34:04 -0700 Subject: [PATCH 408/437] [tests] FDFD/FDTD equivalence test --- meanas/test/test_waveguide_fdtd_fdfd.py | 224 ++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 meanas/test/test_waveguide_fdtd_fdfd.py diff --git a/meanas/test/test_waveguide_fdtd_fdfd.py b/meanas/test/test_waveguide_fdtd_fdfd.py new file mode 100644 index 0000000..396dfda --- /dev/null +++ b/meanas/test/test_waveguide_fdtd_fdfd.py @@ -0,0 +1,224 @@ +import dataclasses +from functools import lru_cache + +import numpy + +from .. import fdfd, fdtd +from ..fdmath import vec, unvec +from ..fdfd import functional, scpml, waveguide_3d + + +DT = 0.25 +PERIOD_STEPS = 64 +OMEGA = 2 * numpy.pi / (PERIOD_STEPS * DT) +CPML_THICKNESS = 3 +WARMUP_PERIODS = 9 +ACCUMULATION_PERIODS = 9 +SHAPE = (3, 25, 13, 13) +SOURCE_SLICES = (slice(4, 5), slice(None), slice(None)) +MONITOR_SLICES = (slice(18, 19), slice(None), slice(None)) +CHOSEN_VARIANT = 'base' + + +@dataclasses.dataclass(frozen=True) +class WaveguideCalibrationResult: + variant: str + e_ph: numpy.ndarray + h_ph: numpy.ndarray + j_ph: numpy.ndarray + e_fdfd: numpy.ndarray + h_fdfd: numpy.ndarray + overlap_td: complex + overlap_fd: complex + flux_td: float + flux_fd: float + + @property + def overlap_rel_err(self) -> float: + return float(abs(self.overlap_td - self.overlap_fd) / abs(self.overlap_fd)) + + @property + def overlap_mag_rel_err(self) -> float: + return float(abs(abs(self.overlap_td) - abs(self.overlap_fd)) / abs(self.overlap_fd)) + + @property + def overlap_phase_deg(self) -> float: + return float(abs(numpy.degrees(numpy.angle(self.overlap_td / self.overlap_fd)))) + + @property + def flux_rel_err(self) -> float: + return float(abs(self.flux_td - self.flux_fd) / abs(self.flux_fd)) + + @property + def combined_error(self) -> float: + return self.overlap_mag_rel_err + self.flux_rel_err + + +def _build_base_dxes() -> list[list[numpy.ndarray]]: + return [[numpy.ones(SHAPE[axis + 1]) for axis in range(3)] for _ in range(2)] + + +def _build_stretched_dxes(base_dxes: list[list[numpy.ndarray]]) -> list[list[numpy.ndarray]]: + stretched_dxes = [[dx.copy() for dx in group] for group in base_dxes] + for axis in (0, 1, 2): + for polarity in (-1, 1): + stretched_dxes = scpml.stretch_with_scpml( + stretched_dxes, + axis=axis, + polarity=polarity, + omega=OMEGA, + epsilon_effective=1.0, + thickness=CPML_THICKNESS, + ) + return stretched_dxes + + +def _build_epsilon() -> numpy.ndarray: + epsilon = numpy.ones(SHAPE, dtype=float) + y0 = (SHAPE[2] - 3) // 2 + z0 = (SHAPE[3] - 3) // 2 + epsilon[:, :, y0:y0 + 3, z0:z0 + 3] = 12.0 + return epsilon + + +@lru_cache(maxsize=2) +def _run_straight_waveguide_case(variant: str) -> WaveguideCalibrationResult: + assert variant in ('stretched', 'base') + + epsilon = _build_epsilon() + base_dxes = _build_base_dxes() + stretched_dxes = _build_stretched_dxes(base_dxes) + mode_dxes = stretched_dxes if variant == 'stretched' else base_dxes + + source_mode = waveguide_3d.solve_mode( + 0, + omega=OMEGA, + dxes=mode_dxes, + axis=0, + polarity=1, + slices=SOURCE_SLICES, + epsilon=epsilon, + ) + j_mode = waveguide_3d.compute_source( + E=source_mode['E'], + wavenumber=source_mode['wavenumber'], + omega=OMEGA, + dxes=mode_dxes, + axis=0, + polarity=1, + slices=SOURCE_SLICES, + epsilon=epsilon, + ) + monitor_mode = waveguide_3d.solve_mode( + 0, + omega=OMEGA, + dxes=mode_dxes, + axis=0, + polarity=1, + slices=MONITOR_SLICES, + epsilon=epsilon, + ) + overlap_e = waveguide_3d.compute_overlap_e( + E=monitor_mode['E'], + wavenumber=monitor_mode['wavenumber'], + dxes=mode_dxes, + axis=0, + polarity=1, + slices=MONITOR_SLICES, + omega=OMEGA, + ) + + pml_params = [ + [fdtd.cpml_params(axis=axis, polarity=polarity, dt=DT, thickness=CPML_THICKNESS, epsilon_eff=1.0) + for polarity in (-1, 1)] + for axis in range(3) + ] + update_e, update_h = fdtd.updates_with_cpml(cpml_params=pml_params, dt=DT, dxes=base_dxes, epsilon=epsilon) + + e_field = numpy.zeros_like(epsilon) + h_field = numpy.zeros_like(epsilon) + e_accumulator = numpy.zeros((1, *SHAPE), dtype=complex) + h_accumulator = numpy.zeros((1, *SHAPE), dtype=complex) + j_accumulator = numpy.zeros((1, *SHAPE), dtype=complex) + + warmup_steps = WARMUP_PERIODS * PERIOD_STEPS + accumulation_steps = ACCUMULATION_PERIODS * PERIOD_STEPS + for step in range(warmup_steps + accumulation_steps): + update_e(e_field, h_field, epsilon) + + t_half = (step + 0.5) * DT + j_real = (j_mode.real * numpy.cos(OMEGA * t_half) - j_mode.imag * numpy.sin(OMEGA * t_half)).real + e_field -= DT * j_real / epsilon + + if step >= warmup_steps: + fdtd.accumulate_phasor_j(j_accumulator, OMEGA, DT, j_real, step) + fdtd.accumulate_phasor_e(e_accumulator, OMEGA, DT, e_field, step + 1) + + update_h(e_field, h_field) + + if step >= warmup_steps: + fdtd.accumulate_phasor_h(h_accumulator, OMEGA, DT, h_field, step + 1) + + e_ph = e_accumulator[0] + h_ph = h_accumulator[0] + j_ph = j_accumulator[0] + + e_fdfd = unvec( + fdfd.solvers.generic( + J=vec(j_ph), + omega=OMEGA, + dxes=stretched_dxes, + epsilon=vec(epsilon), + matrix_solver_opts={'atol': 1e-10, 'rtol': 1e-7}, + ), + SHAPE[1:], + ) + h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd) + + overlap_td = vec(e_ph) @ vec(overlap_e).conj() + overlap_fd = vec(e_fdfd) @ vec(overlap_e).conj() + + poynting_td = functional.poynting_e_cross_h(stretched_dxes)(e_ph, h_ph.conj()) + poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj()) + flux_td = float(0.5 * poynting_td[0, MONITOR_SLICES[0], :, :].real.sum()) + flux_fd = float(0.5 * poynting_fd[0, MONITOR_SLICES[0], :, :].real.sum()) + + return WaveguideCalibrationResult( + variant=variant, + e_ph=e_ph, + h_ph=h_ph, + j_ph=j_ph, + e_fdfd=e_fdfd, + h_fdfd=h_fdfd, + overlap_td=overlap_td, + overlap_fd=overlap_fd, + flux_td=flux_td, + flux_fd=flux_fd, + ) + + +def test_straight_waveguide_base_variant_outperforms_stretched_variant() -> None: + base_result = _run_straight_waveguide_case('base') + stretched_result = _run_straight_waveguide_case('stretched') + + assert base_result.variant == CHOSEN_VARIANT + assert base_result.combined_error < stretched_result.combined_error + + +def test_straight_waveguide_fdtd_fdfd_overlap_and_flux_agree() -> None: + result = _run_straight_waveguide_case(CHOSEN_VARIANT) + + assert numpy.isfinite(result.e_ph).all() + assert numpy.isfinite(result.h_ph).all() + assert numpy.isfinite(result.j_ph).all() + assert numpy.isfinite(result.e_fdfd).all() + assert numpy.isfinite(result.h_fdfd).all() + assert abs(result.overlap_td) > 0 + assert abs(result.overlap_fd) > 0 + assert abs(result.flux_td) > 0 + assert abs(result.flux_fd) > 0 + + assert result.overlap_mag_rel_err < 0.01 + assert result.flux_rel_err < 0.01 + assert result.overlap_rel_err < 0.01 + assert result.overlap_phase_deg < 0.5 From 0568e1ba50d3935b2200db34fb78c79fe7bc713c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 14:07:15 -0700 Subject: [PATCH 409/437] [tests] add a waveguide scattering test --- meanas/test/test_waveguide_fdtd_fdfd.py | 226 +++++++++++++++++++++++- 1 file changed, 219 insertions(+), 7 deletions(-) diff --git a/meanas/test/test_waveguide_fdtd_fdfd.py b/meanas/test/test_waveguide_fdtd_fdfd.py index 396dfda..92a0422 100644 --- a/meanas/test/test_waveguide_fdtd_fdfd.py +++ b/meanas/test/test_waveguide_fdtd_fdfd.py @@ -18,6 +18,13 @@ SHAPE = (3, 25, 13, 13) SOURCE_SLICES = (slice(4, 5), slice(None), slice(None)) MONITOR_SLICES = (slice(18, 19), slice(None), slice(None)) CHOSEN_VARIANT = 'base' +SCATTERING_SHAPE = (3, 35, 15, 15) +SCATTERING_SOURCE_SLICES = (slice(4, 5), slice(None), slice(None)) +SCATTERING_REFLECT_SLICES = (slice(10, 11), slice(None), slice(None)) +SCATTERING_TRANSMIT_SLICES = (slice(29, 30), slice(None), slice(None)) +SCATTERING_STEP_X = 18 +SCATTERING_WARMUP_PERIODS = 10 +SCATTERING_ACCUMULATION_PERIODS = 10 @dataclasses.dataclass(frozen=True) @@ -54,8 +61,45 @@ class WaveguideCalibrationResult: return self.overlap_mag_rel_err + self.flux_rel_err +@dataclasses.dataclass(frozen=True) +class WaveguideScatteringResult: + e_ph: numpy.ndarray + h_ph: numpy.ndarray + j_ph: numpy.ndarray + e_fdfd: numpy.ndarray + h_fdfd: numpy.ndarray + reflected_td: complex + reflected_fd: complex + transmitted_td: complex + transmitted_fd: complex + reflected_flux_td: float + reflected_flux_fd: float + transmitted_flux_td: float + transmitted_flux_fd: float + + @property + def reflected_overlap_mag_rel_err(self) -> float: + return float(abs(abs(self.reflected_td) - abs(self.reflected_fd)) / abs(self.reflected_fd)) + + @property + def transmitted_overlap_mag_rel_err(self) -> float: + return float(abs(abs(self.transmitted_td) - abs(self.transmitted_fd)) / abs(self.transmitted_fd)) + + @property + def reflected_flux_rel_err(self) -> float: + return float(abs(self.reflected_flux_td - self.reflected_flux_fd) / abs(self.reflected_flux_fd)) + + @property + def transmitted_flux_rel_err(self) -> float: + return float(abs(self.transmitted_flux_td - self.transmitted_flux_fd) / abs(self.transmitted_flux_fd)) + + +def _build_uniform_dxes(shape: tuple[int, int, int, int]) -> list[list[numpy.ndarray]]: + return [[numpy.ones(shape[axis + 1]) for axis in range(3)] for _ in range(2)] + + def _build_base_dxes() -> list[list[numpy.ndarray]]: - return [[numpy.ones(SHAPE[axis + 1]) for axis in range(3)] for _ in range(2)] + return _build_uniform_dxes(SHAPE) def _build_stretched_dxes(base_dxes: list[list[numpy.ndarray]]) -> list[list[numpy.ndarray]]: @@ -81,6 +125,23 @@ def _build_epsilon() -> numpy.ndarray: return epsilon +def _build_scattering_epsilon() -> numpy.ndarray: + epsilon = numpy.ones(SCATTERING_SHAPE, dtype=float) + y0 = SCATTERING_SHAPE[2] // 2 + z0 = SCATTERING_SHAPE[3] // 2 + epsilon[:, :SCATTERING_STEP_X, y0 - 1:y0 + 2, z0 - 1:z0 + 2] = 12.0 + epsilon[:, SCATTERING_STEP_X:, y0 - 2:y0 + 3, z0 - 2:z0 + 3] = 12.0 + return epsilon + + +def _build_cpml_params() -> list[list[dict[str, numpy.ndarray | float]]]: + return [ + [fdtd.cpml_params(axis=axis, polarity=polarity, dt=DT, thickness=CPML_THICKNESS, epsilon_eff=1.0) + for polarity in (-1, 1)] + for axis in range(3) + ] + + @lru_cache(maxsize=2) def _run_straight_waveguide_case(variant: str) -> WaveguideCalibrationResult: assert variant in ('stretched', 'base') @@ -128,12 +189,7 @@ def _run_straight_waveguide_case(variant: str) -> WaveguideCalibrationResult: omega=OMEGA, ) - pml_params = [ - [fdtd.cpml_params(axis=axis, polarity=polarity, dt=DT, thickness=CPML_THICKNESS, epsilon_eff=1.0) - for polarity in (-1, 1)] - for axis in range(3) - ] - update_e, update_h = fdtd.updates_with_cpml(cpml_params=pml_params, dt=DT, dxes=base_dxes, epsilon=epsilon) + update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon) e_field = numpy.zeros_like(epsilon) h_field = numpy.zeros_like(epsilon) @@ -197,6 +253,139 @@ def _run_straight_waveguide_case(variant: str) -> WaveguideCalibrationResult: ) +@lru_cache(maxsize=1) +def _run_width_step_scattering_case() -> WaveguideScatteringResult: + epsilon = _build_scattering_epsilon() + base_dxes = _build_uniform_dxes(SCATTERING_SHAPE) + stretched_dxes = _build_stretched_dxes(base_dxes) + + source_mode = waveguide_3d.solve_mode( + 0, + omega=OMEGA, + dxes=base_dxes, + axis=0, + polarity=1, + slices=SCATTERING_SOURCE_SLICES, + epsilon=epsilon, + ) + j_mode = waveguide_3d.compute_source( + E=source_mode['E'], + wavenumber=source_mode['wavenumber'], + omega=OMEGA, + dxes=base_dxes, + axis=0, + polarity=1, + slices=SCATTERING_SOURCE_SLICES, + epsilon=epsilon, + ) + reflected_mode = waveguide_3d.solve_mode( + 0, + omega=OMEGA, + dxes=base_dxes, + axis=0, + polarity=-1, + slices=SCATTERING_REFLECT_SLICES, + epsilon=epsilon, + ) + reflected_overlap = waveguide_3d.compute_overlap_e( + E=reflected_mode['E'], + wavenumber=reflected_mode['wavenumber'], + dxes=base_dxes, + axis=0, + polarity=-1, + slices=SCATTERING_REFLECT_SLICES, + omega=OMEGA, + ) + transmitted_mode = waveguide_3d.solve_mode( + 0, + omega=OMEGA, + dxes=base_dxes, + axis=0, + polarity=1, + slices=SCATTERING_TRANSMIT_SLICES, + epsilon=epsilon, + ) + transmitted_overlap = waveguide_3d.compute_overlap_e( + E=transmitted_mode['E'], + wavenumber=transmitted_mode['wavenumber'], + dxes=base_dxes, + axis=0, + polarity=1, + slices=SCATTERING_TRANSMIT_SLICES, + omega=OMEGA, + ) + + update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon) + + e_field = numpy.zeros_like(epsilon) + h_field = numpy.zeros_like(epsilon) + e_accumulator = numpy.zeros((1, *SCATTERING_SHAPE), dtype=complex) + h_accumulator = numpy.zeros((1, *SCATTERING_SHAPE), dtype=complex) + j_accumulator = numpy.zeros((1, *SCATTERING_SHAPE), dtype=complex) + + warmup_steps = SCATTERING_WARMUP_PERIODS * PERIOD_STEPS + accumulation_steps = SCATTERING_ACCUMULATION_PERIODS * PERIOD_STEPS + for step in range(warmup_steps + accumulation_steps): + update_e(e_field, h_field, epsilon) + + t_half = (step + 0.5) * DT + j_real = (j_mode.real * numpy.cos(OMEGA * t_half) - j_mode.imag * numpy.sin(OMEGA * t_half)).real + e_field -= DT * j_real / epsilon + + if step >= warmup_steps: + fdtd.accumulate_phasor_j(j_accumulator, OMEGA, DT, j_real, step) + fdtd.accumulate_phasor_e(e_accumulator, OMEGA, DT, e_field, step + 1) + + update_h(e_field, h_field) + + if step >= warmup_steps: + fdtd.accumulate_phasor_h(h_accumulator, OMEGA, DT, h_field, step + 1) + + e_ph = e_accumulator[0] + h_ph = h_accumulator[0] + j_ph = j_accumulator[0] + + e_fdfd = unvec( + fdfd.solvers.generic( + J=vec(j_ph), + omega=OMEGA, + dxes=stretched_dxes, + epsilon=vec(epsilon), + matrix_solver_opts={'atol': 1e-10, 'rtol': 1e-7}, + ), + SCATTERING_SHAPE[1:], + ) + h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd) + + reflected_td = vec(e_ph) @ vec(reflected_overlap).conj() + reflected_fd = vec(e_fdfd) @ vec(reflected_overlap).conj() + transmitted_td = vec(e_ph) @ vec(transmitted_overlap).conj() + transmitted_fd = vec(e_fdfd) @ vec(transmitted_overlap).conj() + + poynting_td = functional.poynting_e_cross_h(stretched_dxes)(e_ph, h_ph.conj()) + poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj()) + reflected_flux_td = float(0.5 * poynting_td[0, SCATTERING_REFLECT_SLICES[0], :, :].real.sum()) + reflected_flux_fd = float(0.5 * poynting_fd[0, SCATTERING_REFLECT_SLICES[0], :, :].real.sum()) + transmitted_flux_td = float(0.5 * poynting_td[0, SCATTERING_TRANSMIT_SLICES[0], :, :].real.sum()) + transmitted_flux_fd = float(0.5 * poynting_fd[0, SCATTERING_TRANSMIT_SLICES[0], :, :].real.sum()) + + return WaveguideScatteringResult( + e_ph=e_ph, + h_ph=h_ph, + j_ph=j_ph, + e_fdfd=e_fdfd, + h_fdfd=h_fdfd, + reflected_td=reflected_td, + reflected_fd=reflected_fd, + transmitted_td=transmitted_td, + transmitted_fd=transmitted_fd, + reflected_flux_td=reflected_flux_td, + reflected_flux_fd=reflected_flux_fd, + transmitted_flux_td=transmitted_flux_td, + transmitted_flux_fd=transmitted_flux_fd, + ) + + def test_straight_waveguide_base_variant_outperforms_stretched_variant() -> None: base_result = _run_straight_waveguide_case('base') stretched_result = _run_straight_waveguide_case('stretched') @@ -222,3 +411,26 @@ def test_straight_waveguide_fdtd_fdfd_overlap_and_flux_agree() -> None: assert result.flux_rel_err < 0.01 assert result.overlap_rel_err < 0.01 assert result.overlap_phase_deg < 0.5 + + +def test_width_step_waveguide_fdtd_fdfd_modal_powers_and_flux_agree() -> None: + result = _run_width_step_scattering_case() + + assert numpy.isfinite(result.e_ph).all() + assert numpy.isfinite(result.h_ph).all() + assert numpy.isfinite(result.j_ph).all() + assert numpy.isfinite(result.e_fdfd).all() + assert numpy.isfinite(result.h_fdfd).all() + assert abs(result.reflected_td) > 0 + assert abs(result.reflected_fd) > 0 + assert abs(result.transmitted_td) > 0 + assert abs(result.transmitted_fd) > 0 + assert abs(result.reflected_flux_td) > 0 + assert abs(result.reflected_flux_fd) > 0 + assert abs(result.transmitted_flux_td) > 0 + assert abs(result.transmitted_flux_fd) > 0 + + assert result.transmitted_overlap_mag_rel_err < 0.03 + assert result.reflected_overlap_mag_rel_err < 0.03 + assert result.transmitted_flux_rel_err < 0.01 + assert result.reflected_flux_rel_err < 0.01 From 5e95d66a7e721e60842f03e9c5e91e865d43cd9a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 14:24:18 -0700 Subject: [PATCH 410/437] [docs] expand API and derivation docs --- README.md | 59 +++++++++- examples/fdtd.py | 23 +++- examples/waveguide.py | 29 +++-- meanas/fdfd/__init__.py | 11 +- meanas/fdfd/functional.py | 27 ++++- meanas/fdfd/operators.py | 57 ++++++++-- meanas/fdfd/waveguide_2d.py | 82 ++++++++++++-- meanas/fdfd/waveguide_3d.py | 97 ++++++++++++---- meanas/fdfd/waveguide_cyl.py | 213 ++++++++++++++++++++++++++--------- meanas/fdtd/__init__.py | 39 ++++++- meanas/fdtd/energy.py | 48 +++++++- meanas/fdtd/pml.py | 60 +++++++++- 12 files changed, 613 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 3dcdfe0..c3632c0 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,59 @@ python3 -m pytest -rsxX | tee test_results.txt ## Use -See `examples/` for some simple examples; you may need additional -packages such as [gridlock](https://mpxd.net/code/jan/gridlock) -to run the examples. +`meanas` is organized around a few core workflows: + +- `meanas.fdfd`: frequency-domain wave equations, sparse operators, SCPML, and + iterative solves for driven problems. +- `meanas.fdfd.waveguide_2d` / `meanas.fdfd.waveguide_3d`: waveguide mode + solvers, mode-source construction, and overlap windows for port-based + excitation and analysis. +- `meanas.fdtd`: Yee-step updates, CPML boundaries, flux/energy accounting, and + on-the-fly phasor extraction for comparing time-domain runs against FDFD. +- `meanas.fdmath`: low-level finite-difference operators, vectorization helpers, + and derivations shared by the FDTD and FDFD layers. + +The most mature user-facing workflows are: + +1. Build an FDFD operator or waveguide port source, then solve a driven + frequency-domain problem. +2. Run an FDTD simulation, extract one or more frequency-domain phasors with + `meanas.fdtd.accumulate_phasor(...)`, and compare those phasors against an + FDFD reference on the same Yee grid. + +Tracked examples under `examples/` are the intended starting points: + +- `examples/fdtd.py`: broadband FDTD pulse excitation, phasor extraction, and a + residual check against the matching FDFD operator. +- `examples/waveguide.py`: waveguide mode solving, unidirectional mode-source + construction, overlap readout, and FDTD/FDFD comparison on a guided structure. +- `examples/fdfd.py`: direct frequency-domain waveguide excitation and overlap / + Poynting analysis without a time-domain run. + +Several examples rely on optional packages such as +[gridlock](https://mpxd.net/code/jan/gridlock). + +### Frequency-domain waveguide workflow + +For a structure with a constant cross-section in one direction: + +1. Build `dxes` and the diagonal `epsilon` / `mu` distributions on the Yee grid. +2. Solve the port mode with `meanas.fdfd.waveguide_3d.solve_mode(...)`. +3. Build a unidirectional source with `compute_source(...)`. +4. Build a matching overlap window with `compute_overlap_e(...)`. +5. Solve the full FDFD problem and project the result onto the overlap window or + evaluate plane flux with `meanas.fdfd.functional.poynting_e_cross_h(...)`. + +### Time-domain phasor workflow + +For a broadband or continuous-wave FDTD run: + +1. Advance the fields with `meanas.fdtd.maxwell_e/maxwell_h` or + `updates_with_cpml(...)`. +2. Inject electric current using the same sign convention used throughout the + examples and library: `E -= dt * J / epsilon`. +3. Accumulate the desired phasor with `accumulate_phasor(...)` or the Yee-aware + wrappers `accumulate_phasor_e/h/j(...)`. +4. Build the matching FDFD operator on the stretched `dxes` if CPML/SCPML is + part of the simulation, and compare the extracted phasor to the FDFD field or + residual. diff --git a/examples/fdtd.py b/examples/fdtd.py index 2a50ddc..704c2f1 100644 --- a/examples/fdtd.py +++ b/examples/fdtd.py @@ -1,7 +1,12 @@ """ -Example code for running an FDTD simulation +Example code for a broadband FDTD run with phasor extraction. -See main() for simulation setup. +This script shows the intended low-level workflow for: + +1. building a Yee-grid simulation with CPML on all faces, +2. driving it with an electric-current pulse, +3. extracting a single-frequency phasor on the fly, and +4. checking that phasor against the matching stretched-grid FDFD operator. """ import sys @@ -150,7 +155,8 @@ def main(): # print(f'Save time interval would be {sample_interval} * dt = {sample_interval * dt:3g}') - # Source parameters and function + # Source parameters and function. The pulse normalization is kept outside + # accumulate_phasor(); the helper only performs the Fourier sum. source_phasor, delay = gaussian_packet(wl=wl, dwl=100, dt=dt, turn_on=1e-5) aa, cc, ss = source_phasor(numpy.arange(max_t)) srca_real = aa * cc @@ -170,7 +176,8 @@ def main(): update_E(ee, hh, epsilon) if tt < src_maxt: - # This codebase uses E -= dt * J / epsilon for electric-current injection. + # Electric-current injection uses E -= dt * J / epsilon, which is + # the same sign convention used by the matching FDFD right-hand side. ee[1, *(grid.shape // 2)] -= srca_real[tt] update_H(ee, hh) @@ -193,9 +200,11 @@ def main(): dt, ee, tt, - # The pulse is delayed relative to t=0, so the readout needs the same phase shift. + # The pulse is delayed relative to t=0, so the extracted phasor + # needs the same phase offset in its sample times. offset_steps=0.5 - delay / dt, - # accumulate_phasor() already includes dt, so undo the dt in phasor_norm here. + # accumulate_phasor() already multiplies by dt, so pass the + # discrete-sum normalization without its extra dt factor. weight=phasor_norm / dt, ) @@ -205,6 +214,8 @@ def main(): for pp in (-1, +1): for dd in range(3): stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_air ** 2, thickness=pml_thickness) + # Compare the extracted phasor to the FDFD operator on the stretched grid, + # not the unstretched Yee spacings used by the raw time-domain update. A = e_full(omega=omega, dxes=dxes_fdfd, epsilon=epsilon) residual = norm(A @ vec(Eph) - vec(b)) / norm(vec(b)) print(f'FDFD residual is {residual}') diff --git a/examples/waveguide.py b/examples/waveguide.py index 1bb02fa..a100ee3 100644 --- a/examples/waveguide.py +++ b/examples/waveguide.py @@ -1,7 +1,11 @@ """ -Example code for running an OpenCL FDTD simulation +Example code for guided-wave FDFD and FDTD comparison. -See main() for simulation setup. +This example is the reference workflow for: + +1. solving a waveguide port mode, +2. turning that mode into a one-sided source and overlap window, +3. comparing a direct FDFD solve against a time-domain phasor extracted from FDTD. """ from typing import Callable import logging @@ -78,7 +82,7 @@ def get_waveguide_mode( omega: float, epsilon: fdfield_t, ) -> tuple[vcfdfield_t, vcfdfield_t]: - """ Create a mode source and overlap window """ + """Create a mode source and overlap window for one forward-going port.""" dims = numpy.array([[-10, -20, -15], [-10, 20, 15]]) * [[numpy.median(numpy.real(dx)) for dx in dxes[0]]] ind_dims = (grid.pos2ind(dims[0], which_shifts=None).astype(int), @@ -94,6 +98,8 @@ def get_waveguide_mode( J = waveguide_3d.compute_source(E=wg_results['E'], wavenumber=wg_results['wavenumber'], omega=omega, epsilon=epsilon, **wg_args) + # compute_overlap_e() returns the normalized upstream overlap window used to + # project another field onto this same guided mode. e_overlap = waveguide_3d.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], **wg_args, omega=omega) return J, e_overlap @@ -111,7 +117,8 @@ def main( grid, epsilon = draw_grid(dx=dx, pml_thickness=pml_thickness) - # Add PML + # Add SCPML stretching to the FDFD grid; this matches the CPML-backed FDTD + # run below so the two solvers see the same absorbing boundary profile. dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (0, 1, 2): for p in (-1, 1): @@ -275,7 +282,8 @@ def main2(): # print(f'Save time interval would be {sample_interval} * dt = {sample_interval * dt:3g}') - # Source parameters and function + # Source parameters and function. The phasor helper only performs the + # Fourier accumulation; the pulse normalization stays explicit here. source_phasor, delay = gaussian_packet(wl=wl, dwl=100, dt=dt, turn_on=1e-5) aa, cc, ss = source_phasor(numpy.arange(max_t)) srca_real = aa * cc @@ -295,7 +303,8 @@ def main2(): update_E(ee, hh, epsilon) if tt < src_maxt: - # This codebase uses E -= dt * J / epsilon for electric-current injection. + # Electric-current injection uses E -= dt * J / epsilon, which is + # the sign convention matched by the FDFD source term -1j * omega * J. ee[1, *(grid.shape // 2)] -= srca_real[tt] update_H(ee, hh) @@ -317,9 +326,11 @@ def main2(): dt, ee, tt, - # The pulse is delayed relative to t=0, so the readout needs the same phase shift. + # The pulse is delayed relative to t=0, so the extracted phasor must + # apply the same delay to its sample times. offset_steps=0.5 - delay / dt, - # accumulate_phasor() already includes dt, so undo the dt in phasor_norm here. + # accumulate_phasor() already contributes dt, so remove the extra dt + # from the externally computed normalization. weight=phasor_norm / dt, ) @@ -329,6 +340,8 @@ def main2(): for pp in (-1, +1): for dd in range(3): stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_cladding ** 2, thickness=pml_thickness) + # Residuals must be checked on the stretched FDFD grid, because the FDTD run + # already includes those same absorbing layers through CPML. A = e_full(omega=omega, dxes=dxes_fdfd, epsilon=epsilon) residual = norm(A @ vec(Eph) - vec(b)) / norm(vec(b)) print(f'FDFD residual is {residual}') diff --git a/meanas/fdfd/__init__.py b/meanas/fdfd/__init__.py index ba57fc4..e94adc3 100644 --- a/meanas/fdfd/__init__.py +++ b/meanas/fdfd/__init__.py @@ -9,9 +9,12 @@ Submodules: - `operators`, `functional`: General FDFD problem setup. - `solvers`: Solver interface and reference implementation. -- `scpml`: Stretched-coordinate perfectly matched layer (scpml) boundary conditions +- `scpml`: Stretched-coordinate perfectly matched layer (SCPML) boundary conditions. - `waveguide_2d`: Operators and mode-solver for waveguides with constant cross-section. -- `waveguide_3d`: Functions for transforming `waveguide_2d` results into 3D. +- `waveguide_3d`: Functions for transforming `waveguide_2d` results into 3D, + including mode-source and overlap-window construction. +- `farfield`, `bloch`, `eme`: specialized helper modules for near/far transforms, + Bloch-periodic problems, and eigenmode expansion. ================================================================ @@ -86,10 +89,6 @@ $$ -\omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath \omega \tilde{J}_{\vec{r}} \\ $$ -# TODO FDFD? -# TODO PML - - """ from . import ( solvers as solvers, diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index 9d98798..ddd074b 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -158,10 +158,23 @@ def e_tfsf_source( epsilon: fdfield, mu: fdfield | None = None, ) -> cfdfield_updater_t: - """ + r""" Operator that turns an E-field distribution into a total-field/scattered-field (TFSF) source. + If `A` is the full wave operator from `e_full(...)` and `Q` is the diagonal + mask selecting the total-field region, then the TFSF source is the commutator + + $$ + \frac{A Q - Q A}{-i \omega} E. + $$ + + This vanishes in the interior of the total-field and scattered-field regions + and is supported only at their shared boundary, where the mask discontinuity + makes `A` and `Q` fail to commute. The returned current is therefore the + distributed source needed to inject the desired total field without also + forcing the scattered-field region. + Args: TF_region: mask which is set to 1 in the total-field region, and 0 elsewhere (i.e. in the scattered-field region). @@ -175,7 +188,6 @@ def e_tfsf_source( Function `f` which takes an E field and returns a current distribution, `f(E)` -> `J` """ - # TODO documentation A = e_full(omega, dxes, epsilon, mu) def op(e: cfdfield) -> cfdfield_t: @@ -188,7 +200,13 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield, cfdfield], cfdfi r""" Generates a function that takes the single-frequency `E` and `H` fields and calculates the cross product `E` x `H` = $E \times H$ as required - for the Poynting vector, $S = E \times H$ + for the Poynting vector, $S = E \times H$. + + On the Yee grid, the electric and magnetic components are not stored at the + same locations. This helper therefore applies the same one-cell electric-field + shifts used by the sparse `operators.poynting_e_cross(...)` construction so + that the discrete cross product matches the face-centered energy flux used in + `meanas.fdtd.energy.poynting(...)`. Note: This function also shifts the input `E` field by one cell as required @@ -204,7 +222,8 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield, cfdfield], cfdfi dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` Returns: - Function `f` that returns E x H as required for the poynting vector. + Function `f` that returns the staggered-grid cross product `E \times H`. + For time-average power, call it as `f(E, H.conj())` and take `Re(...) / 2`. """ def exh(e: cfdfield, h: cfdfield) -> cfdfield_t: s = numpy.empty_like(e) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 1282ea6..18aaade 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -310,16 +310,22 @@ def m2j( def poynting_e_cross(e: vcfdfield, dxes: dx_lists_t) -> sparse.sparray: - """ - Operator for computing the Poynting vector, containing the - (E x) portion of the Poynting vector. + r""" + Operator for computing the staggered-grid `(E \times)` part of the Poynting vector. + + On the Yee grid the E and H components live on different edges, so the + electric field must be shifted by one cell in the appropriate direction + before forming the discrete cross product. This sparse operator encodes that + shifted cross product directly and is the matrix equivalent of + `functional.poynting_e_cross_h(...)`. Args: e: Vectorized E-field for the ExH cross product dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` Returns: - Sparse matrix containing (E x) portion of Poynting cross product. + Sparse matrix containing the `(E \times)` part of the staggered Poynting + cross product. """ shape = [len(dx) for dx in dxes[0]] @@ -339,15 +345,26 @@ def poynting_e_cross(e: vcfdfield, dxes: dx_lists_t) -> sparse.sparray: def poynting_h_cross(h: vcfdfield, dxes: dx_lists_t) -> sparse.sparray: - """ - Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector. + r""" + Operator for computing the staggered-grid `(H \times)` part of the Poynting vector. + + Together with `poynting_e_cross(...)`, this provides the matrix form of the + Yee-grid cross product used in the flux helpers. The two are related by the + usual antisymmetry of the cross product, + + $$ + H \times E = -(E \times H), + $$ + + once the same staggered field placement is used on both sides. Args: h: Vectorized H-field for the HxE cross product dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` Returns: - Sparse matrix containing (H x) portion of Poynting cross product. + Sparse matrix containing the `(H \times)` part of the staggered Poynting + cross product. """ shape = [len(dx) for dx in dxes[0]] @@ -372,11 +389,23 @@ def e_tfsf_source( epsilon: vfdfield, mu: vfdfield | None = None, ) -> sparse.sparray: - """ + r""" Operator that turns a desired E-field distribution into a total-field/scattered-field (TFSF) source. - TODO: Reference Rumpf paper + Let `A` be the full wave operator from `e_full(...)`, and let + `Q = \mathrm{diag}(TF_region)` be the projector onto the total-field region. + Then the TFSF current operator is the commutator + + $$ + \frac{A Q - Q A}{-i \omega}. + $$ + + Inside regions where `Q` is locally constant, `A` and `Q` commute and the + source vanishes. Only cells at the TF/SF boundary contribute nonzero current, + which is exactly the desired distributed source for injecting the chosen + field into the total-field region without directly forcing the + scattered-field region. Args: TF_region: Mask, which is set to 1 inside the total-field region and 0 in the @@ -388,9 +417,7 @@ def e_tfsf_source( Returns: Sparse matrix that turns an E-field into a current (J) distribution. - """ - # TODO documentation A = e_full(omega, dxes, epsilon, mu) Q = sparse.diags_array(TF_region) return (A @ Q - Q @ A) / (-1j * omega) @@ -404,11 +431,17 @@ def e_boundary_source( mu: vfdfield | None = None, periodic_mask_edges: bool = False, ) -> sparse.sparray: - """ + r""" Operator that turns an E-field distrubtion into a current (J) distribution along the edges (external and internal) of the provided mask. This is just an `e_tfsf_source()` with an additional masking step. + Equivalently, this helper first constructs the TFSF commutator source for the + full mask and then zeroes out all cells except the mask boundary. The + boundary is defined as the set of cells whose mask value changes under a + one-cell shift in any Cartesian direction. With `periodic_mask_edges=False` + the shifts mirror at the domain boundary; with `True` they wrap periodically. + Args: mask: The current distribution is generated at the edges of the mask, i.e. any points where shifting the mask by one cell in any direction diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 7d1f651..1074e2b 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -175,8 +175,6 @@ if the result is introduced into a space with a discretized z-axis. """ -# TODO update module docs - from typing import Any from collections.abc import Sequence import numpy @@ -339,7 +337,7 @@ def normalized_fields_e( mu: vfdslice | None = None, prop_phase: float = 0, ) -> tuple[vcfdslice_t, vcfdslice_t]: - """ + r""" Given a vector `e_xy` containing the vectorized E_x and E_y fields, returns normalized, vectorized E and H fields for the system. @@ -357,6 +355,21 @@ def normalized_fields_e( Returns: `(e, h)`, where each field is vectorized, normalized, and contains all three vector components. + + Notes: + `e_xy` is only the transverse electric eigenvector. This helper first + reconstructs the full three-component `E` and `H` fields with `exy2e(...)` + and `exy2h(...)`, then normalizes them to unit forward power using + `_normalized_fields(...)`. + + The normalization target is + + $$ + \Re\left[\mathrm{inner\_product}(e, h, \mathrm{conj\_h}=True)\right] = 1, + $$ + + so the returned fields represent a unit-power forward mode under the + discrete Yee-grid Poynting inner product. """ e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy @@ -374,7 +387,7 @@ def normalized_fields_h( mu: vfdslice | None = None, prop_phase: float = 0, ) -> tuple[vcfdslice_t, vcfdslice_t]: - """ + r""" Given a vector `h_xy` containing the vectorized H_x and H_y fields, returns normalized, vectorized E and H fields for the system. @@ -392,6 +405,13 @@ def normalized_fields_h( Returns: `(e, h)`, where each field is vectorized, normalized, and contains all three vector components. + + Notes: + This is the `H_x/H_y` analogue of `normalized_fields_e(...)`. The final + normalized mode should describe the same physical solution, but because + the overall complex phase and sign are chosen heuristically, + `normalized_fields_e(...)` and `normalized_fields_h(...)` need not return + identical representatives for nearly symmetric modes. """ e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy @@ -409,7 +429,25 @@ def _normalized_fields( mu: vfdslice | None = None, prop_phase: float = 0, ) -> tuple[vcfdslice_t, vcfdslice_t]: - # TODO documentation + r""" + Normalize a reconstructed waveguide mode to unit forward power. + + The eigenproblem solved by `solve_mode(s)` determines only the mode shape and + propagation constant. The overall complex amplitude and sign are still free. + This helper fixes those remaining degrees of freedom in two steps: + + 1. Compute the discrete longitudinal Poynting flux with + `inner_product(e, h, conj_h=True)`, including the half-cell longitudinal + phase adjustment controlled by `prop_phase`. + 2. Multiply both fields by a scalar chosen so that the real forward power is + `1`, then choose a reproducible phase/sign representative by making a + dominant-energy sample real and using a weighted quadrant sum to break + mirror-symmetry ties. + + The sign heuristic is intentionally pragmatic rather than fundamental: it is + only there to make downstream tests and source/overlap construction choose a + consistent representative when the physical mode is symmetric. + """ shape = [s.size for s in dxes[0]] # Find time-averaged Sz and normalize to it @@ -921,7 +959,7 @@ def solve_mode( return vcfdfield2_t(e_xys[0]), wavenumbers[0] -def inner_product( # TODO documentation +def inner_product( e1: vcfdfield2, h2: vcfdfield2, dxes: dx_lists2_t, @@ -929,6 +967,36 @@ def inner_product( # TODO documentation conj_h: bool = False, trapezoid: bool = False, ) -> complex: + r""" + Compute the discrete waveguide overlap / Poynting inner product. + + This is the 2D transverse integral corresponding to the time-averaged + longitudinal Poynting flux, + + $$ + \frac{1}{2}\int (E_x H_y - E_y H_x) \, dx \, dy + $$ + + with the Yee-grid staggering and optional propagation-phase adjustment used + by the waveguide helpers in this module. + + Args: + e1: Vectorized electric field, typically from `exy2e(...)` or + `normalized_fields_e(...)`. + h2: Vectorized magnetic field, typically from `hxy2h(...)`, + `exy2h(...)`, or one of the normalization helpers. + dxes: Two-dimensional Yee-grid spacing lists `[dx_e, dx_h]`. + prop_phase: Phase advance over one propagation cell. This is used to + shift the H field into the same longitudinal reference plane as the + E field. + conj_h: Whether to conjugate `h2` before forming the overlap. Use + `True` for the usual time-averaged power normalization. + trapezoid: Whether to use trapezoidal quadrature instead of the default + rectangular Yee-cell sum. + + Returns: + Complex overlap / longitudinal power integral. + """ shape = [s.size for s in dxes[0]] @@ -951,5 +1019,3 @@ def inner_product( # TODO documentation Sz_b = E1[1] * H2[0] * dxes_real[0][0][:, None] * dxes_real[1][1][None, :] Sz = 0.5 * (Sz_a.sum() - Sz_b.sum()) return Sz - - diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index 19975db..66cc7cc 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -3,6 +3,21 @@ Tools for working with waveguide modes in 3D domains. This module relies heavily on `waveguide_2d` and mostly just transforms its parameters into 2D equivalents and expands the results back into 3D. + +The intended workflow is: + +1. Select a single-cell slice normal to the propagation axis. +2. Solve the corresponding 2D mode problem with `solve_mode(...)`. +3. Turn that mode into a one-sided source with `compute_source(...)`. +4. Build an overlap window with `compute_overlap_e(...)` for port readout. + +`polarity` is part of the public convention throughout this module: + +- `+1` means forward propagation toward increasing index along `axis` +- `-1` means backward propagation toward decreasing index along `axis` + +That same convention controls which side of the selected slice is used for the +overlap window and how the expanded field is phased. """ from typing import Any, cast import warnings @@ -26,9 +41,9 @@ def solve_mode( epsilon: fdfield, mu: fdfield | None = None, ) -> dict[str, complex | NDArray[complexfloating]]: - """ + r""" Given a 3D grid, selects a slice from the grid and attempts to - solve for an eigenmode propagating through that slice. + solve for an eigenmode propagating through that slice. Args: mode_number: Number of the mode, 0-indexed @@ -37,19 +52,22 @@ def solve_mode( axis: Propagation axis (0=x, 1=y, 2=z) polarity: Propagation direction (+1 for +ve, -1 for -ve) slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use - as the waveguide cross-section. `slices[axis]` should select only one item. + as the waveguide cross-section. `slices[axis]` must select exactly one item. epsilon: Dielectric constant mu: Magnetic permeability (default 1 everywhere) Returns: - ``` - { - 'E': NDArray[complexfloating], - 'H': NDArray[complexfloating], - 'wavenumber': complex, - 'wavenumber_2d': complex, - } - ``` + Dictionary containing: + + - `E`: full-grid electric field for the solved mode + - `H`: full-grid magnetic field for the solved mode + - `wavenumber`: propagation constant corrected for the discretized + propagation axis + - `wavenumber_2d`: propagation constant of the reduced 2D eigenproblem + + Notes: + The returned fields are normalized through the `waveguide_2d` + normalization convention before being expanded back to 3D. """ if mu is None: mu = numpy.ones_like(epsilon) @@ -139,7 +157,14 @@ def compute_source( mu: Magnetic permeability (default 1 everywhere) Returns: - J distribution for the unidirectional source + `J` distribution for a one-sided electric-current source. + + Notes: + The source is built from the expanded mode field and a boundary-source + operator. The resulting current is intended to be injected with the + same sign convention used elsewhere in the package: + + `E -= dt * J / epsilon` """ E_expanded = expand_e(E=E, dxes=dxes, wavenumber=wavenumber, axis=axis, polarity=polarity, slices=slices) @@ -167,10 +192,33 @@ def compute_overlap_e( slices: Sequence[slice], omega: float, ) -> cfdfield_t: - """ - Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the - mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) - [assumes reflection symmetry]. + r""" + Build an overlap field for projecting another 3D electric field onto a mode. + + The returned field is intended for the discrete overlap expression + + $$ + \sum \mathrm{overlap\_e} \; E_\mathrm{other}^* + $$ + + where the sum is over the full Yee-grid field storage. + + The construction uses a two-cell window immediately upstream of the selected + slice: + + - for `polarity=+1`, the two cells just before `slices[axis].start` + - for `polarity=-1`, the two cells just after `slices[axis].stop` + + The window is clipped to the simulation domain if necessary. A clipped but + non-empty window raises `RuntimeWarning`; an empty window raises + `ValueError`. + + The derivation below assumes reflection symmetry and the standard waveguide + overlap relation involving + + $$ + \int ((E \times H_\mathrm{mode}) + (E_\mathrm{mode} \times H)) \cdot dn. + $$ E x H_mode + E_mode x H -> Ex Hmy - EyHmx + Emx Hy - Emy Hx (Z-prop) @@ -183,8 +231,6 @@ def compute_overlap_e( B/wu (Ex Emx + Ey Emy) - j/wu (Ex dx Emz + Ey dy Emz) - TODO: add reference - Args: E: E-field of the mode wavenumber: Wavenumber of the mode @@ -195,7 +241,8 @@ def compute_overlap_e( as the waveguide cross-section. slices[axis] should select only one item. Returns: - overlap_e such that `numpy.sum(overlap_e * other_e.conj())` computes the overlap integral + `overlap_e` normalized so that `numpy.sum(overlap_e * E.conj()) == 1` + over the retained overlap window. """ slices = tuple(slices) @@ -240,7 +287,7 @@ def expand_e( polarity: int, slices: Sequence[slice], ) -> cfdfield_t: - """ + r""" Given an eigenmode obtained by `solve_mode`, expands the E-field from the 2D slice where the mode was calculated to the entire domain (along the propagation axis). This assumes the epsilon cross-section remains constant throughout the @@ -258,6 +305,16 @@ def expand_e( Returns: `E`, with the original field expanded along the specified `axis`. + + Notes: + This helper assumes that the waveguide cross-section remains constant + along the propagation axis and applies the phase factor + + $$ + e^{-i \, \mathrm{polarity} \, wavenumber \, \Delta z} + $$ + + to each copied slice. """ slices = tuple(slices) diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index 597e1cb..caedfaf 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -2,63 +2,125 @@ r""" Operators and helper functions for cylindrical waveguides with unchanging cross-section. Waveguide operator is derived according to 10.1364/OL.33.001848. -The curl equations in the complex coordinate system become + +As in `waveguide_2d`, the propagation dependence is separated from the +transverse solve. Here the propagation coordinate is the bend angle `\theta`, +and the fields are assumed to have the form + +$$ +\vec{E}(r, y, \theta), \vec{H}(r, y, \theta) \propto e^{-\imath m \theta}, +$$ + +where `m` is the angular wavenumber returned by `solve_mode(s)`. It is often +convenient to introduce the corresponding linear wavenumber + +$$ +\beta = \frac{m}{r_{\min}}, +$$ + +so that the cylindrical problem resembles the straight-waveguide problem with +additional metric factors. + +Those metric factors live on the staggered radial Yee grids. If the left edge of +the computational window is at `r = r_{\min}`, define the electric-grid and +magnetic-grid radial sample locations by $$ \begin{aligned} --\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \imath \beta frac{E_y}{\tilde{t}_x} \\ --\imath \omega \mu_{yy} H_y &= -\imath \beta E_x - \frac{1}{\hat{t}_x} \tilde{\partial}_x \tilde{t}_x E_z \\ --\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\ -\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta \frac{H_y}{\hat{T}} \\ -\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \{1}{\tilde{t}_x} \hat{\partial}_x \hat{t}_x} H_z \\ -\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\ +r_a(n) &= r_{\min} + \sum_{j \le n} \Delta r_{e, j}, \\ +r_b\!\left(n + \tfrac{1}{2}\right) &= r_{\min} + \tfrac{1}{2}\Delta r_{e, n} + + \sum_{j < n} \Delta r_{h, j}, \end{aligned} $$ -where $t_x = 1 + \frac{\Delta_{x, m}}{R_0}$ is the grid spacing adjusted by the nominal radius $R0$. - -Rewrite the last three equations as +and from them the diagonal metric matrices $$ \begin{aligned} -\imath \beta H_y &= \imath \omega \hat{t}_x \epsilon_{xx} E_x - \hat{t}_x \hat{\partial}_y H_z \\ -\imath \beta H_x &= -\imath \omega \hat{t}_x \epsilon_{yy} E_y - \hat{t}_x \hat{\partial}_x H_z \\ -\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\ +T_a &= \operatorname{diag}(r_a / r_{\min}), \\ +T_b &= \operatorname{diag}(r_b / r_{\min}). \end{aligned} $$ -The derivation then follows the same steps as the straight waveguide, leading to the eigenvalue problem - -$$ -\beta^2 \begin{bmatrix} E_x \\ - E_y \end{bmatrix} = - (\omega^2 \begin{bmatrix} T_b T_b \mu_{yy} \epsilon_{xx} & 0 \\ - 0 & T_a T_a \mu_{xx} \epsilon_{yy} \end{bmatrix} + - \begin{bmatrix} -T_b \mu_{yy} \hat{\partial}_y \\ - T_a \mu_{xx} \hat{\partial}_x \end{bmatrix} T_b \mu_{zz}^{-1} - \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} + - \begin{bmatrix} \tilde{\partial}_x \\ - \tilde{\partial}_y \end{bmatrix} T_a \epsilon_{zz}^{-1} - \begin{bmatrix} \hat{\partial}_x T_b \epsilon_{xx} & \hat{\partial}_y T_a \epsilon_{yy} \end{bmatrix}) - \begin{bmatrix} E_x \\ - E_y \end{bmatrix} -$$ - -which resembles the straight waveguide eigenproblem with additonal $T_a$ and $T_b$ terms. These -are diagonal matrices containing the $t_x$ values: +With the same forward/backward derivative notation used in `waveguide_2d`, the +coordinate-transformed discrete curl equations used here are $$ \begin{aligned} -T_a &= 1 + \frac{\Delta_{x, m }}{R_0} -T_b &= 1 + \frac{\Delta_{x, m + \frac{1}{2} }}{R_0} +-\imath \omega \mu_{rr} H_r &= \tilde{\partial}_y E_z + \imath \beta T_a^{-1} E_y, \\ +-\imath \omega \mu_{yy} H_y &= -\imath \beta T_b^{-1} E_r + - T_b^{-1} \tilde{\partial}_r (T_a E_z), \\ +-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_r E_y - \tilde{\partial}_y E_r, \\ +\imath \beta H_y &= -\imath \omega T_b \epsilon_{rr} E_r - T_b \hat{\partial}_y H_z, \\ +\imath \beta H_r &= \imath \omega T_a \epsilon_{yy} E_y + - T_b T_a^{-1} \hat{\partial}_r (T_b H_z), \\ +\imath \omega E_z &= T_a \epsilon_{zz}^{-1} + \left(\hat{\partial}_r H_y - \hat{\partial}_y H_r\right). \end{aligned} - - -TODO: consider 10.1364/OE.20.021583 for an alternate approach $$ -As in the straight waveguide case, all the functions in this file assume a 2D grid - (i.e. `dxes = [[[dr_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`). +The first three equations are the cylindrical analogue of the straight-guide +relations for `H_r`, `H_y`, and `H_z`. The next two are the metric-weighted +versions of the straight-guide identities for `\imath \beta H_y` and +`\imath \beta H_r`, and the last equation plays the same role as the +longitudinal `E_z` reconstruction in `waveguide_2d`. + +Following the same elimination steps as in `waveguide_2d`, apply +`\imath \beta \tilde{\partial}_r` and `\imath \beta \tilde{\partial}_y` to the +equation for `E_z`, substitute for `\imath \beta H_r` and `\imath \beta H_y`, +and then eliminate `H_z` with + +$$ +H_z = \frac{1}{-\imath \omega \mu_{zz}} +\left(\tilde{\partial}_r E_y - \tilde{\partial}_y E_r\right). +$$ + +This yields the transverse electric eigenproblem implemented by +`cylindrical_operator(...)`: + +$$ +\beta^2 +\begin{bmatrix} E_r \\ E_y \end{bmatrix} += +\left( +\omega^2 +\begin{bmatrix} +T_b^2 \mu_{yy} \epsilon_{xx} & 0 \\ +0 & T_a^2 \mu_{xx} \epsilon_{yy} +\end{bmatrix} ++ +\begin{bmatrix} +-T_b \mu_{yy} \hat{\partial}_y \\ + T_a \mu_{xx} \hat{\partial}_x +\end{bmatrix} +T_b \mu_{zz}^{-1} +\begin{bmatrix} +-\tilde{\partial}_y & \tilde{\partial}_x +\end{bmatrix} ++ +\begin{bmatrix} +\tilde{\partial}_x \\ +\tilde{\partial}_y +\end{bmatrix} +T_a \epsilon_{zz}^{-1} +\begin{bmatrix} +\hat{\partial}_x T_b \epsilon_{xx} & +\hat{\partial}_y T_a \epsilon_{yy} +\end{bmatrix} +\right) +\begin{bmatrix} E_r \\ E_y \end{bmatrix}. +$$ + +Since `\beta = m / r_{\min}`, the solver implemented in this file returns the +angular wavenumber `m`, while the operator itself is most naturally written in +terms of the linear quantity `\beta`. The helpers below reconstruct the full +field components from the solved transverse eigenvector and then normalize the +mode to unit forward power with the same discrete longitudinal Poynting inner +product used by `waveguide_2d`. + +As in the straight-waveguide case, all functions here assume a 2D grid: + +`dxes = [[[dr_e_0, dr_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`. """ from typing import Any, cast from collections.abc import Sequence @@ -94,17 +156,18 @@ def cylindrical_operator( \begin{bmatrix} \tilde{\partial}_x \\ \tilde{\partial}_y \end{bmatrix} T_a \epsilon_{zz}^{-1} \begin{bmatrix} \hat{\partial}_x T_b \epsilon_{xx} & \hat{\partial}_y T_a \epsilon_{yy} \end{bmatrix}) - \begin{bmatrix} E_x \\ + \begin{bmatrix} E_r \\ E_y \end{bmatrix} $$ for use with a field vector of the form `[E_r, E_y]`. This operator can be used to form an eigenvalue problem of the form - A @ [E_r, E_y] = wavenumber**2 * [E_r, E_y] + A @ [E_r, E_y] = beta**2 * [E_r, E_y] which can then be solved for the eigenmodes of the system - (an `exp(-i * wavenumber * theta)` theta-dependence is assumed for the fields). + (an `exp(-i * angular_wavenumber * theta)` theta-dependence is assumed for + the fields, with `beta = angular_wavenumber / rmin`). (NOTE: See module docs and 10.1364/OL.33.001848) @@ -270,7 +333,7 @@ def exy2h( mu: vfdslice | None = None ) -> sparse.sparray: """ - Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, + Operator which transforms the vector `e_xy` containing the vectorized E_r and E_y fields, into a vectorized H containing all three H components Args: @@ -298,11 +361,11 @@ def exy2e( epsilon: vfdslice, ) -> sparse.sparray: """ - Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, + Operator which transforms the vector `e_xy` containing the vectorized E_r and E_y fields, into a vectorized E containing all three E components Unlike the straight waveguide case, the H_z components do not cancel and must be calculated - from E_x and E_y in order to then calculate E_z. + from E_r and E_y in order to then calculate E_z. Args: angular_wavenumber: Wavenumber assuming fields have theta-dependence of @@ -360,9 +423,10 @@ def e2h( This operator is created directly from the initial coordinate-transformed equations: $$ \begin{aligned} - \imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta \frac{H_y}{\hat{T}} \\ - \imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \{1}{\tilde{t}_x} \hat{\partial}_x \hat{t}_x} H_z \\ - \imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\ + -\imath \omega \mu_{rr} H_r &= \tilde{\partial}_y E_z + \imath \beta T_a^{-1} E_y, \\ + -\imath \omega \mu_{yy} H_y &= -\imath \beta T_b^{-1} E_r + - T_b^{-1} \tilde{\partial}_r (T_a E_z), \\ + -\imath \omega \mu_{zz} H_z &= \tilde{\partial}_r E_y - \tilde{\partial}_y E_r, \end{aligned} $$ @@ -397,15 +461,18 @@ def dxes2T( rmin: float, ) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]: r""" - Returns the $T_a$ and $T_b$ diagonal matrices which are used to apply the cylindrical - coordinate transformation in various operators. + Construct the cylindrical metric matrices $T_a$ and $T_b$. + + `T_a` is sampled on the E-grid radial locations, while `T_b` is sampled on + the staggered H-grid radial locations. These are the diagonal matrices that + convert the straight-waveguide algebra into its cylindrical counterpart. Args: dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) rmin: Radius at the left edge of the simulation domain (at minimum 'x') Returns: - Sparse matrix representations of the operators Ta and Tb + Sparse diagonal matrices `(T_a, T_b)`. """ ra = rmin + numpy.cumsum(dxes[0][0]) # Radius at Ey points rb = rmin + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0]) # Radius at Ex points @@ -427,12 +494,12 @@ def normalized_fields_e( mu: vfdslice | None = None, prop_phase: float = 0, ) -> tuple[vcfdslice_t, vcfdslice_t]: - """ - Given a vector `e_xy` containing the vectorized E_x and E_y fields, - returns normalized, vectorized E and H fields for the system. + r""" + Given a vector `e_xy` containing the vectorized E_r and E_y fields, + returns normalized, vectorized E and H fields for the system. Args: - e_xy: Vector containing E_x and E_y fields + e_xy: Vector containing E_r and E_y fields angular_wavenumber: Wavenumber assuming fields have theta-dependence of `exp(-i * angular_wavenumber * theta)`. It should satisfy `operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy` @@ -447,6 +514,18 @@ def normalized_fields_e( Returns: `(e, h)`, where each field is vectorized, normalized, and contains all three vector components. + + Notes: + The normalization step is delegated to `_normalized_fields(...)`, which + enforces unit forward power under the discrete inner product + + $$ + \frac{1}{2}\int (E_r H_y^* - E_y H_r^*) \, dr \, dy. + $$ + + The angular wavenumber `m` is first converted into the full three-component + fields, then the overall complex phase and sign are fixed so the result is + reproducible for symmetric modes. """ e = exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon) @ e_xy h = exy2h(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon, mu=mu) @ e_xy @@ -465,8 +544,30 @@ def _normalized_fields( mu: vfdslice | None = None, prop_phase: float = 0, ) -> tuple[vcfdslice_t, vcfdslice_t]: + r""" + Normalize a cylindrical waveguide mode to unit forward power. + + The cylindrical helpers reuse the straight-waveguide inner product after the + field reconstruction step. The extra metric factors have already been folded + into the reconstructed `e`/`h` fields through `dxes2T(...)` and the + cylindrical `exy2e(...)` / `exy2h(...)` operators, so the same discrete + longitudinal Poynting integral can be used here. + + The normalization procedure is: + + 1. Flip the magnetic field sign so the reconstructed `(e, h)` pair follows + the same forward-power convention as `waveguide_2d`. + 2. Compute the time-averaged forward power with + `waveguide_2d.inner_product(..., conj_h=True)`. + 3. Scale by `1 / sqrt(S_z)` so the resulting mode has unit forward power. + 4. Remove the arbitrary complex phase and apply a quadrant-sum sign heuristic + so symmetric modes choose a stable representative. + + `prop_phase` has the same meaning as in `waveguide_2d`: it compensates for + the half-cell longitudinal staggering between the E and H fields when the + propagation direction is itself discretized. + """ h *= -1 - # TODO documentation for normalized_fields shape = [s.size for s in dxes[0]] # Find time-averaged Sz and normalize to it diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py index 2334338..2b15c59 100644 --- a/meanas/fdtd/__init__.py +++ b/meanas/fdtd/__init__.py @@ -168,7 +168,44 @@ t=0 (assuming the source is off for t<0 this gives $\sim 10^{-3}$ error at t=0). Boundary conditions =================== -# TODO notes about boundaries / PMLs + +`meanas.fdtd` exposes two boundary-related building blocks: + +- `conducting_boundary(...)` for simple perfect-electric-conductor style field + clamping at one face of the domain. +- `cpml_params(...)` and `updates_with_cpml(...)` for convolutional perfectly + matched layers (CPMLs) attached to one or more faces of the Yee grid. + +`updates_with_cpml(...)` accepts a three-by-two table of CPML parameter blocks: + +``` +cpml_params[axis][polarity_index] +``` + +where `axis` is `0`, `1`, or `2` and `polarity_index` corresponds to `(-1, +1)`. +Passing `None` for one entry disables CPML on that face while leaving the other +directions unchanged. This is how mixed boundary setups such as "absorbing in x, +periodic in y/z" are expressed. + +When comparing an FDTD run against an FDFD solve, use the same stretched +coordinate system in both places: + +1. Build the FDTD update with the desired CPML parameters. +2. Stretch the FDFD `dxes` with the matching SCPML transform. +3. Compare the extracted phasor against the FDFD field or residual on those + stretched `dxes`. + +The electric-current sign convention used throughout the examples and tests is + +$$ +E \leftarrow E - \Delta_t J / \epsilon +$$ + +which matches the FDFD right-hand side + +$$ +-i \omega J. +$$ """ from .base import ( diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py index 76888ca..6df30dc 100644 --- a/meanas/fdtd/energy.py +++ b/meanas/fdtd/energy.py @@ -4,7 +4,21 @@ from ..fdmath import dx_lists_t, fdfield_t, fdfield from ..fdmath.functional import deriv_back -# TODO documentation +""" +Energy- and flux-accounting helpers for Yee-grid FDTD fields. + +These functions complement the derivation in `meanas.fdtd`: + +- `poynting(...)` and `poynting_divergence(...)` evaluate the discrete flux terms + from the exact time-domain Poynting identity. +- `energy_hstep(...)` / `energy_estep(...)` evaluate the two staggered energy + expressions. +- `delta_energy_*` helpers evaluate the corresponding energy changes between + adjacent half-steps. + +The helpers are intended for diagnostics, regression tests, and consistency +checks between source work, field energy, and flux through cell faces. +""" def poynting( @@ -252,13 +266,23 @@ def delta_energy_j( e1: fdfield, dxes: dx_lists_t | None = None, ) -> fdfield_t: - """ - Calculate + r""" + Calculate the electric-current work term $J \cdot E$ on the Yee grid. - Note that each value of $J$ contributes to the energy twice (i.e. once per field update) - despite only causing the value of $E$ to change once (same for $M$ and $H$). + This is the source contribution that appears beside the flux divergence in + the discrete Poynting identities documented in `meanas.fdtd`. + Note that each value of `J` contributes twice in a full Yee cycle (once per + half-step energy balance) even though it directly changes `E` only once. + Args: + j0: Electric-current density sampled at the same half-step as the + current work term. + e1: Electric field sampled at the matching integer timestep. + dxes: Grid description; defaults to unit spacing. + + Returns: + Per-cell source-work contribution with the scalar field shape. """ if dxes is None: dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) @@ -277,6 +301,20 @@ def dxmul( mu: fdfield | float | None = None, dxes: dx_lists_t | None = None, ) -> fdfield_t: + """ + Multiply E- and H-like field products by material weights and cell volumes. + + Args: + ee: Three-component electric-field product, such as `e0 * e2`. + hh: Three-component magnetic-field product, such as `h1 * h1`. + epsilon: Electric material weight; defaults to `1`. + mu: Magnetic material weight; defaults to `1`. + dxes: Grid description; defaults to unit spacing. + + Returns: + Scalar field containing the weighted electric plus magnetic contribution + for each Yee cell. + """ if epsilon is None: epsilon = 1 if mu is None: diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py index dec3d83..bf61b4e 100644 --- a/meanas/fdtd/pml.py +++ b/meanas/fdtd/pml.py @@ -1,9 +1,19 @@ """ -PML implementations +Convolutional perfectly matched layer (CPML) support for FDTD updates. -#TODO discussion of PMLs -#TODO cpml documentation +The helpers in this module construct per-face CPML parameters and then wrap the +standard Yee updates with the additional auxiliary `psi` fields needed by the +CPML recurrence. +The intended call pattern is: + +1. Build a `cpml_params[axis][polarity_index]` table with `cpml_params(...)`. +2. Pass that table into `updates_with_cpml(...)` together with `dt`, `dxes`, and + `epsilon`. +3. Advance the returned `update_E` / `update_H` closures in the simulation loop. + +Each face can be enabled or disabled independently by replacing one table entry +with `None`. """ # TODO retest pmls! @@ -32,6 +42,29 @@ def cpml_params( ma: float = 1, cfs_alpha: float = 0, ) -> dict[str, Any]: + """ + Construct the parameter block for one CPML face. + + Args: + axis: Which Cartesian axis the CPML is normal to (`0`, `1`, or `2`). + polarity: Which face along that axis (`-1` for the low-index face, + `+1` for the high-index face). + dt: Timestep used by the Yee update. + thickness: Number of Yee cells occupied by the CPML region. + ln_R_per_layer: Logarithmic attenuation target per layer. + epsilon_eff: Effective permittivity used when choosing the CPML scaling. + mu_eff: Effective permeability used when choosing the CPML scaling. + m: Polynomial grading exponent for `sigma` and `kappa`. + ma: Polynomial grading exponent for the complex-frequency shift `alpha`. + cfs_alpha: Maximum complex-frequency shift parameter. + + Returns: + Dictionary with: + + - `param_e`: `(p0, p1, p2)` arrays for the E update + - `param_h`: `(p0, p1, p2)` arrays for the H update + - `region`: slice tuple selecting the CPML cells on that face + """ if axis not in range(3): raise Exception(f'Invalid axis: {axis}') @@ -98,6 +131,27 @@ def updates_with_cpml( dtype: DTypeLike = numpy.float32, ) -> tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None], Callable[[fdfield_t, fdfield_t, fdfield_t], None]]: + """ + Build Yee-step update closures augmented with CPML terms. + + Args: + cpml_params: Three-by-two sequence indexed as `[axis][polarity_index]`. + Entries are the dictionaries returned by `cpml_params(...)`; use + `None` to disable CPML on one face. + dt: Timestep. + dxes: Yee-grid spacing lists `[dx_e, dx_h]`. + epsilon: Electric material distribution used by the E update. + dtype: Storage dtype for the auxiliary CPML state arrays. + + Returns: + `(update_E, update_H)` closures with the same call shape as the basic + Yee updates: + + - `update_E(e, h, epsilon)` + - `update_H(e, h, mu)` + + The closures retain the CPML auxiliary state internally. + """ Dfx, Dfy, Dfz = deriv_forward(dxes[1]) Dbx, Dby, Dbz = deriv_back(dxes[1]) From a82eb5858a2f72201703bf3a25ac5158e95cb569 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 15:05:35 -0700 Subject: [PATCH 411/437] [docs] switch generated docs to MkDocs --- .gitignore | 4 + README.md | 27 + docs/api/eigensolvers.md | 3 + docs/api/fdfd.md | 15 + docs/api/fdmath.md | 13 + docs/api/fdtd.md | 15 + docs/api/index.md | 14 + docs/api/meanas.md | 3 + docs/api/waveguides.md | 7 + docs/assets/vendor/mathjax/core.js | 1 + docs/assets/vendor/mathjax/input/asciimath.js | 1 + docs/assets/vendor/mathjax/input/mml.js | 1 + .../vendor/mathjax/input/mml/entities.js | 1 + docs/assets/vendor/mathjax/input/tex-full.js | 34 ++ docs/assets/vendor/mathjax/loader.js | 1 + docs/assets/vendor/mathjax/manifest.json | 38 ++ docs/assets/vendor/mathjax/output/chtml.js | 1 + .../vendor/mathjax/output/chtml/fonts/tex.js | 1 + .../fonts/woff-v2/MathJax_AMS-Regular.woff | Bin 0 -> 40808 bytes .../woff-v2/MathJax_Calligraphic-Bold.woff | Bin 0 -> 9908 bytes .../woff-v2/MathJax_Calligraphic-Regular.woff | Bin 0 -> 9600 bytes .../fonts/woff-v2/MathJax_Fraktur-Bold.woff | Bin 0 -> 22340 bytes .../woff-v2/MathJax_Fraktur-Regular.woff | Bin 0 -> 21480 bytes .../fonts/woff-v2/MathJax_Main-Bold.woff | Bin 0 -> 34464 bytes .../fonts/woff-v2/MathJax_Main-Italic.woff | Bin 0 -> 20832 bytes .../fonts/woff-v2/MathJax_Main-Regular.woff | Bin 0 -> 34160 bytes .../woff-v2/MathJax_Math-BoldItalic.woff | Bin 0 -> 19776 bytes .../fonts/woff-v2/MathJax_Math-Italic.woff | Bin 0 -> 19360 bytes .../fonts/woff-v2/MathJax_Math-Regular.woff | Bin 0 -> 19288 bytes .../fonts/woff-v2/MathJax_SansSerif-Bold.woff | Bin 0 -> 15944 bytes .../woff-v2/MathJax_SansSerif-Italic.woff | Bin 0 -> 14628 bytes .../woff-v2/MathJax_SansSerif-Regular.woff | Bin 0 -> 12660 bytes .../fonts/woff-v2/MathJax_Script-Regular.woff | Bin 0 -> 11852 bytes .../fonts/woff-v2/MathJax_Size1-Regular.woff | Bin 0 -> 5792 bytes .../fonts/woff-v2/MathJax_Size2-Regular.woff | Bin 0 -> 5464 bytes .../fonts/woff-v2/MathJax_Size3-Regular.woff | Bin 0 -> 3244 bytes .../fonts/woff-v2/MathJax_Size4-Regular.woff | Bin 0 -> 5148 bytes .../woff-v2/MathJax_Typewriter-Regular.woff | Bin 0 -> 17604 bytes .../fonts/woff-v2/MathJax_Vector-Bold.woff | Bin 0 -> 1116 bytes .../fonts/woff-v2/MathJax_Vector-Regular.woff | Bin 0 -> 1136 bytes .../chtml/fonts/woff-v2/MathJax_Zero.woff | Bin 0 -> 1368 bytes docs/assets/vendor/mathjax/startup.js | 1 + docs/index.md | 33 ++ docs/javascripts/mathjax.js | 19 + docs/stylesheets/extra.css | 13 + make_docs.sh | 20 +- mkdocs.yml | 76 +++ pdoc_templates/config.mako | 47 -- pdoc_templates/css.mako | 389 ------------- pdoc_templates/html.mako | 445 --------------- pdoc_templates/html_helpers.py | 539 ------------------ pdoc_templates/pdf.mako | 185 ------ pdoc_templates/pdoc.css | 381 ------------- pyproject.toml | 22 +- 54 files changed, 350 insertions(+), 2000 deletions(-) create mode 100644 docs/api/eigensolvers.md create mode 100644 docs/api/fdfd.md create mode 100644 docs/api/fdmath.md create mode 100644 docs/api/fdtd.md create mode 100644 docs/api/index.md create mode 100644 docs/api/meanas.md create mode 100644 docs/api/waveguides.md create mode 100644 docs/assets/vendor/mathjax/core.js create mode 100644 docs/assets/vendor/mathjax/input/asciimath.js create mode 100644 docs/assets/vendor/mathjax/input/mml.js create mode 100644 docs/assets/vendor/mathjax/input/mml/entities.js create mode 100644 docs/assets/vendor/mathjax/input/tex-full.js create mode 100644 docs/assets/vendor/mathjax/loader.js create mode 100644 docs/assets/vendor/mathjax/manifest.json create mode 100644 docs/assets/vendor/mathjax/output/chtml.js create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/tex.js create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff create mode 100644 docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Zero.woff create mode 100644 docs/assets/vendor/mathjax/startup.js create mode 100644 docs/index.md create mode 100644 docs/javascripts/mathjax.js create mode 100644 docs/stylesheets/extra.css create mode 100644 mkdocs.yml delete mode 100644 pdoc_templates/config.mako delete mode 100644 pdoc_templates/css.mako delete mode 100644 pdoc_templates/html.mako delete mode 100644 pdoc_templates/html_helpers.py delete mode 100644 pdoc_templates/pdf.mako delete mode 100644 pdoc_templates/pdoc.css diff --git a/.gitignore b/.gitignore index ff695f0..f06c106 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,10 @@ coverage.xml # documentation doc/ +site/ +_doc_mathimg/ +doc.md +doc.htex # PyBuilder target/ diff --git a/README.md b/README.md index c3632c0..01bc257 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,33 @@ The most mature user-facing workflows are: `meanas.fdtd.accumulate_phasor(...)`, and compare those phasors against an FDFD reference on the same Yee grid. +## Documentation + +API and workflow docs are generated from the package docstrings with +[MkDocs](https://www.mkdocs.org/), [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/), +and [mkdocstrings](https://mkdocstrings.github.io/). + +Install the docs toolchain with: + +```bash +pip3 install -e './meanas[docs]' +``` + +Then build the docs site with: + +```bash +./make_docs.sh +``` + +This produces: + +- a normal multi-page site under `site/` +- a combined printable single-page HTML site under `site/print_page/` +- an optional fully inlined `site/standalone.html` when `htmlark` is available + +The docs build uses a local MathJax bundle vendored under `docs/assets/`, so +the rendered HTML does not rely on external services for equation rendering. + Tracked examples under `examples/` are the intended starting points: - `examples/fdtd.py`: broadband FDTD pulse excitation, phasor extraction, and a diff --git a/docs/api/eigensolvers.md b/docs/api/eigensolvers.md new file mode 100644 index 0000000..7f16e01 --- /dev/null +++ b/docs/api/eigensolvers.md @@ -0,0 +1,3 @@ +# eigensolvers + +::: meanas.eigensolvers diff --git a/docs/api/fdfd.md b/docs/api/fdfd.md new file mode 100644 index 0000000..e47c278 --- /dev/null +++ b/docs/api/fdfd.md @@ -0,0 +1,15 @@ +# fdfd + +::: meanas.fdfd + +## Core operator layers + +::: meanas.fdfd.functional + +::: meanas.fdfd.operators + +::: meanas.fdfd.solvers + +::: meanas.fdfd.scpml + +::: meanas.fdfd.farfield diff --git a/docs/api/fdmath.md b/docs/api/fdmath.md new file mode 100644 index 0000000..3623207 --- /dev/null +++ b/docs/api/fdmath.md @@ -0,0 +1,13 @@ +# fdmath + +::: meanas.fdmath + +## Functional and sparse operators + +::: meanas.fdmath.functional + +::: meanas.fdmath.operators + +::: meanas.fdmath.vectorization + +::: meanas.fdmath.types diff --git a/docs/api/fdtd.md b/docs/api/fdtd.md new file mode 100644 index 0000000..3e1823c --- /dev/null +++ b/docs/api/fdtd.md @@ -0,0 +1,15 @@ +# fdtd + +::: meanas.fdtd + +## Core update and analysis helpers + +::: meanas.fdtd.base + +::: meanas.fdtd.pml + +::: meanas.fdtd.boundaries + +::: meanas.fdtd.energy + +::: meanas.fdtd.phasor diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..d30ae80 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,14 @@ +# API Overview + +The package is documented directly from its docstrings. The most useful entry +points are: + +- [meanas](meanas.md): top-level package overview +- [eigensolvers](eigensolvers.md): generic eigenvalue utilities used by the mode solvers +- [fdfd](fdfd.md): frequency-domain operators, sources, PML, solvers, and far-field transforms +- [waveguides](waveguides.md): straight, cylindrical, and 3D waveguide mode helpers +- [fdtd](fdtd.md): timestepping, CPML, energy/flux helpers, and phasor extraction +- [fdmath](fdmath.md): shared discrete operators, vectorization helpers, and derivation background + +The waveguide and FDTD pages are the best places to start if you want the +mathematical derivations rather than just the callable reference. diff --git a/docs/api/meanas.md b/docs/api/meanas.md new file mode 100644 index 0000000..d5c5f4e --- /dev/null +++ b/docs/api/meanas.md @@ -0,0 +1,3 @@ +# meanas + +::: meanas diff --git a/docs/api/waveguides.md b/docs/api/waveguides.md new file mode 100644 index 0000000..834614c --- /dev/null +++ b/docs/api/waveguides.md @@ -0,0 +1,7 @@ +# waveguides + +::: meanas.fdfd.waveguide_2d + +::: meanas.fdfd.waveguide_3d + +::: meanas.fdfd.waveguide_cyl diff --git a/docs/assets/vendor/mathjax/core.js b/docs/assets/vendor/mathjax/core.js new file mode 100644 index 0000000..8b9db53 --- /dev/null +++ b/docs/assets/vendor/mathjax/core.js @@ -0,0 +1 @@ +!function(){"use strict";var t,e,r,n,o,i,s,a,l,u,c,p,f,h,d,y,O,M,E,v,b,m,g,L,N,R,T,S,A,C,_,x,I,w,P,j,D,B,k,X,H,W,F,q,J,G,z,V,U,K,$,Y,Z,Q,tt,et,rt,nt,ot,it,st,at,lt,ut,ct,pt,ft,ht,dt,yt,Ot,Mt,Et,vt,bt,mt,gt,Lt={444:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.HTMLAdaptor=void 0;var s=function(t){function e(e){var r=t.call(this,e.document)||this;return r.window=e,r.parser=new e.DOMParser,r}return o(e,t),e.prototype.parse=function(t,e){return void 0===e&&(e="text/html"),this.parser.parseFromString(t,e)},e.prototype.create=function(t,e){return e?this.document.createElementNS(e,t):this.document.createElement(t)},e.prototype.text=function(t){return this.document.createTextNode(t)},e.prototype.head=function(t){return t.head},e.prototype.body=function(t){return t.body},e.prototype.root=function(t){return t.documentElement},e.prototype.doctype=function(t){return""},e.prototype.tags=function(t,e,r){void 0===r&&(r=null);var n=r?t.getElementsByTagNameNS(r,e):t.getElementsByTagName(e);return Array.from(n)},e.prototype.getElements=function(t,e){var r,n,o=[];try{for(var s=i(t),a=s.next();!a.done;a=s.next()){var l=a.value;"string"==typeof l?o=o.concat(Array.from(this.document.querySelectorAll(l))):Array.isArray(l)||l instanceof this.window.NodeList||l instanceof this.window.HTMLCollection?o=o.concat(Array.from(l)):o.push(l)}}catch(t){r={error:t}}finally{try{a&&!a.done&&(n=s.return)&&n.call(s)}finally{if(r)throw r.error}}return o},e.prototype.contains=function(t,e){return t.contains(e)},e.prototype.parent=function(t){return t.parentNode},e.prototype.append=function(t,e){return t.appendChild(e)},e.prototype.insert=function(t,e){return this.parent(e).insertBefore(t,e)},e.prototype.remove=function(t){return this.parent(t).removeChild(t)},e.prototype.replace=function(t,e){return this.parent(e).replaceChild(t,e)},e.prototype.clone=function(t){return t.cloneNode(!0)},e.prototype.split=function(t,e){return t.splitText(e)},e.prototype.next=function(t){return t.nextSibling},e.prototype.previous=function(t){return t.previousSibling},e.prototype.firstChild=function(t){return t.firstChild},e.prototype.lastChild=function(t){return t.lastChild},e.prototype.childNodes=function(t){return Array.from(t.childNodes)},e.prototype.childNode=function(t,e){return t.childNodes[e]},e.prototype.kind=function(t){var e=t.nodeType;return 1===e||3===e||8===e?t.nodeName.toLowerCase():""},e.prototype.value=function(t){return t.nodeValue||""},e.prototype.textContent=function(t){return t.textContent},e.prototype.innerHTML=function(t){return t.innerHTML},e.prototype.outerHTML=function(t){return t.outerHTML},e.prototype.serializeXML=function(t){return(new this.window.XMLSerializer).serializeToString(t)},e.prototype.setAttribute=function(t,e,r,n){return void 0===n&&(n=null),n?(e=n.replace(/.*\//,"")+":"+e.replace(/^.*:/,""),t.setAttributeNS(n,e,r)):t.setAttribute(e,r)},e.prototype.getAttribute=function(t,e){return t.getAttribute(e)},e.prototype.removeAttribute=function(t,e){return t.removeAttribute(e)},e.prototype.hasAttribute=function(t,e){return t.hasAttribute(e)},e.prototype.allAttributes=function(t){return Array.from(t.attributes).map((function(t){return{name:t.name,value:t.value}}))},e.prototype.addClass=function(t,e){t.classList?t.classList.add(e):t.className=(t.className+" "+e).trim()},e.prototype.removeClass=function(t,e){t.classList?t.classList.remove(e):t.className=t.className.split(/ /).filter((function(t){return t!==e})).join(" ")},e.prototype.hasClass=function(t,e){return t.classList?t.classList.contains(e):t.className.split(/ /).indexOf(e)>=0},e.prototype.setStyle=function(t,e,r){t.style[e]=r},e.prototype.getStyle=function(t,e){return t.style[e]},e.prototype.allStyles=function(t){return t.style.cssText},e.prototype.fontSize=function(t){var e=this.window.getComputedStyle(t);return parseFloat(e.fontSize)},e.prototype.fontFamily=function(t){return this.window.getComputedStyle(t).fontFamily||""},e.prototype.nodeSize=function(t,e,r){if(void 0===e&&(e=1),void 0===r&&(r=!1),r&&t.getBBox){var n=t.getBBox();return[n.width/e,n.height/e]}return[t.offsetWidth/e,t.offsetHeight/e]},e.prototype.nodeBBox=function(t){var e=t.getBoundingClientRect();return{left:e.left,right:e.right,top:e.top,bottom:e.bottom}},e}(r(5009).AbstractDOMAdaptor);e.HTMLAdaptor=s},6191:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.browserAdaptor=void 0;var n=r(444);e.browserAdaptor=function(){return new n.HTMLAdaptor(window)}},9515:function(t,e,r){var n=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};function o(t){return"object"==typeof t&&null!==t}function i(t,e){var r,s;try{for(var a=n(Object.keys(e)),l=a.next();!l.done;l=a.next()){var u=l.value;"__esModule"!==u&&(!o(t[u])||!o(e[u])||e[u]instanceof Promise?null!==e[u]&&void 0!==e[u]&&(t[u]=e[u]):i(t[u],e[u]))}}catch(t){r={error:t}}finally{try{l&&!l.done&&(s=a.return)&&s.call(a)}finally{if(r)throw r.error}}return t}Object.defineProperty(e,"__esModule",{value:!0}),e.MathJax=e.combineWithMathJax=e.combineDefaults=e.combineConfig=e.isObject=void 0,e.isObject=o,e.combineConfig=i,e.combineDefaults=function t(e,r,i){var s,a;e[r]||(e[r]={}),e=e[r];try{for(var l=n(Object.keys(i)),u=l.next();!u.done;u=l.next()){var c=u.value;o(e[c])&&o(i[c])?t(e,c,i[c]):null==e[c]&&null!=i[c]&&(e[c]=i[c])}}catch(t){s={error:t}}finally{try{u&&!u.done&&(a=l.return)&&a.call(l)}finally{if(s)throw s.error}}return e},e.combineWithMathJax=function(t){return i(e.MathJax,t)},void 0===r.g.MathJax&&(r.g.MathJax={}),r.g.MathJax.version||(r.g.MathJax={version:"3.1.4",_:{},config:r.g.MathJax}),e.MathJax=r.g.MathJax},5009:function(t,e){var r=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractDOMAdaptor=void 0;var n=function(){function t(t){void 0===t&&(t=null),this.document=t}return t.prototype.node=function(t,e,n,o){var i,s;void 0===e&&(e={}),void 0===n&&(n=[]);var a=this.create(t,o);this.setAttributes(a,e);try{for(var l=r(n),u=l.next();!u.done;u=l.next()){var c=u.value;this.append(a,c)}}catch(t){i={error:t}}finally{try{u&&!u.done&&(s=l.return)&&s.call(l)}finally{if(i)throw i.error}}return a},t.prototype.setAttributes=function(t,e){var n,o,i,s,a,l;if(e.style&&"string"!=typeof e.style)try{for(var u=r(Object.keys(e.style)),c=u.next();!c.done;c=u.next()){var p=c.value;this.setStyle(t,p.replace(/-([a-z])/g,(function(t,e){return e.toUpperCase()})),e.style[p])}}catch(t){n={error:t}}finally{try{c&&!c.done&&(o=u.return)&&o.call(u)}finally{if(n)throw n.error}}if(e.properties)try{for(var f=r(Object.keys(e.properties)),h=f.next();!h.done;h=f.next()){t[p=h.value]=e.properties[p]}}catch(t){i={error:t}}finally{try{h&&!h.done&&(s=f.return)&&s.call(f)}finally{if(i)throw i.error}}try{for(var d=r(Object.keys(e)),y=d.next();!y.done;y=d.next()){"style"===(p=y.value)&&"string"!=typeof e.style||"properties"===p||this.setAttribute(t,p,e[p])}}catch(t){a={error:t}}finally{try{y&&!y.done&&(l=d.return)&&l.call(d)}finally{if(a)throw a.error}}},t.prototype.replace=function(t,e){return this.insert(t,e),this.remove(e),e},t.prototype.childNode=function(t,e){return this.childNodes(t)[e]},t.prototype.allClasses=function(t){var e=this.getAttribute(t,"class");return e?e.replace(/ +/g," ").replace(/^ /,"").replace(/ $/,"").split(/ /):[]},t}();e.AbstractDOMAdaptor=n},3494:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractFindMath=void 0;var n=r(7233),o=function(){function t(t){var e=this.constructor;this.options=n.userOptions(n.defaultOptions({},e.OPTIONS),t)}return t.OPTIONS={},t}();e.AbstractFindMath=o},3670:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractHandler=void 0;var i=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e}(r(5722).AbstractMathDocument),s=function(){function t(t,e){void 0===e&&(e=5),this.documentClass=i,this.adaptor=t,this.priority=e}return Object.defineProperty(t.prototype,"name",{get:function(){return this.constructor.NAME},enumerable:!1,configurable:!0}),t.prototype.handlesDocument=function(t){return!1},t.prototype.create=function(t,e){return new this.documentClass(t,this.adaptor,e)},t.NAME="generic",t}();e.AbstractHandler=s},805:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.HandlerList=void 0;var s=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.register=function(t){return this.add(t,t.priority)},e.prototype.unregister=function(t){this.remove(t)},e.prototype.handlesDocument=function(t){var e,r;try{for(var n=i(this),o=n.next();!o.done;o=n.next()){var s=o.value.item;if(s.handlesDocument(t))return s}}catch(t){e={error:t}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(e)throw e.error}}throw new Error("Can't find handler for document")},e.prototype.document=function(t,e){return void 0===e&&(e=null),this.handlesDocument(t).create(t,e)},e}(r(8666).PrioritizedList);e.HandlerList=s},9206:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractInputJax=void 0;var n=r(7233),o=r(7525),i=function(){function t(t){void 0===t&&(t={}),this.adaptor=null,this.mmlFactory=null;var e=this.constructor;this.options=n.userOptions(n.defaultOptions({},e.OPTIONS),t),this.preFilters=new o.FunctionList,this.postFilters=new o.FunctionList}return Object.defineProperty(t.prototype,"name",{get:function(){return this.constructor.NAME},enumerable:!1,configurable:!0}),t.prototype.setAdaptor=function(t){this.adaptor=t},t.prototype.setMmlFactory=function(t){this.mmlFactory=t},t.prototype.initialize=function(){},t.prototype.reset=function(){for(var t=[],e=0;e=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},s=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,i=r.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},a=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r=e&&a.item.renderDoc(t))return}}catch(t){r={error:t}}finally{try{s&&!s.done&&(n=o.return)&&n.call(o)}finally{if(r)throw r.error}}},e.prototype.renderMath=function(t,e,r){var n,o;void 0===r&&(r=f.STATE.UNPROCESSED);try{for(var s=i(this.items),a=s.next();!a.done;a=s.next()){var l=a.value;if(l.priority>=r&&l.item.renderMath(t,e))return}}catch(t){n={error:t}}finally{try{a&&!a.done&&(o=s.return)&&o.call(s)}finally{if(n)throw n.error}}},e.prototype.renderConvert=function(t,e,r){var n,o;void 0===r&&(r=f.STATE.LAST);try{for(var s=i(this.items),a=s.next();!a.done;a=s.next()){var l=a.value;if(l.priority>r)return;if(l.item.convert&&l.item.renderMath(t,e))return}}catch(t){n={error:t}}finally{try{a&&!a.done&&(o=s.return)&&o.call(s)}finally{if(n)throw n.error}}},e.prototype.findID=function(t){var e,r;try{for(var n=i(this.items),o=n.next();!o.done;o=n.next()){var s=o.value;if(s.item.id===t)return s.item}}catch(t){e={error:t}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(e)throw e.error}}return null},e}(r(8666).PrioritizedList);e.RenderList=y,e.resetOptions={all:!1,processed:!1,inputJax:null,outputJax:null},e.resetAllOptions={all:!0,processed:!0,inputJax:[],outputJax:[]};var O=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.compile=function(t){return null},e}(u.AbstractInputJax),M=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.typeset=function(t,e){return void 0===e&&(e=null),null},e.prototype.escaped=function(t,e){return null},e}(c.AbstractOutputJax),E=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e}(p.AbstractMathList),v=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e}(f.AbstractMathItem),b=function(){function t(e,r,n){var o=this,i=this.constructor;this.document=e,this.options=l.userOptions(l.defaultOptions({},i.OPTIONS),n),this.math=new(this.options.MathList||E),this.renderActions=y.create(this.options.renderActions),this.processed=new t.ProcessBits,this.outputJax=this.options.OutputJax||new M;var s=this.options.InputJax||[new O];Array.isArray(s)||(s=[s]),this.inputJax=s,this.adaptor=r,this.outputJax.setAdaptor(r),this.inputJax.map((function(t){return t.setAdaptor(r)})),this.mmlFactory=this.options.MmlFactory||new h.MmlFactory,this.inputJax.map((function(t){return t.setMmlFactory(o.mmlFactory)})),this.outputJax.initialize(),this.inputJax.map((function(t){return t.initialize()}))}return Object.defineProperty(t.prototype,"kind",{get:function(){return this.constructor.KIND},enumerable:!1,configurable:!0}),t.prototype.addRenderAction=function(t){for(var e=[],r=1;r=r&&this.state(r-1),t.renderActions.renderMath(this,t,r)},t.prototype.convert=function(t,r){void 0===r&&(r=e.STATE.LAST),t.renderActions.renderConvert(this,t,r)},t.prototype.compile=function(t){this.state()=e.STATE.INSERTED&&this.removeFromDocument(r),t=e.STATE.TYPESET&&(this.outputData={}),t=e.STATE.COMPILED&&(this.inputData={}),this._state=t),this._state},t.prototype.reset=function(t){void 0===t&&(t=!1),this.state(e.STATE.UNPROCESSED,t)},t}();e.AbstractMathItem=r,e.STATE={UNPROCESSED:0,FINDMATH:10,COMPILED:20,CONVERT:100,METRICS:110,RERENDER:125,TYPESET:150,INSERTED:200,LAST:1e4},e.newState=function(t,r){if(t in e.STATE)throw Error("State "+t+" already exists");e.STATE[t]=r}},9e3:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractMathList=void 0;var i=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.isBefore=function(t,e){return t.start.i=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.Attributes=e.INHERIT=void 0,e.INHERIT="_inherit_";var n=function(){function t(t,e){this.global=e,this.defaults=Object.create(e),this.inherited=Object.create(this.defaults),this.attributes=Object.create(this.inherited),Object.assign(this.defaults,t)}return t.prototype.set=function(t,e){this.attributes[t]=e},t.prototype.setList=function(t){Object.assign(this.attributes,t)},t.prototype.get=function(t){var r=this.attributes[t];return r===e.INHERIT&&(r=this.global[t]),r},t.prototype.getExplicit=function(t){if(this.attributes.hasOwnProperty(t))return this.attributes[t]},t.prototype.getList=function(){for(var t,e,n=[],o=0;o=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.MathMLVisitor=void 0;var s=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.document=null,e}return o(e,t),e.prototype.visitTree=function(t,e){this.document=e;var r=e.createElement("top");return this.visitNode(t,r),this.document=null,r.firstChild},e.prototype.visitTextNode=function(t,e){e.appendChild(this.document.createTextNode(t.getText()))},e.prototype.visitXMLNode=function(t,e){e.appendChild(t.getXML().cloneNode(!0))},e.prototype.visitInferredMrowNode=function(t,e){var r,n;try{for(var o=i(t.childNodes),s=o.next();!s.done;s=o.next()){var a=s.value;this.visitNode(a,e)}}catch(t){r={error:t}}finally{try{s&&!s.done&&(n=o.return)&&n.call(o)}finally{if(r)throw r.error}}},e.prototype.visitDefault=function(t,e){var r,n,o=this.document.createElement(t.kind);this.addAttributes(t,o);try{for(var s=i(t.childNodes),a=s.next();!a.done;a=s.next()){var l=a.value;this.visitNode(l,o)}}catch(t){r={error:t}}finally{try{a&&!a.done&&(n=s.return)&&n.call(s)}finally{if(r)throw r.error}}e.appendChild(o)},e.prototype.addAttributes=function(t,e){var r,n,o=t.attributes,s=o.getExplicitNames();try{for(var a=i(s),l=a.next();!l.done;l=a.next()){var u=l.value;e.setAttribute(u,o.getExplicit(u).toString())}}catch(t){r={error:t}}finally{try{l&&!l.done&&(n=a.return)&&n.call(a)}finally{if(r)throw r.error}}},e}(r(6325).MmlVisitor);e.MathMLVisitor=s},3909:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.MmlFactory=void 0;var i=r(7860),s=r(6336),a=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"MML",{get:function(){return this.node},enumerable:!1,configurable:!0}),e.defaultNodes=s.MML,e}(i.AbstractNodeFactory);e.MmlFactory=a},9007:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,n=arguments.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},a=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,i=r.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s};Object.defineProperty(e,"__esModule",{value:!0}),e.XMLNode=e.TextNode=e.AbstractMmlEmptyNode=e.AbstractMmlBaseNode=e.AbstractMmlLayoutNode=e.AbstractMmlTokenNode=e.AbstractMmlNode=e.indentAttributes=e.TEXCLASSNAMES=e.TEXCLASS=void 0;var l=r(91),u=r(4596);e.TEXCLASS={ORD:0,OP:1,BIN:2,REL:3,OPEN:4,CLOSE:5,PUNCT:6,INNER:7,VCENTER:8,NONE:-1},e.TEXCLASSNAMES=["ORD","OP","BIN","REL","OPEN","CLOSE","PUNCT","INNER","VCENTER"];var c=["","thinmathspace","mediummathspace","thickmathspace"],p=[[0,-1,2,3,0,0,0,1],[-1,-1,0,3,0,0,0,1],[2,2,0,0,2,0,0,2],[3,3,0,0,3,0,0,3],[0,0,0,0,0,0,0,0],[0,-1,2,3,0,0,0,1],[1,1,0,1,1,1,1,1],[1,-1,2,3,1,0,1,1]];e.indentAttributes=["indentalign","indentalignfirst","indentshift","indentshiftfirst"];var f=function(t){function r(e,r,n){void 0===r&&(r={}),void 0===n&&(n=[]);var o=t.call(this,e)||this;return o.prevClass=null,o.prevLevel=null,o.texclass=null,o.arity<0&&(o.childNodes=[e.create("inferredMrow")],o.childNodes[0].parent=o),o.setChildren(n),o.attributes=new l.Attributes(e.getNodeClass(o.kind).defaults,e.getNodeClass("math").defaults),o.attributes.setList(r),o}return o(r,t),Object.defineProperty(r.prototype,"texClass",{get:function(){return this.texclass},set:function(t){this.texclass=t},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"isToken",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"isEmbellished",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"isSpacelike",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"linebreakContainer",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"hasNewLine",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"arity",{get:function(){return 1/0},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"isInferred",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"Parent",{get:function(){for(var t=this.parent;t&&t.notParent;)t=t.Parent;return t},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"notParent",{get:function(){return!1},enumerable:!1,configurable:!0}),r.prototype.setChildren=function(e){return this.arity<0?this.childNodes[0].setChildren(e):t.prototype.setChildren.call(this,e)},r.prototype.appendChild=function(e){var r,n,o=this;if(this.arity<0)return this.childNodes[0].appendChild(e),e;if(e.isInferred){if(this.arity===1/0)return e.childNodes.forEach((function(e){return t.prototype.appendChild.call(o,e)})),e;var i=e;(e=this.factory.create("mrow")).setChildren(i.childNodes),e.attributes=i.attributes;try{for(var a=s(i.getPropertyNames()),l=a.next();!l.done;l=a.next()){var u=l.value;e.setProperty(u,i.getProperty(u))}}catch(t){r={error:t}}finally{try{l&&!l.done&&(n=a.return)&&n.call(a)}finally{if(r)throw r.error}}}return t.prototype.appendChild.call(this,e)},r.prototype.replaceChild=function(e,r){return this.arity<0?(this.childNodes[0].replaceChild(e,r),e):t.prototype.replaceChild.call(this,e,r)},r.prototype.core=function(){return this},r.prototype.coreMO=function(){return this},r.prototype.coreIndex=function(){return 0},r.prototype.childPosition=function(){for(var t,e,r=this,n=r.parent;n&&n.notParent;)r=n,n=n.parent;if(n){var o=0;try{for(var i=s(n.childNodes),a=i.next();!a.done;a=i.next()){if(a.value===r)return o;o++}}catch(e){t={error:e}}finally{try{a&&!a.done&&(e=i.return)&&e.call(i)}finally{if(t)throw t.error}}}return null},r.prototype.setTeXclass=function(t){return this.getPrevClass(t),null!=this.texClass?this:t},r.prototype.updateTeXclass=function(t){t&&(this.prevClass=t.prevClass,this.prevLevel=t.prevLevel,t.prevClass=t.prevLevel=null,this.texClass=t.texClass)},r.prototype.getPrevClass=function(t){t&&(this.prevClass=t.texClass,this.prevLevel=t.attributes.get("scriptlevel"))},r.prototype.texSpacing=function(){var t=null!=this.prevClass?this.prevClass:e.TEXCLASS.NONE,r=this.texClass||e.TEXCLASS.ORD;if(t===e.TEXCLASS.NONE||r===e.TEXCLASS.NONE)return"";t===e.TEXCLASS.VCENTER&&(t=e.TEXCLASS.ORD),r===e.TEXCLASS.VCENTER&&(r=e.TEXCLASS.ORD);var n=p[t][r];return(this.prevLevel>0||this.attributes.get("scriptlevel")>0)&&n>=0?"":c[Math.abs(n)]},r.prototype.hasSpacingAttributes=function(){return this.isEmbellished&&this.coreMO().hasSpacingAttributes()},r.prototype.setInheritedAttributes=function(t,e,n,o){var i,l;void 0===t&&(t={}),void 0===e&&(e=!1),void 0===n&&(n=0),void 0===o&&(o=!1);var u=this.attributes.getAllDefaults();try{for(var c=s(Object.keys(t)),p=c.next();!p.done;p=c.next()){var f=p.value;if(u.hasOwnProperty(f)||r.alwaysInherit.hasOwnProperty(f)){var h=a(t[f],2),d=h[0],y=h[1];((r.noInherit[d]||{})[this.kind]||{})[f]||this.attributes.setInherited(f,y)}}}catch(t){i={error:t}}finally{try{p&&!p.done&&(l=c.return)&&l.call(c)}finally{if(i)throw i.error}}void 0===this.attributes.getExplicit("displaystyle")&&this.attributes.setInherited("displaystyle",e),void 0===this.attributes.getExplicit("scriptlevel")&&this.attributes.setInherited("scriptlevel",n),o&&this.setProperty("texprimestyle",o);var O=this.arity;if(O>=0&&O!==1/0&&(1===O&&0===this.childNodes.length||1!==O&&this.childNodes.length!==O))if(O=0&&e!==1/0&&(1===e&&0===this.childNodes.length||1!==e&&this.childNodes.length!==e)&&this.mError('Wrong number of children for "'+this.kind+'" node',t,!0),this.verifyChildren(t)}},r.prototype.verifyAttributes=function(t){var e,r;if(t.checkAttributes){var n=this.attributes,o=[];try{for(var i=s(n.getExplicitNames()),a=i.next();!a.done;a=i.next()){var l=a.value;"data-"===l.substr(0,5)||void 0!==n.getDefault(l)||l.match(/^(?:class|style|id|(?:xlink:)?href)$/)||o.push(l)}}catch(t){e={error:t}}finally{try{a&&!a.done&&(r=i.return)&&r.call(i)}finally{if(e)throw e.error}}o.length&&this.mError("Unknown attributes for "+this.kind+" node: "+o.join(", "),t)}},r.prototype.verifyChildren=function(t){var e,r;try{for(var n=s(this.childNodes),o=n.next();!o.done;o=n.next()){o.value.verifyTree(t)}}catch(t){e={error:t}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(e)throw e.error}}},r.prototype.mError=function(t,e,r){if(void 0===r&&(r=!1),this.parent&&this.parent.isKind("merror"))return null;var n=this.factory.create("merror");if(e.fullErrors||r){var o=this.factory.create("mtext"),i=this.factory.create("text");i.setText(e.fullErrors?t:this.kind),o.appendChild(i),n.appendChild(o),this.parent.replaceChild(n,this)}else this.parent.replaceChild(n,this),n.appendChild(this);return n},r.defaults={mathbackground:l.INHERIT,mathcolor:l.INHERIT,mathsize:l.INHERIT,dir:l.INHERIT},r.noInherit={mstyle:{mpadded:{width:!0,height:!0,depth:!0,lspace:!0,voffset:!0},mtable:{width:!0,height:!0,depth:!0,align:!0}},maligngroup:{mrow:{groupalign:!0},mtable:{groupalign:!0}}},r.alwaysInherit={scriptminsize:!0,scriptsizemultiplier:!0},r.verifyDefaults={checkArity:!0,checkAttributes:!1,fullErrors:!1,fixMmultiscripts:!0,fixMtables:!0},r}(u.AbstractNode);e.AbstractMmlNode=f;var h=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"isToken",{get:function(){return!0},enumerable:!1,configurable:!0}),e.prototype.getText=function(){var t,e,r="";try{for(var n=s(this.childNodes),o=n.next();!o.done;o=n.next()){var i=o.value;i instanceof M&&(r+=i.getText())}}catch(e){t={error:e}}finally{try{o&&!o.done&&(e=n.return)&&e.call(n)}finally{if(t)throw t.error}}return r},e.prototype.setChildInheritedAttributes=function(t,e,r,n){var o,i;try{for(var a=s(this.childNodes),l=a.next();!l.done;l=a.next()){var u=l.value;u instanceof f&&u.setInheritedAttributes(t,e,r,n)}}catch(t){o={error:t}}finally{try{l&&!l.done&&(i=a.return)&&i.call(a)}finally{if(o)throw o.error}}},e.prototype.walkTree=function(t,e){var r,n;t(this,e);try{for(var o=s(this.childNodes),i=o.next();!i.done;i=o.next()){var a=i.value;a instanceof f&&a.walkTree(t,e)}}catch(t){r={error:t}}finally{try{i&&!i.done&&(n=o.return)&&n.call(o)}finally{if(r)throw r.error}}return e},e.defaults=i(i({},f.defaults),{mathvariant:"normal",mathsize:l.INHERIT}),e}(f);e.AbstractMmlTokenNode=h;var d=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"isSpacelike",{get:function(){return this.childNodes[0].isSpacelike},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isEmbellished",{get:function(){return this.childNodes[0].isEmbellished},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"arity",{get:function(){return-1},enumerable:!1,configurable:!0}),e.prototype.core=function(){return this.childNodes[0]},e.prototype.coreMO=function(){return this.childNodes[0].coreMO()},e.prototype.setTeXclass=function(t){return t=this.childNodes[0].setTeXclass(t),this.updateTeXclass(this.childNodes[0]),t},e.defaults=f.defaults,e}(f);e.AbstractMmlLayoutNode=d;var y=function(t){function r(){return null!==t&&t.apply(this,arguments)||this}return o(r,t),Object.defineProperty(r.prototype,"isEmbellished",{get:function(){return this.childNodes[0].isEmbellished},enumerable:!1,configurable:!0}),r.prototype.core=function(){return this.childNodes[0]},r.prototype.coreMO=function(){return this.childNodes[0].coreMO()},r.prototype.setTeXclass=function(t){var r,n;this.getPrevClass(t),this.texClass=e.TEXCLASS.ORD;var o=this.childNodes[0];o?this.isEmbellished||o.isKind("mi")?(t=o.setTeXclass(t),this.updateTeXclass(this.core())):(o.setTeXclass(null),t=this):t=this;try{for(var i=s(this.childNodes.slice(1)),a=i.next();!a.done;a=i.next()){var l=a.value;l&&l.setTeXclass(null)}}catch(t){r={error:t}}finally{try{a&&!a.done&&(n=i.return)&&n.call(i)}finally{if(r)throw r.error}}return t},r.defaults=f.defaults,r}(f);e.AbstractMmlBaseNode=y;var O=function(t){function r(){return null!==t&&t.apply(this,arguments)||this}return o(r,t),Object.defineProperty(r.prototype,"isToken",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"isEmbellished",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"isSpacelike",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"linebreakContainer",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"hasNewLine",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"arity",{get:function(){return 0},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"isInferred",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"notParent",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"Parent",{get:function(){return this.parent},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"texClass",{get:function(){return e.TEXCLASS.NONE},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"prevClass",{get:function(){return e.TEXCLASS.NONE},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"prevLevel",{get:function(){return 0},enumerable:!1,configurable:!0}),r.prototype.hasSpacingAttributes=function(){return!1},Object.defineProperty(r.prototype,"attributes",{get:function(){return null},enumerable:!1,configurable:!0}),r.prototype.core=function(){return this},r.prototype.coreMO=function(){return this},r.prototype.coreIndex=function(){return 0},r.prototype.childPosition=function(){return 0},r.prototype.setTeXclass=function(t){return t},r.prototype.texSpacing=function(){return""},r.prototype.setInheritedAttributes=function(t,e,r,n){},r.prototype.inheritAttributesFrom=function(t){},r.prototype.verifyTree=function(t){},r.prototype.mError=function(t,e,r){void 0===r&&(r=!1)},r}(u.AbstractEmptyNode);e.AbstractMmlEmptyNode=O;var M=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.text="",e}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"text"},enumerable:!1,configurable:!0}),e.prototype.getText=function(){return this.text},e.prototype.setText=function(t){return this.text=t,this},e.prototype.toString=function(){return this.text},e}(O);e.TextNode=M;var E=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.xml=null,e.adaptor=null,e}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"XML"},enumerable:!1,configurable:!0}),e.prototype.getXML=function(){return this.xml},e.prototype.setXML=function(t,e){return void 0===e&&(e=null),this.xml=t,this.adaptor=e,this},e.prototype.getSerializedXML=function(){return this.adaptor.serializeXML(this.xml)},e.prototype.toString=function(){return"XML data"},e}(O);e.XMLNode=E},3948:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,n=arguments.length;rthis.childNodes.length&&(t=1),this.attributes.set("selection",t)},e.defaults=i(i({},s.AbstractMmlNode.defaults),{actiontype:"toggle",selection:1}),e}(s.AbstractMmlNode);e.MmlMaction=a},142:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,n=arguments.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMfenced=void 0;var a=r(9007),l=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.texclass=a.TEXCLASS.INNER,e.separators=[],e.open=null,e.close=null,e}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"mfenced"},enumerable:!1,configurable:!0}),e.prototype.setTeXclass=function(t){this.getPrevClass(t),this.open&&(t=this.open.setTeXclass(t)),this.childNodes[0]&&(t=this.childNodes[0].setTeXclass(t));for(var e=1,r=this.childNodes.length;e=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMfrac=void 0;var a=r(9007),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"mfrac"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"arity",{get:function(){return 2},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"linebreakContainer",{get:function(){return!0},enumerable:!1,configurable:!0}),e.prototype.setTeXclass=function(t){var e,r;this.getPrevClass(t);try{for(var n=s(this.childNodes),o=n.next();!o.done;o=n.next()){o.value.setTeXclass(null)}}catch(t){e={error:t}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(e)throw e.error}}return this},e.prototype.setChildInheritedAttributes=function(t,e,r,n){(!e||r>0)&&r++,this.childNodes[0].setInheritedAttributes(t,!1,r,n),this.childNodes[1].setInheritedAttributes(t,!1,r,!0)},e.defaults=i(i({},a.AbstractMmlBaseNode.defaults),{linethickness:"medium",numalign:"center",denomalign:"center",bevelled:!1}),e}(a.AbstractMmlBaseNode);e.MmlMfrac=l},3985:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,n=arguments.length;r1&&r.match(e.operatorName)&&"normal"===this.attributes.get("mathvariant")&&void 0===this.getProperty("autoOP")&&void 0===this.getProperty("texClass")&&(this.texClass=s.TEXCLASS.OP,this.setProperty("autoOP",!0)),this},e.defaults=i({},s.AbstractMmlTokenNode.defaults),e.operatorName=/^[a-z][a-z0-9]*$/i,e.singleCharacter=/^[\uD800-\uDBFF]?.[\u0300-\u036F\u1AB0-\u1ABE\u1DC0-\u1DFF\u20D0-\u20EF]*$/,e}(s.AbstractMmlTokenNode);e.MmlMi=a},6405:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,n=arguments.length;r0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},a=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMo=void 0;var l=r(9007),u=r(4082),c=r(505),p=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e._texClass=null,e.lspace=5/18,e.rspace=5/18,e}return o(e,t),Object.defineProperty(e.prototype,"texClass",{get:function(){if(null===this._texClass){var t=this.getText(),e=s(this.handleExplicitForm(this.getForms()),3),r=e[0],n=e[1],o=e[2],i=this.constructor.OPTABLE,a=i[r][t]||i[n][t]||i[o][t];return a?a[2]:l.TEXCLASS.REL}return this._texClass},set:function(t){this._texClass=t},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"kind",{get:function(){return"mo"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isEmbellished",{get:function(){return!0},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"hasNewLine",{get:function(){return"newline"===this.attributes.get("linebreak")},enumerable:!1,configurable:!0}),e.prototype.coreParent=function(){for(var t=this,e=this,r=this.factory.getNodeClass("math");e&&e.isEmbellished&&e.coreMO()===this&&!(e instanceof r);)t=e,e=e.parent;return t},e.prototype.coreText=function(t){if(!t)return"";if(t.isEmbellished)return t.coreMO().getText();for(;((t.isKind("mrow")||t.isKind("TeXAtom")||t.isKind("mstyle")||t.isKind("mphantom"))&&1===t.childNodes.length||t.isKind("munderover"))&&t.childNodes[0];)t=t.childNodes[0];return t.isToken?t.getText():""},e.prototype.hasSpacingAttributes=function(){return this.attributes.isSet("lspace")||this.attributes.isSet("rspace")},Object.defineProperty(e.prototype,"isAccent",{get:function(){var t=!1,e=this.coreParent().parent;if(e){var r=e.isKind("mover")?e.childNodes[e.over].coreMO()?"accent":"":e.isKind("munder")?e.childNodes[e.under].coreMO()?"accentunder":"":e.isKind("munderover")?this===e.childNodes[e.over].coreMO()?"accent":this===e.childNodes[e.under].coreMO()?"accentunder":"":"";if(r)t=void 0!==e.attributes.getExplicit(r)?t:this.attributes.get("accent")}return t},enumerable:!1,configurable:!0}),e.prototype.setTeXclass=function(t){var e=this.attributes.getList("form","fence"),r=e.form,n=e.fence;return void 0===this.getProperty("texClass")&&(this.attributes.isSet("lspace")||this.attributes.isSet("rspace"))?null:(n&&this.texClass===l.TEXCLASS.REL&&("prefix"===r&&(this.texClass=l.TEXCLASS.OPEN),"postfix"===r&&(this.texClass=l.TEXCLASS.CLOSE)),"\u2061"===this.getText()?(t&&void 0===t.getProperty("texClass")&&"italic"!==t.attributes.get("mathvariant")&&(t.texClass=l.TEXCLASS.OP,t.setProperty("fnOP",!0)),this.texClass=this.prevClass=l.TEXCLASS.NONE,t):this.adjustTeXclass(t))},e.prototype.adjustTeXclass=function(t){var e=this.texClass,r=this.prevClass;if(e===l.TEXCLASS.NONE)return t;if(t?(!t.getProperty("autoOP")||e!==l.TEXCLASS.BIN&&e!==l.TEXCLASS.REL||(r=t.texClass=l.TEXCLASS.ORD),r=this.prevClass=t.texClass||l.TEXCLASS.ORD,this.prevLevel=this.attributes.getInherited("scriptlevel")):r=this.prevClass=l.TEXCLASS.NONE,e!==l.TEXCLASS.BIN||r!==l.TEXCLASS.NONE&&r!==l.TEXCLASS.BIN&&r!==l.TEXCLASS.OP&&r!==l.TEXCLASS.REL&&r!==l.TEXCLASS.OPEN&&r!==l.TEXCLASS.PUNCT)if(r!==l.TEXCLASS.BIN||e!==l.TEXCLASS.REL&&e!==l.TEXCLASS.CLOSE&&e!==l.TEXCLASS.PUNCT){if(e===l.TEXCLASS.BIN){for(var n=this,o=this.parent;o&&o.parent&&o.isEmbellished&&(1===o.childNodes.length||!o.isKind("mrow")&&o.core()===n);)n=o,o=o.parent;o.childNodes[o.childNodes.length-1]===n&&(this.texClass=l.TEXCLASS.ORD)}}else t.texClass=this.prevClass=l.TEXCLASS.ORD;else this.texClass=l.TEXCLASS.ORD;return this},e.prototype.setInheritedAttributes=function(e,r,n,o){void 0===e&&(e={}),void 0===r&&(r=!1),void 0===n&&(n=0),void 0===o&&(o=!1),t.prototype.setInheritedAttributes.call(this,e,r,n,o);var i=this.getText();this.checkOperatorTable(i),this.checkPseudoScripts(i),this.checkPrimes(i),this.checkMathAccent(i)},e.prototype.checkOperatorTable=function(t){var e,r,n=s(this.handleExplicitForm(this.getForms()),3),o=n[0],i=n[1],l=n[2];this.attributes.setInherited("form",o);var u=this.constructor.OPTABLE,c=u[o][t]||u[i][t]||u[l][t];if(c){void 0===this.getProperty("texClass")&&(this.texClass=c[2]);try{for(var p=a(Object.keys(c[3]||{})),f=p.next();!f.done;f=p.next()){var h=f.value;this.attributes.setInherited(h,c[3][h])}}catch(t){e={error:t}}finally{try{f&&!f.done&&(r=p.return)&&r.call(p)}finally{if(e)throw e.error}}this.lspace=(c[0]+1)/18,this.rspace=(c[1]+1)/18}else{var d=this.getRange(t);if(d){void 0===this.getProperty("texClass")&&(this.texClass=d[2]);var y=this.constructor.MMLSPACING[d[2]];this.lspace=(y[0]+1)/18,this.rspace=(y[1]+1)/18}}},e.prototype.getForms=function(){for(var t=this,e=this.parent,r=this.Parent;r&&r.isEmbellished;)t=e,e=r.parent,r=r.Parent;if(e&&e.isKind("mrow")&&1!==e.nonSpaceLength()){if(e.firstNonSpace()===t)return["prefix","infix","postfix"];if(e.lastNonSpace()===t)return["postfix","infix","prefix"]}return["infix","prefix","postfix"]},e.prototype.handleExplicitForm=function(t){if(this.attributes.isSet("form")){var e=this.attributes.get("form");t=[e].concat(t.filter((function(t){return t!==e})))}return t},e.prototype.getRange=function(t){var e,r;if(!t.match(/^[\uD800-\uDBFF]?.$/))return null;var n=t.codePointAt(0),o=this.constructor.RANGES;try{for(var i=a(o),s=i.next();!s.done;s=i.next()){var l=s.value;if(l[0]<=n&&n<=l[1])return l;if(n=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.MmlInferredMrow=e.MmlMrow=void 0;var a=r(9007),l=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e._core=null,e}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"mrow"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isSpacelike",{get:function(){var t,e;try{for(var r=s(this.childNodes),n=r.next();!n.done;n=r.next()){if(!n.value.isSpacelike)return!1}}catch(e){t={error:e}}finally{try{n&&!n.done&&(e=r.return)&&e.call(r)}finally{if(t)throw t.error}}return!0},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isEmbellished",{get:function(){var t,e,r=!1,n=0;try{for(var o=s(this.childNodes),i=o.next();!i.done;i=o.next()){var a=i.value;if(a)if(a.isEmbellished){if(r)return!1;r=!0,this._core=n}else if(!a.isSpacelike)return!1;n++}}catch(e){t={error:e}}finally{try{i&&!i.done&&(e=o.return)&&e.call(o)}finally{if(t)throw t.error}}return r},enumerable:!1,configurable:!0}),e.prototype.core=function(){return this.isEmbellished&&null!=this._core?this.childNodes[this._core]:this},e.prototype.coreMO=function(){return this.isEmbellished&&null!=this._core?this.childNodes[this._core].coreMO():this},e.prototype.nonSpaceLength=function(){var t,e,r=0;try{for(var n=s(this.childNodes),o=n.next();!o.done;o=n.next()){var i=o.value;i&&!i.isSpacelike&&r++}}catch(e){t={error:e}}finally{try{o&&!o.done&&(e=n.return)&&e.call(n)}finally{if(t)throw t.error}}return r},e.prototype.firstNonSpace=function(){var t,e;try{for(var r=s(this.childNodes),n=r.next();!n.done;n=r.next()){var o=n.value;if(o&&!o.isSpacelike)return o}}catch(e){t={error:e}}finally{try{n&&!n.done&&(e=r.return)&&e.call(r)}finally{if(t)throw t.error}}return null},e.prototype.lastNonSpace=function(){for(var t=this.childNodes.length;--t>=0;){var e=this.childNodes[t];if(e&&!e.isSpacelike)return e}return null},e.prototype.setTeXclass=function(t){var e,r,n,o;if(null!=this.getProperty("open")||null!=this.getProperty("close")){this.getPrevClass(t),t=null;try{for(var i=s(this.childNodes),l=i.next();!l.done;l=i.next()){t=l.value.setTeXclass(t)}}catch(t){e={error:t}}finally{try{l&&!l.done&&(r=i.return)&&r.call(i)}finally{if(e)throw e.error}}null==this.texClass&&(this.texClass=a.TEXCLASS.INNER)}else{try{for(var u=s(this.childNodes),c=u.next();!c.done;c=u.next()){t=c.value.setTeXclass(t)}}catch(t){n={error:t}}finally{try{c&&!c.done&&(o=u.return)&&o.call(u)}finally{if(n)throw n.error}}this.childNodes[0]&&this.updateTeXclass(this.childNodes[0])}return t},e.defaults=i({},a.AbstractMmlNode.defaults),e}(a.AbstractMmlNode);e.MmlMrow=l;var u=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"inferredMrow"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isInferred",{get:function(){return!0},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"notParent",{get:function(){return!0},enumerable:!1,configurable:!0}),e.prototype.toString=function(){return"["+this.childNodes.join(",")+"]"},e.defaults=l.defaults,e}(l);e.MmlInferredMrow=u},7265:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,n=arguments.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMtable=void 0;var a=r(9007),l=r(505),u=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.properties={useHeight:!0},e.texclass=a.TEXCLASS.ORD,e}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"mtable"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"linebreakContainer",{get:function(){return!0},enumerable:!1,configurable:!0}),e.prototype.setInheritedAttributes=function(e,r,n,o){var i,l;try{for(var u=s(a.indentAttributes),c=u.next();!c.done;c=u.next()){var p=c.value;e[p]&&this.attributes.setInherited(p,e[p][1]),void 0!==this.attributes.getExplicit(p)&&delete this.attributes.getAllAttributes()[p]}}catch(t){i={error:t}}finally{try{c&&!c.done&&(l=u.return)&&l.call(u)}finally{if(i)throw i.error}}t.prototype.setInheritedAttributes.call(this,e,r,n,o)},e.prototype.setChildInheritedAttributes=function(t,e,r,n){var o,i,a,u;try{for(var c=s(this.childNodes),p=c.next();!p.done;p=c.next()){(y=p.value).isKind("mtr")||this.replaceChild(this.factory.create("mtr"),y).appendChild(y)}}catch(t){o={error:t}}finally{try{p&&!p.done&&(i=c.return)&&i.call(c)}finally{if(o)throw o.error}}r=this.getProperty("scriptlevel")||r,e=!(!this.attributes.getExplicit("displaystyle")&&!this.attributes.getDefault("displaystyle")),t=this.addInheritedAttributes(t,{columnalign:this.attributes.get("columnalign"),rowalign:"center"});var f=l.split(this.attributes.get("rowalign"));try{for(var h=s(this.childNodes),d=h.next();!d.done;d=h.next()){var y=d.value;t.rowalign[1]=f.shift()||t.rowalign[1],y.setInheritedAttributes(t,e,r,n)}}catch(t){a={error:t}}finally{try{d&&!d.done&&(u=h.return)&&u.call(h)}finally{if(a)throw a.error}}},e.prototype.verifyChildren=function(e){var r,n;if(!e.fixMtables)try{for(var o=s(this.childNodes),i=o.next();!i.done;i=o.next()){i.value.isKind("mtr")||this.mError("Children of "+this.kind+" must be mtr or mlabeledtr",e)}}catch(t){r={error:t}}finally{try{i&&!i.done&&(n=o.return)&&n.call(o)}finally{if(r)throw r.error}}t.prototype.verifyChildren.call(this,e)},e.prototype.setTeXclass=function(t){var e,r;this.getPrevClass(t);try{for(var n=s(this.childNodes),o=n.next();!o.done;o=n.next()){o.value.setTeXclass(null)}}catch(t){e={error:t}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(e)throw e.error}}return this},e.defaults=i(i({},a.AbstractMmlNode.defaults),{align:"axis",rowalign:"baseline",columnalign:"center",groupalign:"{left}",alignmentscope:!0,columnwidth:"auto",width:"auto",rowspacing:"1ex",columnspacing:".8em",rowlines:"none",columnlines:"none",frame:"none",framespacing:"0.4em 0.5ex",equalrows:!1,equalcolumns:!1,displaystyle:!1,side:"right",minlabelspacing:"0.8em"}),e}(a.AbstractMmlNode);e.MmlMtable=u},4359:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,n=arguments.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMlabeledtr=e.MmlMtr=void 0;var a=r(9007),l=r(91),u=r(505),c=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"mtr"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"linebreakContainer",{get:function(){return!0},enumerable:!1,configurable:!0}),e.prototype.setChildInheritedAttributes=function(t,e,r,n){var o,i,a,l;try{for(var c=s(this.childNodes),p=c.next();!p.done;p=c.next()){(y=p.value).isKind("mtd")||this.replaceChild(this.factory.create("mtd"),y).appendChild(y)}}catch(t){o={error:t}}finally{try{p&&!p.done&&(i=c.return)&&i.call(c)}finally{if(o)throw o.error}}var f=u.split(this.attributes.get("columnalign"));1===this.arity&&f.unshift(this.parent.attributes.get("side")),t=this.addInheritedAttributes(t,{rowalign:this.attributes.get("rowalign"),columnalign:"center"});try{for(var h=s(this.childNodes),d=h.next();!d.done;d=h.next()){var y=d.value;t.columnalign[1]=f.shift()||t.columnalign[1],y.setInheritedAttributes(t,e,r,n)}}catch(t){a={error:t}}finally{try{d&&!d.done&&(l=h.return)&&l.call(h)}finally{if(a)throw a.error}}},e.prototype.verifyChildren=function(e){var r,n;if(!this.parent||this.parent.isKind("mtable")){if(!e.fixMtables)try{for(var o=s(this.childNodes),i=o.next();!i.done;i=o.next()){var a=i.value;if(!a.isKind("mtd"))this.replaceChild(this.factory.create("mtr"),a).mError("Children of "+this.kind+" must be mtd",e,!0)}}catch(t){r={error:t}}finally{try{i&&!i.done&&(n=o.return)&&n.call(o)}finally{if(r)throw r.error}}t.prototype.verifyChildren.call(this,e)}else this.mError(this.kind+" can only be a child of an mtable",e,!0)},e.prototype.setTeXclass=function(t){var e,r;this.getPrevClass(t);try{for(var n=s(this.childNodes),o=n.next();!o.done;o=n.next()){o.value.setTeXclass(null)}}catch(t){e={error:t}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(e)throw e.error}}return this},e.defaults=i(i({},a.AbstractMmlNode.defaults),{rowalign:l.INHERIT,columnalign:l.INHERIT,groupalign:l.INHERIT}),e}(a.AbstractMmlNode);e.MmlMtr=c;var p=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"mlabeledtr"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"arity",{get:function(){return 1},enumerable:!1,configurable:!0}),e}(c);e.MmlMlabeledtr=p},5184:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,n=arguments.length;r":e.MO.BIN5,".":[0,3,n.TEXCLASS.PUNCT,{separator:!0}],"/":e.MO.ORD11,"//":o(1,1),"/=":e.MO.BIN4,":":[1,2,n.TEXCLASS.REL,null],":=":e.MO.BIN4,";":[0,3,n.TEXCLASS.PUNCT,{linebreakstyle:"after",separator:!0}],"<":e.MO.REL,"<=":e.MO.BIN5,"<>":o(1,1),"=":e.MO.REL,"==":e.MO.BIN4,">":e.MO.REL,">=":e.MO.BIN5,"?":[1,1,n.TEXCLASS.CLOSE,null],"@":e.MO.ORD11,"\\":e.MO.ORD,"^":e.MO.ORD11,_:e.MO.ORD11,"|":[2,2,n.TEXCLASS.ORD,{fence:!0,stretchy:!0,symmetric:!0}],"||":[2,2,n.TEXCLASS.BIN,{fence:!0,stretchy:!0,symmetric:!0}],"|||":[2,2,n.TEXCLASS.ORD,{fence:!0,stretchy:!0,symmetric:!0}],"\xb1":e.MO.BIN4,"\xb7":e.MO.BIN4,"\xd7":e.MO.BIN4,"\xf7":e.MO.BIN4,"\u02b9":e.MO.ORD,"\u0300":e.MO.ACCENT,"\u0301":e.MO.ACCENT,"\u0303":e.MO.WIDEACCENT,"\u0304":e.MO.ACCENT,"\u0306":e.MO.ACCENT,"\u0307":e.MO.ACCENT,"\u0308":e.MO.ACCENT,"\u030c":e.MO.ACCENT,"\u0332":e.MO.WIDEACCENT,"\u0338":e.MO.REL4,"\u2015":[0,0,n.TEXCLASS.ORD,{stretchy:!0}],"\u2017":[0,0,n.TEXCLASS.ORD,{stretchy:!0}],"\u2020":e.MO.BIN3,"\u2021":e.MO.BIN3,"\u2022":e.MO.BIN4,"\u2026":e.MO.INNER,"\u2043":e.MO.BIN4,"\u2044":e.MO.TALLBIN,"\u2061":e.MO.ORD,"\u2062":e.MO.ORD,"\u2063":[0,0,n.TEXCLASS.ORD,{linebreakstyle:"after",separator:!0}],"\u2064":e.MO.ORD,"\u20d7":e.MO.ACCENT,"\u2111":e.MO.ORD,"\u2113":e.MO.ORD,"\u2118":e.MO.ORD,"\u211c":e.MO.ORD,"\u2190":e.MO.WIDEREL,"\u2191":e.MO.RELSTRETCH,"\u2192":e.MO.WIDEREL,"\u2193":e.MO.RELSTRETCH,"\u2194":e.MO.WIDEREL,"\u2195":e.MO.RELSTRETCH,"\u2196":e.MO.RELSTRETCH,"\u2197":e.MO.RELSTRETCH,"\u2198":e.MO.RELSTRETCH,"\u2199":e.MO.RELSTRETCH,"\u219a":e.MO.RELACCENT,"\u219b":e.MO.RELACCENT,"\u219c":e.MO.WIDEREL,"\u219d":e.MO.WIDEREL,"\u219e":e.MO.WIDEREL,"\u219f":e.MO.WIDEREL,"\u21a0":e.MO.WIDEREL,"\u21a1":e.MO.RELSTRETCH,"\u21a2":e.MO.WIDEREL,"\u21a3":e.MO.WIDEREL,"\u21a4":e.MO.WIDEREL,"\u21a5":e.MO.RELSTRETCH,"\u21a6":e.MO.WIDEREL,"\u21a7":e.MO.RELSTRETCH,"\u21a8":e.MO.RELSTRETCH,"\u21a9":e.MO.WIDEREL,"\u21aa":e.MO.WIDEREL,"\u21ab":e.MO.WIDEREL,"\u21ac":e.MO.WIDEREL,"\u21ad":e.MO.WIDEREL,"\u21ae":e.MO.RELACCENT,"\u21af":e.MO.RELSTRETCH,"\u21b0":e.MO.RELSTRETCH,"\u21b1":e.MO.RELSTRETCH,"\u21b2":e.MO.RELSTRETCH,"\u21b3":e.MO.RELSTRETCH,"\u21b4":e.MO.RELSTRETCH,"\u21b5":e.MO.RELSTRETCH,"\u21b6":e.MO.RELACCENT,"\u21b7":e.MO.RELACCENT,"\u21b8":e.MO.REL,"\u21b9":e.MO.WIDEREL,"\u21ba":e.MO.REL,"\u21bb":e.MO.REL,"\u21bc":e.MO.WIDEREL,"\u21bd":e.MO.WIDEREL,"\u21be":e.MO.RELSTRETCH,"\u21bf":e.MO.RELSTRETCH,"\u21c0":e.MO.WIDEREL,"\u21c1":e.MO.WIDEREL,"\u21c2":e.MO.RELSTRETCH,"\u21c3":e.MO.RELSTRETCH,"\u21c4":e.MO.WIDEREL,"\u21c5":e.MO.RELSTRETCH,"\u21c6":e.MO.WIDEREL,"\u21c7":e.MO.WIDEREL,"\u21c8":e.MO.RELSTRETCH,"\u21c9":e.MO.WIDEREL,"\u21ca":e.MO.RELSTRETCH,"\u21cb":e.MO.WIDEREL,"\u21cc":e.MO.WIDEREL,"\u21cd":e.MO.RELACCENT,"\u21ce":e.MO.RELACCENT,"\u21cf":e.MO.RELACCENT,"\u21d0":e.MO.WIDEREL,"\u21d1":e.MO.RELSTRETCH,"\u21d2":e.MO.WIDEREL,"\u21d3":e.MO.RELSTRETCH,"\u21d4":e.MO.WIDEREL,"\u21d5":e.MO.RELSTRETCH,"\u21d6":e.MO.RELSTRETCH,"\u21d7":e.MO.RELSTRETCH,"\u21d8":e.MO.RELSTRETCH,"\u21d9":e.MO.RELSTRETCH,"\u21da":e.MO.WIDEREL,"\u21db":e.MO.WIDEREL,"\u21dc":e.MO.WIDEREL,"\u21dd":e.MO.WIDEREL,"\u21de":e.MO.REL,"\u21df":e.MO.REL,"\u21e0":e.MO.WIDEREL,"\u21e1":e.MO.RELSTRETCH,"\u21e2":e.MO.WIDEREL,"\u21e3":e.MO.RELSTRETCH,"\u21e4":e.MO.WIDEREL,"\u21e5":e.MO.WIDEREL,"\u21e6":e.MO.WIDEREL,"\u21e7":e.MO.RELSTRETCH,"\u21e8":e.MO.WIDEREL,"\u21e9":e.MO.RELSTRETCH,"\u21ea":e.MO.RELSTRETCH,"\u21eb":e.MO.RELSTRETCH,"\u21ec":e.MO.RELSTRETCH,"\u21ed":e.MO.RELSTRETCH,"\u21ee":e.MO.RELSTRETCH,"\u21ef":e.MO.RELSTRETCH,"\u21f0":e.MO.WIDEREL,"\u21f1":e.MO.REL,"\u21f2":e.MO.REL,"\u21f3":e.MO.RELSTRETCH,"\u21f4":e.MO.RELACCENT,"\u21f5":e.MO.RELSTRETCH,"\u21f6":e.MO.WIDEREL,"\u21f7":e.MO.RELACCENT,"\u21f8":e.MO.RELACCENT,"\u21f9":e.MO.RELACCENT,"\u21fa":e.MO.RELACCENT,"\u21fb":e.MO.RELACCENT,"\u21fc":e.MO.RELACCENT,"\u21fd":e.MO.WIDEREL,"\u21fe":e.MO.WIDEREL,"\u21ff":e.MO.WIDEREL,"\u2201":o(1,2,n.TEXCLASS.ORD),"\u2205":e.MO.ORD,"\u2206":e.MO.BIN3,"\u2208":e.MO.REL,"\u2209":e.MO.REL,"\u220a":e.MO.REL,"\u220b":e.MO.REL,"\u220c":e.MO.REL,"\u220d":e.MO.REL,"\u220e":e.MO.BIN3,"\u2212":e.MO.BIN4,"\u2213":e.MO.BIN4,"\u2214":e.MO.BIN4,"\u2215":e.MO.TALLBIN,"\u2216":e.MO.BIN4,"\u2217":e.MO.BIN4,"\u2218":e.MO.BIN4,"\u2219":e.MO.BIN4,"\u221d":e.MO.REL,"\u221e":e.MO.ORD,"\u221f":e.MO.REL,"\u2223":e.MO.REL,"\u2224":e.MO.REL,"\u2225":e.MO.REL,"\u2226":e.MO.REL,"\u2227":e.MO.BIN4,"\u2228":e.MO.BIN4,"\u2229":e.MO.BIN4,"\u222a":e.MO.BIN4,"\u2234":e.MO.REL,"\u2235":e.MO.REL,"\u2236":e.MO.REL,"\u2237":e.MO.REL,"\u2238":e.MO.BIN4,"\u2239":e.MO.REL,"\u223a":e.MO.BIN4,"\u223b":e.MO.REL,"\u223c":e.MO.REL,"\u223d":e.MO.REL,"\u223d\u0331":e.MO.BIN3,"\u223e":e.MO.REL,"\u223f":e.MO.BIN3,"\u2240":e.MO.BIN4,"\u2241":e.MO.REL,"\u2242":e.MO.REL,"\u2242\u0338":e.MO.REL,"\u2243":e.MO.REL,"\u2244":e.MO.REL,"\u2245":e.MO.REL,"\u2246":e.MO.REL,"\u2247":e.MO.REL,"\u2248":e.MO.REL,"\u2249":e.MO.REL,"\u224a":e.MO.REL,"\u224b":e.MO.REL,"\u224c":e.MO.REL,"\u224d":e.MO.REL,"\u224e":e.MO.REL,"\u224e\u0338":e.MO.REL,"\u224f":e.MO.REL,"\u224f\u0338":e.MO.REL,"\u2250":e.MO.REL,"\u2251":e.MO.REL,"\u2252":e.MO.REL,"\u2253":e.MO.REL,"\u2254":e.MO.REL,"\u2255":e.MO.REL,"\u2256":e.MO.REL,"\u2257":e.MO.REL,"\u2258":e.MO.REL,"\u2259":e.MO.REL,"\u225a":e.MO.REL,"\u225b":e.MO.REL,"\u225c":e.MO.REL,"\u225d":e.MO.REL,"\u225e":e.MO.REL,"\u225f":e.MO.REL,"\u2260":e.MO.REL,"\u2261":e.MO.REL,"\u2262":e.MO.REL,"\u2263":e.MO.REL,"\u2264":e.MO.REL,"\u2265":e.MO.REL,"\u2266":e.MO.REL,"\u2266\u0338":e.MO.REL,"\u2267":e.MO.REL,"\u2268":e.MO.REL,"\u2269":e.MO.REL,"\u226a":e.MO.REL,"\u226a\u0338":e.MO.REL,"\u226b":e.MO.REL,"\u226b\u0338":e.MO.REL,"\u226c":e.MO.REL,"\u226d":e.MO.REL,"\u226e":e.MO.REL,"\u226f":e.MO.REL,"\u2270":e.MO.REL,"\u2271":e.MO.REL,"\u2272":e.MO.REL,"\u2273":e.MO.REL,"\u2274":e.MO.REL,"\u2275":e.MO.REL,"\u2276":e.MO.REL,"\u2277":e.MO.REL,"\u2278":e.MO.REL,"\u2279":e.MO.REL,"\u227a":e.MO.REL,"\u227b":e.MO.REL,"\u227c":e.MO.REL,"\u227d":e.MO.REL,"\u227e":e.MO.REL,"\u227f":e.MO.REL,"\u227f\u0338":e.MO.REL,"\u2280":e.MO.REL,"\u2281":e.MO.REL,"\u2282":e.MO.REL,"\u2282\u20d2":e.MO.REL,"\u2283":e.MO.REL,"\u2283\u20d2":e.MO.REL,"\u2284":e.MO.REL,"\u2285":e.MO.REL,"\u2286":e.MO.REL,"\u2287":e.MO.REL,"\u2288":e.MO.REL,"\u2289":e.MO.REL,"\u228a":e.MO.REL,"\u228b":e.MO.REL,"\u228c":e.MO.BIN4,"\u228d":e.MO.BIN4,"\u228e":e.MO.BIN4,"\u228f":e.MO.REL,"\u228f\u0338":e.MO.REL,"\u2290":e.MO.REL,"\u2290\u0338":e.MO.REL,"\u2291":e.MO.REL,"\u2292":e.MO.REL,"\u2293":e.MO.BIN4,"\u2294":e.MO.BIN4,"\u2295":e.MO.BIN4,"\u2296":e.MO.BIN4,"\u2297":e.MO.BIN4,"\u2298":e.MO.BIN4,"\u2299":e.MO.BIN4,"\u229a":e.MO.BIN4,"\u229b":e.MO.BIN4,"\u229c":e.MO.BIN4,"\u229d":e.MO.BIN4,"\u229e":e.MO.BIN4,"\u229f":e.MO.BIN4,"\u22a0":e.MO.BIN4,"\u22a1":e.MO.BIN4,"\u22a2":e.MO.REL,"\u22a3":e.MO.REL,"\u22a4":e.MO.ORD55,"\u22a5":e.MO.REL,"\u22a6":e.MO.REL,"\u22a7":e.MO.REL,"\u22a8":e.MO.REL,"\u22a9":e.MO.REL,"\u22aa":e.MO.REL,"\u22ab":e.MO.REL,"\u22ac":e.MO.REL,"\u22ad":e.MO.REL,"\u22ae":e.MO.REL,"\u22af":e.MO.REL,"\u22b0":e.MO.REL,"\u22b1":e.MO.REL,"\u22b2":e.MO.REL,"\u22b3":e.MO.REL,"\u22b4":e.MO.REL,"\u22b5":e.MO.REL,"\u22b6":e.MO.REL,"\u22b7":e.MO.REL,"\u22b8":e.MO.REL,"\u22b9":e.MO.REL,"\u22ba":e.MO.BIN4,"\u22bb":e.MO.BIN4,"\u22bc":e.MO.BIN4,"\u22bd":e.MO.BIN4,"\u22be":e.MO.BIN3,"\u22bf":e.MO.BIN3,"\u22c4":e.MO.BIN4,"\u22c5":e.MO.BIN4,"\u22c6":e.MO.BIN4,"\u22c7":e.MO.BIN4,"\u22c8":e.MO.REL,"\u22c9":e.MO.BIN4,"\u22ca":e.MO.BIN4,"\u22cb":e.MO.BIN4,"\u22cc":e.MO.BIN4,"\u22cd":e.MO.REL,"\u22ce":e.MO.BIN4,"\u22cf":e.MO.BIN4,"\u22d0":e.MO.REL,"\u22d1":e.MO.REL,"\u22d2":e.MO.BIN4,"\u22d3":e.MO.BIN4,"\u22d4":e.MO.REL,"\u22d5":e.MO.REL,"\u22d6":e.MO.REL,"\u22d7":e.MO.REL,"\u22d8":e.MO.REL,"\u22d9":e.MO.REL,"\u22da":e.MO.REL,"\u22db":e.MO.REL,"\u22dc":e.MO.REL,"\u22dd":e.MO.REL,"\u22de":e.MO.REL,"\u22df":e.MO.REL,"\u22e0":e.MO.REL,"\u22e1":e.MO.REL,"\u22e2":e.MO.REL,"\u22e3":e.MO.REL,"\u22e4":e.MO.REL,"\u22e5":e.MO.REL,"\u22e6":e.MO.REL,"\u22e7":e.MO.REL,"\u22e8":e.MO.REL,"\u22e9":e.MO.REL,"\u22ea":e.MO.REL,"\u22eb":e.MO.REL,"\u22ec":e.MO.REL,"\u22ed":e.MO.REL,"\u22ee":e.MO.ORD55,"\u22ef":e.MO.INNER,"\u22f0":e.MO.REL,"\u22f1":[5,5,n.TEXCLASS.INNER,null],"\u22f2":e.MO.REL,"\u22f3":e.MO.REL,"\u22f4":e.MO.REL,"\u22f5":e.MO.REL,"\u22f6":e.MO.REL,"\u22f7":e.MO.REL,"\u22f8":e.MO.REL,"\u22f9":e.MO.REL,"\u22fa":e.MO.REL,"\u22fb":e.MO.REL,"\u22fc":e.MO.REL,"\u22fd":e.MO.REL,"\u22fe":e.MO.REL,"\u22ff":e.MO.REL,"\u2305":e.MO.BIN3,"\u2306":e.MO.BIN3,"\u2322":e.MO.REL4,"\u2323":e.MO.REL4,"\u2329":e.MO.OPEN,"\u232a":e.MO.CLOSE,"\u23aa":e.MO.ORD,"\u23af":[0,0,n.TEXCLASS.ORD,{stretchy:!0}],"\u23b0":e.MO.OPEN,"\u23b1":e.MO.CLOSE,"\u2500":e.MO.ORD,"\u25b3":e.MO.BIN4,"\u25b5":e.MO.BIN4,"\u25b9":e.MO.BIN4,"\u25bd":e.MO.BIN4,"\u25bf":e.MO.BIN4,"\u25c3":e.MO.BIN4,"\u25ef":e.MO.BIN3,"\u2660":e.MO.ORD,"\u2661":e.MO.ORD,"\u2662":e.MO.ORD,"\u2663":e.MO.ORD,"\u2758":e.MO.REL,"\u27f0":e.MO.RELSTRETCH,"\u27f1":e.MO.RELSTRETCH,"\u27f5":e.MO.WIDEREL,"\u27f6":e.MO.WIDEREL,"\u27f7":e.MO.WIDEREL,"\u27f8":e.MO.WIDEREL,"\u27f9":e.MO.WIDEREL,"\u27fa":e.MO.WIDEREL,"\u27fb":e.MO.WIDEREL,"\u27fc":e.MO.WIDEREL,"\u27fd":e.MO.WIDEREL,"\u27fe":e.MO.WIDEREL,"\u27ff":e.MO.WIDEREL,"\u2900":e.MO.RELACCENT,"\u2901":e.MO.RELACCENT,"\u2902":e.MO.RELACCENT,"\u2903":e.MO.RELACCENT,"\u2904":e.MO.RELACCENT,"\u2905":e.MO.RELACCENT,"\u2906":e.MO.RELACCENT,"\u2907":e.MO.RELACCENT,"\u2908":e.MO.REL,"\u2909":e.MO.REL,"\u290a":e.MO.RELSTRETCH,"\u290b":e.MO.RELSTRETCH,"\u290c":e.MO.WIDEREL,"\u290d":e.MO.WIDEREL,"\u290e":e.MO.WIDEREL,"\u290f":e.MO.WIDEREL,"\u2910":e.MO.WIDEREL,"\u2911":e.MO.RELACCENT,"\u2912":e.MO.RELSTRETCH,"\u2913":e.MO.RELSTRETCH,"\u2914":e.MO.RELACCENT,"\u2915":e.MO.RELACCENT,"\u2916":e.MO.RELACCENT,"\u2917":e.MO.RELACCENT,"\u2918":e.MO.RELACCENT,"\u2919":e.MO.RELACCENT,"\u291a":e.MO.RELACCENT,"\u291b":e.MO.RELACCENT,"\u291c":e.MO.RELACCENT,"\u291d":e.MO.RELACCENT,"\u291e":e.MO.RELACCENT,"\u291f":e.MO.RELACCENT,"\u2920":e.MO.RELACCENT,"\u2921":e.MO.RELSTRETCH,"\u2922":e.MO.RELSTRETCH,"\u2923":e.MO.REL,"\u2924":e.MO.REL,"\u2925":e.MO.REL,"\u2926":e.MO.REL,"\u2927":e.MO.REL,"\u2928":e.MO.REL,"\u2929":e.MO.REL,"\u292a":e.MO.REL,"\u292b":e.MO.REL,"\u292c":e.MO.REL,"\u292d":e.MO.REL,"\u292e":e.MO.REL,"\u292f":e.MO.REL,"\u2930":e.MO.REL,"\u2931":e.MO.REL,"\u2932":e.MO.REL,"\u2933":e.MO.RELACCENT,"\u2934":e.MO.REL,"\u2935":e.MO.REL,"\u2936":e.MO.REL,"\u2937":e.MO.REL,"\u2938":e.MO.REL,"\u2939":e.MO.REL,"\u293a":e.MO.RELACCENT,"\u293b":e.MO.RELACCENT,"\u293c":e.MO.RELACCENT,"\u293d":e.MO.RELACCENT,"\u293e":e.MO.REL,"\u293f":e.MO.REL,"\u2940":e.MO.REL,"\u2941":e.MO.REL,"\u2942":e.MO.RELACCENT,"\u2943":e.MO.RELACCENT,"\u2944":e.MO.RELACCENT,"\u2945":e.MO.RELACCENT,"\u2946":e.MO.RELACCENT,"\u2947":e.MO.RELACCENT,"\u2948":e.MO.RELACCENT,"\u2949":e.MO.REL,"\u294a":e.MO.RELACCENT,"\u294b":e.MO.RELACCENT,"\u294c":e.MO.REL,"\u294d":e.MO.REL,"\u294e":e.MO.WIDEREL,"\u294f":e.MO.RELSTRETCH,"\u2950":e.MO.WIDEREL,"\u2951":e.MO.RELSTRETCH,"\u2952":e.MO.WIDEREL,"\u2953":e.MO.WIDEREL,"\u2954":e.MO.RELSTRETCH,"\u2955":e.MO.RELSTRETCH,"\u2956":e.MO.RELSTRETCH,"\u2957":e.MO.RELSTRETCH,"\u2958":e.MO.RELSTRETCH,"\u2959":e.MO.RELSTRETCH,"\u295a":e.MO.WIDEREL,"\u295b":e.MO.WIDEREL,"\u295c":e.MO.RELSTRETCH,"\u295d":e.MO.RELSTRETCH,"\u295e":e.MO.WIDEREL,"\u295f":e.MO.WIDEREL,"\u2960":e.MO.RELSTRETCH,"\u2961":e.MO.RELSTRETCH,"\u2962":e.MO.RELACCENT,"\u2963":e.MO.REL,"\u2964":e.MO.RELACCENT,"\u2965":e.MO.REL,"\u2966":e.MO.RELACCENT,"\u2967":e.MO.RELACCENT,"\u2968":e.MO.RELACCENT,"\u2969":e.MO.RELACCENT,"\u296a":e.MO.RELACCENT,"\u296b":e.MO.RELACCENT,"\u296c":e.MO.RELACCENT,"\u296d":e.MO.RELACCENT,"\u296e":e.MO.RELSTRETCH,"\u296f":e.MO.RELSTRETCH,"\u2970":e.MO.RELACCENT,"\u2971":e.MO.RELACCENT,"\u2972":e.MO.RELACCENT,"\u2973":e.MO.RELACCENT,"\u2974":e.MO.RELACCENT,"\u2975":e.MO.RELACCENT,"\u2976":e.MO.RELACCENT,"\u2977":e.MO.RELACCENT,"\u2978":e.MO.RELACCENT,"\u2979":e.MO.RELACCENT,"\u297a":e.MO.RELACCENT,"\u297b":e.MO.RELACCENT,"\u297c":e.MO.RELACCENT,"\u297d":e.MO.RELACCENT,"\u297e":e.MO.REL,"\u297f":e.MO.REL,"\u2981":e.MO.BIN3,"\u2982":e.MO.BIN3,"\u2999":e.MO.BIN3,"\u299a":e.MO.BIN3,"\u299b":e.MO.BIN3,"\u299c":e.MO.BIN3,"\u299d":e.MO.BIN3,"\u299e":e.MO.BIN3,"\u299f":e.MO.BIN3,"\u29a0":e.MO.BIN3,"\u29a1":e.MO.BIN3,"\u29a2":e.MO.BIN3,"\u29a3":e.MO.BIN3,"\u29a4":e.MO.BIN3,"\u29a5":e.MO.BIN3,"\u29a6":e.MO.BIN3,"\u29a7":e.MO.BIN3,"\u29a8":e.MO.BIN3,"\u29a9":e.MO.BIN3,"\u29aa":e.MO.BIN3,"\u29ab":e.MO.BIN3,"\u29ac":e.MO.BIN3,"\u29ad":e.MO.BIN3,"\u29ae":e.MO.BIN3,"\u29af":e.MO.BIN3,"\u29b0":e.MO.BIN3,"\u29b1":e.MO.BIN3,"\u29b2":e.MO.BIN3,"\u29b3":e.MO.BIN3,"\u29b4":e.MO.BIN3,"\u29b5":e.MO.BIN3,"\u29b6":e.MO.BIN4,"\u29b7":e.MO.BIN4,"\u29b8":e.MO.BIN4,"\u29b9":e.MO.BIN4,"\u29ba":e.MO.BIN4,"\u29bb":e.MO.BIN4,"\u29bc":e.MO.BIN4,"\u29bd":e.MO.BIN4,"\u29be":e.MO.BIN4,"\u29bf":e.MO.BIN4,"\u29c0":e.MO.REL,"\u29c1":e.MO.REL,"\u29c2":e.MO.BIN3,"\u29c3":e.MO.BIN3,"\u29c4":e.MO.BIN4,"\u29c5":e.MO.BIN4,"\u29c6":e.MO.BIN4,"\u29c7":e.MO.BIN4,"\u29c8":e.MO.BIN4,"\u29c9":e.MO.BIN3,"\u29ca":e.MO.BIN3,"\u29cb":e.MO.BIN3,"\u29cc":e.MO.BIN3,"\u29cd":e.MO.BIN3,"\u29ce":e.MO.REL,"\u29cf":e.MO.REL,"\u29cf\u0338":e.MO.REL,"\u29d0":e.MO.REL,"\u29d0\u0338":e.MO.REL,"\u29d1":e.MO.REL,"\u29d2":e.MO.REL,"\u29d3":e.MO.REL,"\u29d4":e.MO.REL,"\u29d5":e.MO.REL,"\u29d6":e.MO.BIN4,"\u29d7":e.MO.BIN4,"\u29d8":e.MO.BIN3,"\u29d9":e.MO.BIN3,"\u29db":e.MO.BIN3,"\u29dc":e.MO.BIN3,"\u29dd":e.MO.BIN3,"\u29de":e.MO.REL,"\u29df":e.MO.BIN3,"\u29e0":e.MO.BIN3,"\u29e1":e.MO.REL,"\u29e2":e.MO.BIN4,"\u29e3":e.MO.REL,"\u29e4":e.MO.REL,"\u29e5":e.MO.REL,"\u29e6":e.MO.REL,"\u29e7":e.MO.BIN3,"\u29e8":e.MO.BIN3,"\u29e9":e.MO.BIN3,"\u29ea":e.MO.BIN3,"\u29eb":e.MO.BIN3,"\u29ec":e.MO.BIN3,"\u29ed":e.MO.BIN3,"\u29ee":e.MO.BIN3,"\u29ef":e.MO.BIN3,"\u29f0":e.MO.BIN3,"\u29f1":e.MO.BIN3,"\u29f2":e.MO.BIN3,"\u29f3":e.MO.BIN3,"\u29f4":e.MO.REL,"\u29f5":e.MO.BIN4,"\u29f6":e.MO.BIN4,"\u29f7":e.MO.BIN4,"\u29f8":e.MO.BIN3,"\u29f9":e.MO.BIN3,"\u29fa":e.MO.BIN3,"\u29fb":e.MO.BIN3,"\u29fe":e.MO.BIN4,"\u29ff":e.MO.BIN4,"\u2a1d":e.MO.BIN3,"\u2a1e":e.MO.BIN3,"\u2a1f":e.MO.BIN3,"\u2a20":e.MO.BIN3,"\u2a21":e.MO.BIN3,"\u2a22":e.MO.BIN4,"\u2a23":e.MO.BIN4,"\u2a24":e.MO.BIN4,"\u2a25":e.MO.BIN4,"\u2a26":e.MO.BIN4,"\u2a27":e.MO.BIN4,"\u2a28":e.MO.BIN4,"\u2a29":e.MO.BIN4,"\u2a2a":e.MO.BIN4,"\u2a2b":e.MO.BIN4,"\u2a2c":e.MO.BIN4,"\u2a2d":e.MO.BIN4,"\u2a2e":e.MO.BIN4,"\u2a2f":e.MO.BIN4,"\u2a30":e.MO.BIN4,"\u2a31":e.MO.BIN4,"\u2a32":e.MO.BIN4,"\u2a33":e.MO.BIN4,"\u2a34":e.MO.BIN4,"\u2a35":e.MO.BIN4,"\u2a36":e.MO.BIN4,"\u2a37":e.MO.BIN4,"\u2a38":e.MO.BIN4,"\u2a39":e.MO.BIN4,"\u2a3a":e.MO.BIN4,"\u2a3b":e.MO.BIN4,"\u2a3c":e.MO.BIN4,"\u2a3d":e.MO.BIN4,"\u2a3e":e.MO.BIN4,"\u2a3f":e.MO.BIN4,"\u2a40":e.MO.BIN4,"\u2a41":e.MO.BIN4,"\u2a42":e.MO.BIN4,"\u2a43":e.MO.BIN4,"\u2a44":e.MO.BIN4,"\u2a45":e.MO.BIN4,"\u2a46":e.MO.BIN4,"\u2a47":e.MO.BIN4,"\u2a48":e.MO.BIN4,"\u2a49":e.MO.BIN4,"\u2a4a":e.MO.BIN4,"\u2a4b":e.MO.BIN4,"\u2a4c":e.MO.BIN4,"\u2a4d":e.MO.BIN4,"\u2a4e":e.MO.BIN4,"\u2a4f":e.MO.BIN4,"\u2a50":e.MO.BIN4,"\u2a51":e.MO.BIN4,"\u2a52":e.MO.BIN4,"\u2a53":e.MO.BIN4,"\u2a54":e.MO.BIN4,"\u2a55":e.MO.BIN4,"\u2a56":e.MO.BIN4,"\u2a57":e.MO.BIN4,"\u2a58":e.MO.BIN4,"\u2a59":e.MO.REL,"\u2a5a":e.MO.BIN4,"\u2a5b":e.MO.BIN4,"\u2a5c":e.MO.BIN4,"\u2a5d":e.MO.BIN4,"\u2a5e":e.MO.BIN4,"\u2a5f":e.MO.BIN4,"\u2a60":e.MO.BIN4,"\u2a61":e.MO.BIN4,"\u2a62":e.MO.BIN4,"\u2a63":e.MO.BIN4,"\u2a64":e.MO.BIN4,"\u2a65":e.MO.BIN4,"\u2a66":e.MO.REL,"\u2a67":e.MO.REL,"\u2a68":e.MO.REL,"\u2a69":e.MO.REL,"\u2a6a":e.MO.REL,"\u2a6b":e.MO.REL,"\u2a6c":e.MO.REL,"\u2a6d":e.MO.REL,"\u2a6e":e.MO.REL,"\u2a6f":e.MO.REL,"\u2a70":e.MO.REL,"\u2a71":e.MO.BIN4,"\u2a72":e.MO.BIN4,"\u2a73":e.MO.REL,"\u2a74":e.MO.REL,"\u2a75":e.MO.REL,"\u2a76":e.MO.REL,"\u2a77":e.MO.REL,"\u2a78":e.MO.REL,"\u2a79":e.MO.REL,"\u2a7a":e.MO.REL,"\u2a7b":e.MO.REL,"\u2a7c":e.MO.REL,"\u2a7d":e.MO.REL,"\u2a7d\u0338":e.MO.REL,"\u2a7e":e.MO.REL,"\u2a7e\u0338":e.MO.REL,"\u2a7f":e.MO.REL,"\u2a80":e.MO.REL,"\u2a81":e.MO.REL,"\u2a82":e.MO.REL,"\u2a83":e.MO.REL,"\u2a84":e.MO.REL,"\u2a85":e.MO.REL,"\u2a86":e.MO.REL,"\u2a87":e.MO.REL,"\u2a88":e.MO.REL,"\u2a89":e.MO.REL,"\u2a8a":e.MO.REL,"\u2a8b":e.MO.REL,"\u2a8c":e.MO.REL,"\u2a8d":e.MO.REL,"\u2a8e":e.MO.REL,"\u2a8f":e.MO.REL,"\u2a90":e.MO.REL,"\u2a91":e.MO.REL,"\u2a92":e.MO.REL,"\u2a93":e.MO.REL,"\u2a94":e.MO.REL,"\u2a95":e.MO.REL,"\u2a96":e.MO.REL,"\u2a97":e.MO.REL,"\u2a98":e.MO.REL,"\u2a99":e.MO.REL,"\u2a9a":e.MO.REL,"\u2a9b":e.MO.REL,"\u2a9c":e.MO.REL,"\u2a9d":e.MO.REL,"\u2a9e":e.MO.REL,"\u2a9f":e.MO.REL,"\u2aa0":e.MO.REL,"\u2aa1":e.MO.REL,"\u2aa1\u0338":e.MO.REL,"\u2aa2":e.MO.REL,"\u2aa2\u0338":e.MO.REL,"\u2aa3":e.MO.REL,"\u2aa4":e.MO.REL,"\u2aa5":e.MO.REL,"\u2aa6":e.MO.REL,"\u2aa7":e.MO.REL,"\u2aa8":e.MO.REL,"\u2aa9":e.MO.REL,"\u2aaa":e.MO.REL,"\u2aab":e.MO.REL,"\u2aac":e.MO.REL,"\u2aad":e.MO.REL,"\u2aae":e.MO.REL,"\u2aaf":e.MO.REL,"\u2aaf\u0338":e.MO.REL,"\u2ab0":e.MO.REL,"\u2ab0\u0338":e.MO.REL,"\u2ab1":e.MO.REL,"\u2ab2":e.MO.REL,"\u2ab3":e.MO.REL,"\u2ab4":e.MO.REL,"\u2ab5":e.MO.REL,"\u2ab6":e.MO.REL,"\u2ab7":e.MO.REL,"\u2ab8":e.MO.REL,"\u2ab9":e.MO.REL,"\u2aba":e.MO.REL,"\u2abb":e.MO.REL,"\u2abc":e.MO.REL,"\u2abd":e.MO.REL,"\u2abe":e.MO.REL,"\u2abf":e.MO.REL,"\u2ac0":e.MO.REL,"\u2ac1":e.MO.REL,"\u2ac2":e.MO.REL,"\u2ac3":e.MO.REL,"\u2ac4":e.MO.REL,"\u2ac5":e.MO.REL,"\u2ac6":e.MO.REL,"\u2ac7":e.MO.REL,"\u2ac8":e.MO.REL,"\u2ac9":e.MO.REL,"\u2aca":e.MO.REL,"\u2acb":e.MO.REL,"\u2acc":e.MO.REL,"\u2acd":e.MO.REL,"\u2ace":e.MO.REL,"\u2acf":e.MO.REL,"\u2ad0":e.MO.REL,"\u2ad1":e.MO.REL,"\u2ad2":e.MO.REL,"\u2ad3":e.MO.REL,"\u2ad4":e.MO.REL,"\u2ad5":e.MO.REL,"\u2ad6":e.MO.REL,"\u2ad7":e.MO.REL,"\u2ad8":e.MO.REL,"\u2ad9":e.MO.REL,"\u2ada":e.MO.REL,"\u2adb":e.MO.REL,"\u2add":e.MO.REL,"\u2add\u0338":e.MO.REL,"\u2ade":e.MO.REL,"\u2adf":e.MO.REL,"\u2ae0":e.MO.REL,"\u2ae1":e.MO.REL,"\u2ae2":e.MO.REL,"\u2ae3":e.MO.REL,"\u2ae4":e.MO.REL,"\u2ae5":e.MO.REL,"\u2ae6":e.MO.REL,"\u2ae7":e.MO.REL,"\u2ae8":e.MO.REL,"\u2ae9":e.MO.REL,"\u2aea":e.MO.REL,"\u2aeb":e.MO.REL,"\u2aec":e.MO.REL,"\u2aed":e.MO.REL,"\u2aee":e.MO.REL,"\u2aef":e.MO.REL,"\u2af0":e.MO.REL,"\u2af1":e.MO.REL,"\u2af2":e.MO.REL,"\u2af3":e.MO.REL,"\u2af4":e.MO.BIN4,"\u2af5":e.MO.BIN4,"\u2af6":e.MO.BIN4,"\u2af7":e.MO.REL,"\u2af8":e.MO.REL,"\u2af9":e.MO.REL,"\u2afa":e.MO.REL,"\u2afb":e.MO.BIN4,"\u2afd":e.MO.BIN4,"\u2afe":e.MO.BIN3,"\u2b45":e.MO.RELSTRETCH,"\u2b46":e.MO.RELSTRETCH,"\u3008":e.MO.OPEN,"\u3009":e.MO.CLOSE,"\ufe37":e.MO.WIDEACCENT,"\ufe38":e.MO.WIDEACCENT}},e.OPTABLE.infix["^"]=e.MO.WIDEREL,e.OPTABLE.infix._=e.MO.WIDEREL,e.OPTABLE.infix["\u2adc"]=e.MO.REL},9259:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},s=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,i=r.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s};Object.defineProperty(e,"__esModule",{value:!0}),e.SerializedMmlVisitor=e.toEntity=e.DATAMJX=void 0;var a=r(6325),l=r(9007),u=r(450);e.DATAMJX="data-mjx-";e.toEntity=function(t){return"&#x"+t.codePointAt(0).toString(16).toUpperCase()+";"};var c=function(t){function r(){return null!==t&&t.apply(this,arguments)||this}return o(r,t),r.prototype.visitTree=function(t){return this.visitNode(t,"")},r.prototype.visitTextNode=function(t,e){return this.quoteHTML(t.getText())},r.prototype.visitXMLNode=function(t,e){return e+t.getSerializedXML()},r.prototype.visitInferredMrowNode=function(t,e){var r,n,o=[];try{for(var s=i(t.childNodes),a=s.next();!a.done;a=s.next()){var l=a.value;o.push(this.visitNode(l,e))}}catch(t){r={error:t}}finally{try{a&&!a.done&&(n=s.return)&&n.call(s)}finally{if(r)throw r.error}}return o.join("\n")},r.prototype.visitTeXAtomNode=function(t,e){var r=this.childNodeMml(t,e+" ","\n");return e+""+(r.match(/\S/)?"\n"+r+e:"")+""},r.prototype.visitAnnotationNode=function(t,e){return e+""+this.childNodeMml(t,"","")+""},r.prototype.visitDefault=function(t,e){var r=t.kind,n=s(t.isToken||0===t.childNodes.length?["",""]:["\n",e],2),o=n[0],i=n[1],a=this.childNodeMml(t,e+" ",o);return e+"<"+r+this.getAttributes(t)+">"+(a.match(/\S/)?o+a+i:"")+""},r.prototype.childNodeMml=function(t,e,r){var n,o,s="";try{for(var a=i(t.childNodes),l=a.next();!l.done;l=a.next()){var u=l.value;s+=this.visitNode(u,e)+r}}catch(t){n={error:t}}finally{try{l&&!l.done&&(o=a.return)&&o.call(a)}finally{if(n)throw n.error}}return s},r.prototype.getAttributes=function(t){var e,r,n=[],o=this.constructor.defaultAttributes[t.kind]||{},s=Object.assign({},o,this.getDataAttributes(t),t.attributes.getAllAttributes()),a=this.constructor.variants;s.hasOwnProperty("mathvariant")&&a.hasOwnProperty(s.mathvariant)&&(s.mathvariant=a[s.mathvariant]);try{for(var l=i(Object.keys(s)),u=l.next();!u.done;u=l.next()){var c=u.value,p=String(s[c]);void 0!==p&&n.push(c+'="'+this.quoteHTML(p)+'"')}}catch(t){e={error:t}}finally{try{u&&!u.done&&(r=l.return)&&r.call(l)}finally{if(e)throw e.error}}return n.length?" "+n.join(" "):""},r.prototype.getDataAttributes=function(t){var e={},r=t.attributes.getExplicit("mathvariant"),n=this.constructor.variants;r&&n.hasOwnProperty(r)&&this.setDataAttribute(e,"variant",r),t.getProperty("variantForm")&&this.setDataAttribute(e,"alternate","1"),t.getProperty("pseudoscript")&&this.setDataAttribute(e,"pseudoscript","true"),!1===t.getProperty("autoOP")&&this.setDataAttribute(e,"auto-op","false");var o=t.getProperty("texClass");if(void 0!==o){var i=!0;if(o===l.TEXCLASS.OP&&t.isKind("mi")){var s=t.getText();i=!(s.length>1&&s.match(u.MmlMi.operatorName))}i&&this.setDataAttribute(e,"texclass",o<0?"NONE":l.TEXCLASSNAMES[o])}return t.getProperty("scriptlevel")&&!1===t.getProperty("useHeight")&&this.setDataAttribute(e,"smallmatrix","true"),e},r.prototype.setDataAttribute=function(t,r,n){t[e.DATAMJX+r]=n},r.prototype.quoteHTML=function(t){return t.replace(/&/g,"&").replace(//g,">").replace(/\"/g,""").replace(/[\uD800-\uDBFF]./g,e.toEntity).replace(/[\u0080-\uD7FF\uE000-\uFFFF]/g,e.toEntity)},r.variants={"-tex-calligraphic":"script","-tex-bold-calligraphic":"bold-script","-tex-oldstyle":"normal","-tex-bold-oldstyle":"bold","-tex-mathit":"italic"},r.defaultAttributes={math:{xmlns:"http://www.w3.org/1998/Math/MathML"}},r}(a.MmlVisitor);e.SerializedMmlVisitor=c},2975:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractOutputJax=void 0;var n=r(7233),o=r(7525),i=function(){function t(t){void 0===t&&(t={}),this.adaptor=null;var e=this.constructor;this.options=n.userOptions(n.defaultOptions({},e.OPTIONS),t),this.postFilters=new o.FunctionList}return Object.defineProperty(t.prototype,"name",{get:function(){return this.constructor.NAME},enumerable:!1,configurable:!0}),t.prototype.setAdaptor=function(t){this.adaptor=t},t.prototype.initialize=function(){},t.prototype.reset=function(){for(var t=[],e=0;e=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,i=r.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},o=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractEmptyNode=e.AbstractNode=void 0;var i=function(){function t(t,e,r){var n,i;void 0===e&&(e={}),void 0===r&&(r=[]),this.factory=t,this.parent=null,this.properties={},this.childNodes=[];try{for(var s=o(Object.keys(e)),a=s.next();!a.done;a=s.next()){var l=a.value;this.setProperty(l,e[l])}}catch(t){n={error:t}}finally{try{a&&!a.done&&(i=s.return)&&i.call(s)}finally{if(n)throw n.error}}r.length&&this.setChildren(r)}return Object.defineProperty(t.prototype,"kind",{get:function(){return"unknown"},enumerable:!1,configurable:!0}),t.prototype.setProperty=function(t,e){this.properties[t]=e},t.prototype.getProperty=function(t){return this.properties[t]},t.prototype.getPropertyNames=function(){return Object.keys(this.properties)},t.prototype.getAllProperties=function(){return this.properties},t.prototype.removeProperty=function(){for(var t,e,r=[],n=0;n=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},o=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,i=r.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},i=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},s=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},a=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.HTMLDocument=void 0;var l=r(5722),u=r(7233),c=r(3363),p=r(3335),f=r(5138),h=r(4474),d=function(t){function e(e,r,n){var o=this,i=s(u.separateOptions(n,f.HTMLDomStrings.OPTIONS),2),a=i[0],l=i[1];return(o=t.call(this,e,r,a)||this).domStrings=o.options.DomStrings||new f.HTMLDomStrings(l),o.domStrings.adaptor=r,o.styles=[],o}return o(e,t),e.prototype.findPosition=function(t,e,r,n){var o,i,l=this.adaptor;try{for(var u=a(n[t]),c=u.next();!c.done;c=u.next()){var p=c.value,f=s(p,2),h=f[0],d=f[1];if(e<=d&&"#text"===l.kind(h))return{node:h,n:Math.max(e,0),delim:r};e-=d}}catch(t){o={error:t}}finally{try{c&&!c.done&&(i=u.return)&&i.call(u)}finally{if(o)throw o.error}}return{node:null,n:0,delim:r}},e.prototype.mathItem=function(t,e,r){var n=t.math,o=this.findPosition(t.n,t.start.n,t.open,r),i=this.findPosition(t.n,t.end.n,t.close,r);return new this.options.MathItem(n,e,t.display,o,i)},e.prototype.findMath=function(t){var e,r,n,o,i,l,c,p,f;if(!this.processed.isSet("findMath")){this.adaptor.document=this.document,t=u.userOptions({elements:this.options.elements||[this.adaptor.body(this.document)]},t);try{for(var h=a(this.adaptor.getElements(t.elements,this.document)),d=h.next();!d.done;d=h.next()){var y=d.value,O=s([null,null],2),M=O[0],E=O[1];try{for(var v=(n=void 0,a(this.inputJax)),b=v.next();!b.done;b=v.next()){var m=b.value,g=new this.options.MathList;if(m.processStrings){null===M&&(M=(i=s(this.domStrings.find(y),2))[0],E=i[1]);try{for(var L=(l=void 0,a(m.findMath(M))),N=L.next();!N.done;N=L.next()){var R=N.value;g.push(this.mathItem(R,m,E))}}catch(t){l={error:t}}finally{try{N&&!N.done&&(c=L.return)&&c.call(L)}finally{if(l)throw l.error}}}else try{for(var T=(p=void 0,a(m.findMath(y))),S=T.next();!S.done;S=T.next()){R=S.value;var A=new this.options.MathItem(R.math,m,R.display,R.start,R.end);g.push(A)}}catch(t){p={error:t}}finally{try{S&&!S.done&&(f=T.return)&&f.call(T)}finally{if(p)throw p.error}}this.math.merge(g)}}catch(t){n={error:t}}finally{try{b&&!b.done&&(o=v.return)&&o.call(v)}finally{if(n)throw n.error}}}}catch(t){e={error:t}}finally{try{d&&!d.done&&(r=h.return)&&r.call(h)}finally{if(e)throw e.error}}this.processed.set("findMath")}return this},e.prototype.updateDocument=function(){return this.processed.isSet("updateDocument")||(this.addPageElements(),this.addStyleSheet(),t.prototype.updateDocument.call(this),this.processed.set("updateDocument")),this},e.prototype.addPageElements=function(){var t=this.adaptor.body(this.document),e=this.documentPageElements();e&&this.adaptor.append(t,e)},e.prototype.addStyleSheet=function(){var t=this.documentStyleSheet(),e=this.adaptor;if(t&&!e.parent(t)){var r=e.head(this.document),n=this.findSheet(r,e.getAttribute(t,"id"));n?e.replace(t,n):e.append(r,t)}},e.prototype.findSheet=function(t,e){var r,n;if(e)try{for(var o=a(this.adaptor.tags(t,"style")),i=o.next();!i.done;i=o.next()){var s=i.value;if(this.adaptor.getAttribute(s,"id")===e)return s}}catch(t){r={error:t}}finally{try{i&&!i.done&&(n=o.return)&&n.call(o)}finally{if(r)throw r.error}}return null},e.prototype.removeFromDocument=function(t){var e,r;if(void 0===t&&(t=!1),this.processed.isSet("updateDocument"))try{for(var n=a(this.math),o=n.next();!o.done;o=n.next()){var i=o.value;i.state()>=h.STATE.INSERTED&&i.state(h.STATE.TYPESET,t)}}catch(t){e={error:t}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(e)throw e.error}}return this.processed.clear("updateDocument"),this},e.prototype.documentStyleSheet=function(){return this.outputJax.styleSheet(this)},e.prototype.documentPageElements=function(){return this.outputJax.pageElements(this)},e.prototype.addStyles=function(t){this.styles.push(t)},e.prototype.getStyles=function(){return this.styles},e.KIND="HTML",e.OPTIONS=i(i({},l.AbstractMathDocument.OPTIONS),{renderActions:u.expandable(i(i({},l.AbstractMathDocument.OPTIONS.renderActions),{styles:[h.STATE.INSERTED+1,"","updateStyleSheet",!1]})),MathList:p.HTMLMathList,MathItem:c.HTMLMathItem,DomStrings:null}),e}(l.AbstractMathDocument);e.HTMLDocument=d},5138:function(t,e,r){var n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,i=r.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s};Object.defineProperty(e,"__esModule",{value:!0}),e.HTMLDomStrings=void 0;var o=r(7233),i=function(){function t(t){void 0===t&&(t=null);var e=this.constructor;this.options=o.userOptions(o.defaultOptions({},e.OPTIONS),t),this.init(),this.getPatterns()}return t.prototype.init=function(){this.strings=[],this.string="",this.snodes=[],this.nodes=[],this.stack=[]},t.prototype.getPatterns=function(){var t=o.makeArray(this.options.skipHtmlTags),e=o.makeArray(this.options.ignoreHtmlClass),r=o.makeArray(this.options.processHtmlClass);this.skipHtmlTags=new RegExp("^(?:"+t.join("|")+")$","i"),this.ignoreHtmlClass=new RegExp("(?:^| )(?:"+e.join("|")+")(?: |$)"),this.processHtmlClass=new RegExp("(?:^| )(?:"+r+")(?: |$)")},t.prototype.pushString=function(){this.string.match(/\S/)&&(this.strings.push(this.string),this.nodes.push(this.snodes)),this.string="",this.snodes=[]},t.prototype.extendString=function(t,e){this.snodes.push([t,e.length]),this.string+=e},t.prototype.handleText=function(t,e){return e||this.extendString(t,this.adaptor.value(t)),this.adaptor.next(t)},t.prototype.handleTag=function(t,e){if(!e){var r=this.options.includeHtmlTags[this.adaptor.kind(t)];this.extendString(t,r)}return this.adaptor.next(t)},t.prototype.handleContainer=function(t,e){this.pushString();var r=this.adaptor.getAttribute(t,"class")||"",n=this.adaptor.kind(t)||"",o=this.processHtmlClass.exec(r),i=t;return!this.adaptor.firstChild(t)||this.adaptor.getAttribute(t,"data-MJX")||!o&&this.skipHtmlTags.exec(n)?i=this.adaptor.next(t):(this.adaptor.next(t)&&this.stack.push([this.adaptor.next(t),e]),i=this.adaptor.firstChild(t),e=(e||this.ignoreHtmlClass.exec(r))&&!o),[i,e]},t.prototype.handleOther=function(t,e){return this.pushString(),this.adaptor.next(t)},t.prototype.find=function(t){var e,r;this.init();for(var o=this.adaptor.next(t),i=!1,s=this.options.includeHtmlTags;t&&t!==o;){var a=this.adaptor.kind(t);"#text"===a?t=this.handleText(t,i):s.hasOwnProperty(a)?t=this.handleTag(t,i):a?(t=(e=n(this.handleContainer(t,i),2))[0],i=e[1]):t=this.handleOther(t,i),!t&&this.stack.length&&(this.pushString(),t=(r=n(this.stack.pop(),2))[0],i=r[1])}this.pushString();var l=[this.strings,this.nodes];return this.init(),l},t.OPTIONS={skipHtmlTags:["script","noscript","style","textarea","pre","code","annotation","annotation-xml"],includeHtmlTags:{br:"\n",wbr:"","#comment":""},ignoreHtmlClass:"mathjax_ignore",processHtmlClass:"mathjax_process"},t}();e.HTMLDomStrings=i},3726:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.HTMLHandler=void 0;var i=r(3670),s=r(3683),a=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.documentClass=s.HTMLDocument,e}return o(e,t),e.prototype.handlesDocument=function(t){var e=this.adaptor;if("string"==typeof t)try{t=e.parse(t,"text/html")}catch(t){}return t instanceof e.window.Document||t instanceof e.window.HTMLElement||t instanceof e.window.DocumentFragment},e.prototype.create=function(e,r){var n=this.adaptor;if("string"==typeof e)e=n.parse(e,"text/html");else if(e instanceof n.window.HTMLElement||e instanceof n.window.DocumentFragment){var o=e;e=n.parse("","text/html"),n.append(n.body(e),o)}return t.prototype.create.call(this,e,r)},e}(i.AbstractHandler);e.HTMLHandler=a},3363:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.HTMLMathItem=void 0;var i=r(4474),s=function(t){function e(e,r,n,o,i){return void 0===n&&(n=!0),void 0===o&&(o={node:null,n:0,delim:""}),void 0===i&&(i={node:null,n:0,delim:""}),t.call(this,e,r,n,o,i)||this}return o(e,t),Object.defineProperty(e.prototype,"adaptor",{get:function(){return this.inputJax.adaptor},enumerable:!1,configurable:!0}),e.prototype.updateDocument=function(t){if(this.state()=i.STATE.TYPESET){var e=this.adaptor,r=this.start.node,n=e.text("");if(t){var o=this.start.delim+this.math+this.end.delim;if(this.inputJax.processStrings)n=e.text(o);else{var s=e.parse(o,"text/html");n=e.firstChild(e.body(s))}}e.parent(r)&&e.replace(n,r),this.start.node=this.end.node=n,this.start.n=this.end.n=0}},e}(i.AbstractMathItem);e.HTMLMathItem=s},3335:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.HTMLMathList=void 0;var i=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e}(r(9e3).AbstractMathList);e.HTMLMathList=i},5713:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.mathjax=void 0;var n=r(805),o=r(4542);e.mathjax={version:"3.1.4",handlers:new n.HandlerList,document:function(t,r){return e.mathjax.handlers.document(t,r)},handleRetriesFor:o.handleRetriesFor,retryAfter:o.retryAfter,asyncLoad:null}},9923:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.asyncLoad=void 0;var n=r(5713);e.asyncLoad=function(t){return n.mathjax.asyncLoad?new Promise((function(e,r){var o=n.mathjax.asyncLoad(t);o instanceof Promise?o.then((function(t){return e(t)})).catch((function(t){return r(t)})):e(o)})):Promise.reject("Can't load '"+t+"': No asyncLoad method specified")}},6469:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.BBox=e.BBoxStyleAdjust=void 0;var n=r(6010);e.BBoxStyleAdjust=[["borderTopWidth","h"],["borderRightWidth","w"],["borderBottomWidth","d"],["borderLeftWidth","w",0],["paddingTop","h"],["paddingRight","w"],["paddingBottom","d"],["paddingLeft","w",0]];var o=function(){function t(t){void 0===t&&(t={w:0,h:-n.BIGDIMEN,d:-n.BIGDIMEN}),this.w=t.w||0,this.h="h"in t?t.h:-n.BIGDIMEN,this.d="d"in t?t.d:-n.BIGDIMEN,this.L=this.R=this.ic=this.sk=0,this.scale=this.rscale=1,this.pwidth=""}return t.zero=function(){return new t({h:0,d:0,w:0})},t.empty=function(){return new t},t.prototype.empty=function(){return this.w=0,this.h=this.d=-n.BIGDIMEN,this},t.prototype.clean=function(){this.w===-n.BIGDIMEN&&(this.w=0),this.h===-n.BIGDIMEN&&(this.h=0),this.d===-n.BIGDIMEN&&(this.d=0)},t.prototype.rescale=function(t){this.w*=t,this.h*=t,this.d*=t},t.prototype.combine=function(t,e,r){void 0===e&&(e=0),void 0===r&&(r=0);var n=t.rscale,o=e+n*(t.w+t.L+t.R),i=r+n*t.h,s=n*t.d-r;o>this.w&&(this.w=o),i>this.h&&(this.h=i),s>this.d&&(this.d=s)},t.prototype.append=function(t){var e=t.rscale;this.w+=e*(t.w+t.L+t.R),e*t.h>this.h&&(this.h=e*t.h),e*t.d>this.d&&(this.d=e*t.d)},t.prototype.updateFrom=function(t){this.h=t.h,this.d=t.d,this.w=t.w,t.pwidth&&(this.pwidth=t.pwidth)},t.fullWidth="100%",t}();e.BBox=o},6751:function(t,e){var r,n=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function n(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)}),o=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,i=r.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},s=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r",gtdot:"\u22d7",harrw:"\u21ad",hbar:"\u210f",hellip:"\u2026",hookleftarrow:"\u21a9",hookrightarrow:"\u21aa",imath:"\u0131",infin:"\u221e",intcal:"\u22ba",iota:"\u03b9",jmath:"\u0237",kappa:"\u03ba",kappav:"\u03f0",lEg:"\u2a8b",lambda:"\u03bb",lap:"\u2a85",larrlp:"\u21ab",larrtl:"\u21a2",lbrace:"{",lbrack:"[",le:"\u2264",leftleftarrows:"\u21c7",leftthreetimes:"\u22cb",lessdot:"\u22d6",lmoust:"\u23b0",lnE:"\u2268",lnap:"\u2a89",lne:"\u2a87",lnsim:"\u22e6",longmapsto:"\u27fc",looparrowright:"\u21ac",lowast:"\u2217",loz:"\u25ca",lt:"<",ltimes:"\u22c9",ltri:"\u25c3",macr:"\xaf",malt:"\u2720",mho:"\u2127",mu:"\u03bc",multimap:"\u22b8",nLeftarrow:"\u21cd",nLeftrightarrow:"\u21ce",nRightarrow:"\u21cf",nVDash:"\u22af",nVdash:"\u22ae",natur:"\u266e",nearr:"\u2197",nharr:"\u21ae",nlarr:"\u219a",not:"\xac",nrarr:"\u219b",nu:"\u03bd",nvDash:"\u22ad",nvdash:"\u22ac",nwarr:"\u2196",omega:"\u03c9",omicron:"\u03bf",or:"\u2228",osol:"\u2298",period:".",phi:"\u03c6",phiv:"\u03d5",pi:"\u03c0",piv:"\u03d6",prap:"\u2ab7",precnapprox:"\u2ab9",precneqq:"\u2ab5",precnsim:"\u22e8",prime:"\u2032",psi:"\u03c8",quot:'"',rarrtl:"\u21a3",rbrace:"}",rbrack:"]",rho:"\u03c1",rhov:"\u03f1",rightrightarrows:"\u21c9",rightthreetimes:"\u22cc",ring:"\u02da",rmoust:"\u23b1",rtimes:"\u22ca",rtri:"\u25b9",scap:"\u2ab8",scnE:"\u2ab6",scnap:"\u2aba",scnsim:"\u22e9",sdot:"\u22c5",searr:"\u2198",sect:"\xa7",sharp:"\u266f",sigma:"\u03c3",sigmav:"\u03c2",simne:"\u2246",smile:"\u2323",spades:"\u2660",sub:"\u2282",subE:"\u2ac5",subnE:"\u2acb",subne:"\u228a",supE:"\u2ac6",supnE:"\u2acc",supne:"\u228b",swarr:"\u2199",tau:"\u03c4",theta:"\u03b8",thetav:"\u03d1",tilde:"\u02dc",times:"\xd7",triangle:"\u25b5",triangleq:"\u225c",upsi:"\u03c5",upuparrows:"\u21c8",veebar:"\u22bb",vellip:"\u22ee",weierp:"\u2118",xi:"\u03be",yen:"\xa5",zeta:"\u03b6",zigrarr:"\u21dd",nbsp:"\xa0",rsquo:"\u2019",lsquo:"\u2018"};var i={};function s(t,r){if("#"===r.charAt(0))return a(r.slice(1));if(e.entities[r])return e.entities[r];if(e.options.loadMissingEntities){var s=r.match(/^[a-zA-Z](fr|scr|opf)$/)?RegExp.$1:r.charAt(0).toLowerCase();i[s]||(i[s]=!0,n.retryAfter(o.asyncLoad("./util/entities/"+s+".js")))}return t}function a(t){var e="x"===t.charAt(0)?parseInt(t.slice(1),16):parseInt(t);return String.fromCodePoint(e)}e.add=function(t,r){Object.assign(e.entities,t),i[r]=!0},e.remove=function(t){delete e.entities[t]},e.translate=function(t){return t.replace(/&([a-z][a-z0-9]*|#(?:[0-9]+|x[0-9a-f]+));/gi,s)},e.numeric=a},7525:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},s=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,i=r.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},a=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},n=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.LinkedList=e.ListItem=e.END=void 0,e.END=Symbol();var i=function(t){void 0===t&&(t=null),this.next=null,this.prev=null,this.data=t};e.ListItem=i;var s=function(){function t(){for(var t=[],o=0;o1;){var u=i.shift(),c=i.shift();u.merge(c,e),i.push(u)}return i.length&&(this.list=i[0].list),this},t.prototype.merge=function(t,n){var o,i,s,a,l;void 0===n&&(n=null),null===n&&(n=this.isBefore.bind(this));for(var u=this.list.next,c=t.list.next;u.data!==e.END&&c.data!==e.END;)n(c.data,u.data)?(o=r([u,c],2),c.prev.next=o[0],u.prev.next=o[1],i=r([u.prev,c.prev],2),c.prev=i[0],u.prev=i[1],s=r([t.list,this.list],2),this.list.prev.next=s[0],t.list.prev.next=s[1],a=r([t.list.prev,this.list.prev],2),this.list.prev=a[0],t.list.prev=a[1],u=(l=r([c.next,u],2))[0],c=l[1]):u=u.next;return c.data!==e.END&&(this.list.prev.next=t.list.next,t.list.next.prev=this.list.prev,t.list.prev.next=this.list,this.list.prev=t.list.prev,t.list.next=t.list.prev=t.list),this},t}();e.LinkedList=s},7233:function(t,e){var r=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,i=r.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},o=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;re.length}}}},t.prototype.add=function(e,r){void 0===r&&(r=t.DEFAULTPRIORITY);var n=this.items.length;do{n--}while(n>=0&&r=0&&this.items[e].item!==t);e>=0&&this.items.splice(e,1)},t.prototype.toArray=function(){return Array.from(this)},t.DEFAULTPRIORITY=5,t}();e.PrioritizedList=r},4542:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.retryAfter=e.handleRetriesFor=void 0,e.handleRetriesFor=function(t){return new Promise((function e(r,n){try{r(t())}catch(t){t.retry&&t.retry instanceof Promise?t.retry.then((function(){return e(r,n)})).catch((function(t){return n(t)})):t.restart&&t.restart.isCallback?MathJax.Callback.After((function(){return e(r,n)}),t.restart):n(t)}}))},e.retryAfter=function(t){var e=new Error("MathJax retry");throw e.retry=t,e}},4139:function(t,e){var r=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CssStyles=void 0;var n=function(){function t(t){void 0===t&&(t=null),this.styles={},this.addStyles(t)}return Object.defineProperty(t.prototype,"cssText",{get:function(){return this.getStyleString()},enumerable:!1,configurable:!0}),t.prototype.addStyles=function(t){var e,n;if(t)try{for(var o=r(Object.keys(t)),i=o.next();!i.done;i=o.next()){var s=i.value;this.styles[s]||(this.styles[s]={}),Object.assign(this.styles[s],t[s])}}catch(t){e={error:t}}finally{try{i&&!i.done&&(n=o.return)&&n.call(o)}finally{if(e)throw e.error}}},t.prototype.removeStyles=function(){for(var t,e,n=[],o=0;o=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,i=r.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},o=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r1;)e.shift(),r.push(e.shift());return r}function l(t){var e,n,o=a(this.styles[t]);0===o.length&&o.push(""),1===o.length&&o.push(o[0]),2===o.length&&o.push(o[0]),3===o.length&&o.push(o[1]);try{for(var i=r(v.connect[t].children),s=i.next();!s.done;s=i.next()){var l=s.value;this.setStyle(this.childName(t,l),o.shift())}}catch(t){e={error:t}}finally{try{s&&!s.done&&(n=i.return)&&n.call(i)}finally{if(e)throw e.error}}}function u(t){var e,n,o=v.connect[t].children,i=[];try{for(var s=r(o),a=s.next();!a.done;a=s.next()){var l=a.value,u=this.styles[t+"-"+l];if(!u)return void delete this.styles[t];i.push(u)}}catch(t){e={error:t}}finally{try{a&&!a.done&&(n=s.return)&&n.call(s)}finally{if(e)throw e.error}}i[3]===i[1]&&(i.pop(),i[2]===i[0]&&(i.pop(),i[1]===i[0]&&i.pop())),this.styles[t]=i.join(" ")}function c(t){var e,n;try{for(var o=r(v.connect[t].children),i=o.next();!i.done;i=o.next()){var s=i.value;this.setStyle(this.childName(t,s),this.styles[t])}}catch(t){e={error:t}}finally{try{i&&!i.done&&(n=o.return)&&n.call(o)}finally{if(e)throw e.error}}}function p(t){var e,i,s=o([],n(v.connect[t].children)),a=this.styles[this.childName(t,s.shift())];try{for(var l=r(s),u=l.next();!u.done;u=l.next()){var c=u.value;if(this.styles[this.childName(t,c)]!==a)return void delete this.styles[t]}}catch(t){e={error:t}}finally{try{u&&!u.done&&(i=l.return)&&i.call(l)}finally{if(e)throw e.error}}this.styles[t]=a}var f=/^(?:[\d.]+(?:[a-z]+)|thin|medium|thick|inherit|initial|unset)$/,h=/^(?:none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset|inherit|initial|unset)$/;function d(t){var e,n,o,i,s={width:"",style:"",color:""};try{for(var l=r(a(this.styles[t])),u=l.next();!u.done;u=l.next()){var c=u.value;c.match(f)&&""===s.width?s.width=c:c.match(h)&&""===s.style?s.style=c:s.color=c}}catch(t){e={error:t}}finally{try{u&&!u.done&&(n=l.return)&&n.call(l)}finally{if(e)throw e.error}}try{for(var p=r(v.connect[t].children),d=p.next();!d.done;d=p.next()){var y=d.value;this.setStyle(this.childName(t,y),s[y])}}catch(t){o={error:t}}finally{try{d&&!d.done&&(i=p.return)&&i.call(p)}finally{if(o)throw o.error}}}function y(t){var e,n,o=[];try{for(var i=r(v.connect[t].children),s=i.next();!s.done;s=i.next()){var a=s.value,l=this.styles[this.childName(t,a)];l&&o.push(l)}}catch(t){e={error:t}}finally{try{s&&!s.done&&(n=i.return)&&n.call(i)}finally{if(e)throw e.error}}o.length?this.styles[t]=o.join(" "):delete this.styles[t]}var O={style:/^(?:normal|italic|oblique|inherit|initial|unset)$/,variant:new RegExp("^(?:"+["normal|none","inherit|initial|unset","common-ligatures|no-common-ligatures","discretionary-ligatures|no-discretionary-ligatures","historical-ligatures|no-historical-ligatures","contextual|no-contextual","(?:stylistic|character-variant|swash|ornaments|annotation)\\([^)]*\\)","small-caps|all-small-caps|petite-caps|all-petite-caps|unicase|titling-caps","lining-nums|oldstyle-nums|proportional-nums|tabular-nums","diagonal-fractions|stacked-fractions","ordinal|slashed-zero","jis78|jis83|jis90|jis04|simplified|traditional","full-width|proportional-width","ruby"].join("|")+")$"),weight:/^(?:normal|bold|bolder|lighter|[1-9]00|inherit|initial|unset)$/,stretch:new RegExp("^(?:"+["normal","(?:(?:ultra|extra|semi)-)?condensed","(?:(?:semi|extra|ulta)-)?expanded","inherit|initial|unset"].join("|")+")$"),size:new RegExp("^(?:"+["xx-small|x-small|small|medium|large|x-large|xx-large|larger|smaller","[d.]+%|[d.]+[a-z]+","inherit|initial|unset"].join("|")+")(?:/(?:normal|[d.+](?:%|[a-z]+)?))?$")};function M(t){var e,o,i,s,l=a(this.styles[t]),u={style:"",variant:[],weight:"",stretch:"",size:"",family:"","line-height":""};try{for(var c=r(l),p=c.next();!p.done;p=c.next()){var f=p.value;u.family=f;try{for(var h=(i=void 0,r(Object.keys(O))),d=h.next();!d.done;d=h.next()){var y=d.value;if((Array.isArray(u[y])||""===u[y])&&f.match(O[y]))if("size"===y){var M=n(f.split(/\//),2),E=M[0],b=M[1];u[y]=E,b&&(u["line-height"]=b)}else""===u.size&&(Array.isArray(u[y])?u[y].push(f):u[y]=f)}}catch(t){i={error:t}}finally{try{d&&!d.done&&(s=h.return)&&s.call(h)}finally{if(i)throw i.error}}}}catch(t){e={error:t}}finally{try{p&&!p.done&&(o=c.return)&&o.call(c)}finally{if(e)throw e.error}}!function(t,e){var n,o;try{for(var i=r(v.connect[t].children),s=i.next();!s.done;s=i.next()){var a=s.value,l=this.childName(t,a);if(Array.isArray(e[a])){var u=e[a];u.length&&(this.styles[l]=u.join(" "))}else""!==e[a]&&(this.styles[l]=e[a])}}catch(t){n={error:t}}finally{try{s&&!s.done&&(o=i.return)&&o.call(i)}finally{if(n)throw n.error}}}(t,u),delete this.styles[t]}function E(t){}var v=function(){function t(t){void 0===t&&(t=""),this.parse(t)}return Object.defineProperty(t.prototype,"cssText",{get:function(){var t,e,n=[];try{for(var o=r(Object.keys(this.styles)),i=o.next();!i.done;i=o.next()){var s=i.value,a=this.parentName(s);this.styles[a]||n.push(s+": "+this.styles[s])}}catch(e){t={error:e}}finally{try{i&&!i.done&&(e=o.return)&&e.call(o)}finally{if(t)throw t.error}}return n.join("; ")},enumerable:!1,configurable:!0}),t.prototype.set=function(e,r){for(e=this.normalizeName(e),this.setStyle(e,r),t.connect[e]&&!t.connect[e].combine&&(this.combineChildren(e),delete this.styles[e]);e.match(/-/)&&(e=this.parentName(e),t.connect[e]);)t.connect[e].combine.call(this,e)},t.prototype.get=function(t){return t=this.normalizeName(t),this.styles.hasOwnProperty(t)?this.styles[t]:""},t.prototype.setStyle=function(e,r){this.styles[e]=r,t.connect[e]&&t.connect[e].children&&t.connect[e].split.call(this,e),""===r&&delete this.styles[e]},t.prototype.combineChildren=function(e){var n,o,i=this.parentName(e);try{for(var s=r(t.connect[e].children),a=s.next();!a.done;a=s.next()){var l=a.value,u=this.childName(i,l);t.connect[u].combine.call(this,u)}}catch(t){n={error:t}}finally{try{a&&!a.done&&(o=s.return)&&o.call(s)}finally{if(n)throw n.error}}},t.prototype.parentName=function(t){var e=t.replace(/-[^-]*$/,"");return t===e?"":e},t.prototype.childName=function(e,r){return r.match(/-/)?r:(t.connect[e]&&!t.connect[e].combine&&(r+=e.replace(/.*-/,"-"),e=this.parentName(e)),e+"-"+r)},t.prototype.normalizeName=function(t){return t.replace(/[A-Z]/g,(function(t){return"-"+t.toLowerCase()}))},t.prototype.parse=function(t){void 0===t&&(t="");var e=this.constructor.pattern;this.styles={};for(var r=t.replace(e.comment,"").split(e.style);r.length>1;){var o=n(r.splice(0,3),3),i=o[0],s=o[1],a=o[2];if(i.match(/[^\s\n]/))return;this.set(s,a)}},t.pattern={style:/([-a-z]+)[\s\n]*:[\s\n]*((?:'[^']*'|"[^"]*"|\n|.)*?)[\s\n]*(?:;|$)/g,comment:/\/\*[^]*?\*\//g},t.connect={padding:{children:i,split:l,combine:u},border:{children:i,split:c,combine:p},"border-top":{children:s,split:d,combine:y},"border-right":{children:s,split:d,combine:y},"border-bottom":{children:s,split:d,combine:y},"border-left":{children:s,split:d,combine:y},"border-width":{children:i,split:l,combine:null},"border-style":{children:i,split:l,combine:null},"border-color":{children:i,split:l,combine:null},font:{children:["style","variant","weight","stretch","line-height","size","family"],split:M,combine:E}},t}();e.Styles=v},6010:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.px=e.emRounded=e.em=e.percent=e.length2em=e.MATHSPACE=e.RELUNITS=e.UNITS=e.BIGDIMEN=void 0,e.BIGDIMEN=1e6,e.UNITS={px:1,in:96,cm:96/2.54,mm:96/25.4},e.RELUNITS={em:1,ex:.431,pt:.1,pc:1.2,mu:1/18},e.MATHSPACE={veryverythinmathspace:1/18,verythinmathspace:2/18,thinmathspace:3/18,mediummathspace:4/18,thickmathspace:5/18,verythickmathspace:6/18,veryverythickmathspace:7/18,negativeveryverythinmathspace:-1/18,negativeverythinmathspace:-2/18,negativethinmathspace:-3/18,negativemediummathspace:-4/18,negativethickmathspace:-5/18,negativeverythickmathspace:-6/18,negativeveryverythickmathspace:-7/18,thin:.04,medium:.06,thick:.1,normal:1,big:2,small:1/Math.sqrt(2),infinity:e.BIGDIMEN},e.length2em=function(t,r,n,o){if(void 0===r&&(r=0),void 0===n&&(n=1),void 0===o&&(o=16),"string"!=typeof t&&(t=String(t)),""===t||null==t)return r;if(e.MATHSPACE[t])return e.MATHSPACE[t];var i=t.match(/^\s*([-+]?(?:\.\d+|\d+(?:\.\d*)?))?(pt|em|ex|mu|px|pc|in|mm|cm|%)?/);if(!i)return r;var s=parseFloat(i[1]||"1"),a=i[2];return e.UNITS.hasOwnProperty(a)?s*e.UNITS[a]/o/n:e.RELUNITS.hasOwnProperty(a)?s*e.RELUNITS[a]:"%"===a?s/100*r:s*r},e.percent=function(t){return(100*t).toFixed(1).replace(/\.?0+$/,"")+"%"},e.em=function(t){return Math.abs(t)<.001?"0":t.toFixed(3).replace(/\.?0+$/,"")+"em"},e.emRounded=function(t,e){return void 0===e&&(e=16),t=(Math.round(t*e)+.05)/e,Math.abs(t)<.001?"0em":t.toFixed(3).replace(/\.?0+$/,"")+"em"},e.px=function(t,r,n){return void 0===r&&(r=-e.BIGDIMEN),void 0===n&&(n=16),t*=n,r&&t0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return s},n=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r0)&&!(n=s.next()).done;)r.push(n.value)}catch(t){a={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(a)throw a.error}}return r};Object.defineProperty(e,"__esModule",{value:!0}),e.AsciiMath=void 0;var o=i(309),l=i(406),u=i(77),h=i(577),p=function(t){function e(i){var n=this,a=r(u.separateOptions(i,h.FindAsciiMath.OPTIONS,e.OPTIONS),3),s=a[1],o=a[2];return(n=t.call(this,o)||this).findAsciiMath=n.options.FindAsciiMath||new h.FindAsciiMath(s),n}return a(e,t),e.prototype.compile=function(t,e){return l.LegacyAsciiMath.Compile(t.math,t.display)},e.prototype.findMath=function(t){return this.findAsciiMath.findMath(t)},e.NAME="AsciiMath",e.OPTIONS=s(s({},o.AbstractInputJax.OPTIONS),{FindAsciiMath:null}),e}(o.AbstractInputJax);e.AsciiMath=p},577:function(t,e,i){"use strict";var n,a=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function i(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(i.prototype=e.prototype,new i)}),s=this&&this.__read||function(t,e){var i="function"==typeof Symbol&&t[Symbol.iterator];if(!i)return t;var n,a,s=i.call(t),r=[];try{for(;(void 0===e||e-- >0)&&!(n=s.next()).done;)r.push(n.value)}catch(t){a={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(a)throw a.error}}return r};Object.defineProperty(e,"__esModule",{value:!0}),e.FindAsciiMath=void 0;var r=i(649),o=i(720),l=i(769),u=function(t){function e(e){var i=t.call(this,e)||this;return i.getPatterns(),i}return a(e,t),e.prototype.getPatterns=function(){var t=this,e=this.options,i=[];this.end={},e.delimiters.forEach((function(e){return t.addPattern(i,e,!1)})),this.start=new RegExp(i.join("|"),"g"),this.hasPatterns=i.length>0},e.prototype.addPattern=function(t,e,i){var n=s(e,2),a=n[0],r=n[1];t.push(o.quotePattern(a)),this.end[a]=[r,i,new RegExp(o.quotePattern(r),"g")]},e.prototype.findEnd=function(t,e,i,n){var a=s(n,3),r=a[1],o=a[2],u=o.lastIndex=i.index+i[0].length,h=o.exec(t);return h?l.protoItem(i[0],t.substr(u,h.index-u),h[0],e,i.index,h.index+h[0].length,r):null},e.prototype.findMathInString=function(t,e,i){var n,a;for(this.start.lastIndex=0;n=this.start.exec(i);)(a=this.findEnd(i,e,n,this.end[n[0]]))&&(t.push(a),this.start.lastIndex=a.end.n)},e.prototype.findMath=function(t){var e=[];if(this.hasPatterns)for(var i=0,n=t.length;i1&&(t=2===arguments.length&&"function"!=typeof arguments[0]&&arguments[0]instanceof Object&&"number"==typeof arguments[1]?[].slice.call(t,e):[].slice.call(arguments,0)),t instanceof Array&&1===t.length&&(t=t[0]),"function"==typeof t)return t.execute===i.prototype.execute?t:i({hook:t});if(t instanceof Array){if("string"==typeof t[0]&&t[1]instanceof Object&&"function"==typeof t[1][t[0]])return i({hook:t[1][t[0]],object:t[1],data:t.slice(2)});if("function"==typeof t[0])return i({hook:t[0],data:t.slice(1)});if("function"==typeof t[1])return i({hook:t[1],object:t[0],data:t.slice(2)})}else{if("string"==typeof t)return s&&s(),i({hook:a,data:[t]});if(t instanceof Object)return i(t);if(void 0===t)return i({})}throw Error("Can't make callback from given data")},o=function(t,e){(t=r(t)).called||(c(t,e),e.pending++)},h=function(){var t=this.signal;delete this.signal,this.execute=this.oldExecute,delete this.oldExecute;var e=this.execute.apply(this,arguments);if(n(e)&&!e.called)c(e,t);else for(var i=0,a=t.length;i0&&e=0;t--)this.hooks.splice(t,1);this.remove=[]}}),f=e.Object.Subclass({Init:function(){this.pending=this.running=0,this.queue=[],this.Push.apply(this,arguments)},Push:function(){for(var t,e=0,i=arguments.length;e=this.timeout?(t(this.STATUS.ERROR),1):0},file:function(t,i){i<0?e.Ajax.loadTimeout(t):e.Ajax.loadComplete(t)},execute:function(){this.hook.call(this.object,this,this.data[0],this.data[1])},checkSafari2:function(t,e,i){t.time(i)||(p.styleSheets.length>e&&p.styleSheets[e].cssRules&&p.styleSheets[e].cssRules.length?i(t.STATUS.OK):setTimeout(t,t.delay))},checkLength:function(t,i,n){if(!t.time(n)){var a=0,s=i.sheet||i.styleSheet;try{(s.cssRules||s.rules||[]).length>0&&(a=1)}catch(t){(t.message.match(/protected variable|restricted URI/)||t.message.match(/Security error/))&&(a=1)}a?setTimeout(e.Callback([n,t.STATUS.OK]),0):setTimeout(t,t.delay)}}},loadComplete:function(t){t=this.fileURL(t);var i=this.loading[t];return i&&!i.preloaded?(e.Message.Clear(i.message),i.timeout&&clearTimeout(i.timeout),i.script&&(0===a.length&&setTimeout(s,0),a.push(i.script)),this.loaded[t]=i.status,delete this.loading[t],this.addHook(t,i.callback)):(i&&delete this.loading[t],this.loaded[t]=this.STATUS.OK,i={status:this.STATUS.OK}),this.loadHooks[t]?this.loadHooks[t].Execute(i.status):null},loadTimeout:function(t){this.loading[t].timeout&&clearTimeout(this.loading[t].timeout),this.loading[t].status=this.STATUS.ERROR,this.loadError(t),this.loadComplete(t)},loadError:function(t){e.Message.Set(["LoadFailed","File failed to load: %1",t],null,2e3),e.Hub.signal.Post(["file load error",t])},Styles:function(t,i){var n=this.StyleString(t);if(""===n)(i=e.Callback(i))();else{var a=p.createElement("style");a.type="text/css",this.head=(this.head,null),this.head.appendChild(a),a.styleSheet&&void 0!==a.styleSheet.cssText?a.styleSheet.cssText=n:a.appendChild(p.createTextNode(n)),i=this.timer.create.call(this,i,a)}return i},StyleString:function(t){if("string"==typeof t)return t;var e,i,n="";for(e in t)if(t.hasOwnProperty(e))if("string"==typeof t[e])n+=e+" {"+t[e]+"}\n";else if(t[e]instanceof Array)for(var a=0;a="0"&&r<="9")s[n]=e[s[n]-1],"number"==typeof s[n]&&(s[n]=this.number(s[n]));else if("{"===r)if((r=s[n].substr(1))>="0"&&r<="9")s[n]=e[s[n].substr(1,s[n].length-2)-1],"number"==typeof s[n]&&(s[n]=this.number(s[n]));else{var o=s[n].match(/^\{([a-z]+):%(\d+)\|(.*)\}$/);if(o)if("plural"===o[1]){var l=e[o[2]-1];if(void 0===l)s[n]="???";else{l=this.plural(l)-1;var u=o[3].replace(/(^|[^%])(%%)*%\|/g,"$1$2%\uefef").split(/\|/);l>=0&&l=3?i.push([s[0],s[1],this.processSnippet(t,s[2])]):i.push(e[n])}else i.push(e[n]);return i},markdownPattern:/(%.)|(\*{1,3})((?:%.|.)+?)\2|(`+)((?:%.|.)+?)\4|\[((?:%.|.)+?)\]\(([^\s\)]+)\)/,processMarkdown:function(t,e,i){for(var n,a=[],s=t.split(this.markdownPattern),r=s[0],o=1,l=s.length;o0||this.Get("scriptlevel")>0)&&n>=0?"":this.TEXSPACELENGTH[Math.abs(n)]},TEXSPACELENGTH:["",t.LENGTH.THINMATHSPACE,t.LENGTH.MEDIUMMATHSPACE,t.LENGTH.THICKMATHSPACE],TEXSPACE:[[0,-1,2,3,0,0,0,1],[-1,-1,0,3,0,0,0,1],[2,2,0,0,2,0,0,2],[3,3,0,0,3,0,0,3],[0,0,0,0,0,0,0,0],[0,-1,2,3,0,0,0,1],[1,1,0,1,1,1,1,1],[1,-1,2,3,1,0,1,1]],autoDefault:function(t){return""},isSpacelike:function(){return!1},isEmbellished:function(){return!1},Core:function(){return this},CoreMO:function(){return this},childIndex:function(t){if(null!=t)for(var e=0,i=this.data.length;e=55296&&i.charCodeAt(0)<56320?t.VARIANT.ITALIC:t.VARIANT.NORMAL}return""},setTeXclass:function(e){this.getPrevClass(e);var i=this.data.join("");return i.length>1&&i.match(/^[a-z][a-z0-9]*$/i)&&this.texClass===t.TEXCLASS.ORD&&(this.texClass=t.TEXCLASS.OP,this.autoOP=!0),this}}),t.mn=t.mbase.Subclass({type:"mn",isToken:!0,texClass:t.TEXCLASS.ORD,defaults:{mathvariant:t.INHERIT,mathsize:t.INHERIT,mathbackground:t.INHERIT,mathcolor:t.INHERIT,dir:t.INHERIT}}),t.mo=t.mbase.Subclass({type:"mo",isToken:!0,defaults:{mathvariant:t.INHERIT,mathsize:t.INHERIT,mathbackground:t.INHERIT,mathcolor:t.INHERIT,dir:t.INHERIT,form:t.AUTO,fence:t.AUTO,separator:t.AUTO,lspace:t.AUTO,rspace:t.AUTO,stretchy:t.AUTO,symmetric:t.AUTO,maxsize:t.AUTO,minsize:t.AUTO,largeop:t.AUTO,movablelimits:t.AUTO,accent:t.AUTO,linebreak:t.LINEBREAK.AUTO,lineleading:t.INHERIT,linebreakstyle:t.AUTO,linebreakmultchar:t.INHERIT,indentalign:t.INHERIT,indentshift:t.INHERIT,indenttarget:t.INHERIT,indentalignfirst:t.INHERIT,indentshiftfirst:t.INHERIT,indentalignlast:t.INHERIT,indentshiftlast:t.INHERIT,texClass:t.AUTO},defaultDef:{form:t.FORM.INFIX,fence:!1,separator:!1,lspace:t.LENGTH.THICKMATHSPACE,rspace:t.LENGTH.THICKMATHSPACE,stretchy:!1,symmetric:!1,maxsize:t.SIZE.INFINITY,minsize:"0em",largeop:!1,movablelimits:!1,accent:!1,linebreak:t.LINEBREAK.AUTO,lineleading:"1ex",linebreakstyle:"before",indentalign:t.INDENTALIGN.AUTO,indentshift:"0",indenttarget:"",indentalignfirst:t.INDENTALIGN.INDENTALIGN,indentshiftfirst:t.INDENTSHIFT.INDENTSHIFT,indentalignlast:t.INDENTALIGN.INDENTALIGN,indentshiftlast:t.INDENTSHIFT.INDENTSHIFT,texClass:t.TEXCLASS.REL},SPACE_ATTR:{lspace:1,rspace:2,form:4},useMMLspacing:7,autoDefault:function(e,i){var n=this.def;if(!n){if("form"===e)return this.useMMLspacing&=~this.SPACE_ATTR.form,this.getForm();for(var a=this.data.join(""),s=[this.Get("form"),t.FORM.INFIX,t.FORM.POSTFIX,t.FORM.PREFIX],r=0,o=s.length;r=55296&&i<56320&&(i=(i-55296<<10)+(e.charCodeAt(1)-56320)+65536);for(var n=0,a=this.RANGES.length;n=0;t--)if(this.data[0]&&!this.data[t].isSpacelike())return this.data[t];return null},Core:function(){return this.isEmbellished()&&void 0!==this.core?this.data[this.core]:this},CoreMO:function(){return this.isEmbellished()&&void 0!==this.core?this.data[this.core].CoreMO():this},toString:function(){return this.inferred?"["+this.data.join(",")+"]":this.SUPER(arguments).toString.call(this)},setTeXclass:function(e){var i,n=this.data.length;if(!this.open&&!this.close||e&&e.fnOP){for(i=0;i0)&&e++,e},adjustChild_texprimestyle:function(t){return t==this.den||this.Get("texprimestyle")},setTeXclass:t.mbase.setSeparateTeXclasses}),t.msqrt=t.mbase.Subclass({type:"msqrt",inferRow:!0,linebreakContainer:!0,texClass:t.TEXCLASS.ORD,setTeXclass:t.mbase.setSeparateTeXclasses,adjustChild_texprimestyle:function(t){return!0}}),t.mroot=t.mbase.Subclass({type:"mroot",linebreakContainer:!0,texClass:t.TEXCLASS.ORD,adjustChild_displaystyle:function(t){return 1!==t&&this.Get("displaystyle")},adjustChild_scriptlevel:function(t){var e=this.Get("scriptlevel");return 1===t&&(e+=2),e},adjustChild_texprimestyle:function(t){return 0===t||this.Get("texprimestyle")},setTeXclass:t.mbase.setSeparateTeXclasses}),t.mstyle=t.mbase.Subclass({type:"mstyle",isSpacelike:t.mbase.childrenSpacelike,isEmbellished:t.mbase.childEmbellished,Core:t.mbase.childCore,CoreMO:t.mbase.childCoreMO,inferRow:!0,defaults:{scriptlevel:t.INHERIT,displaystyle:t.INHERIT,scriptsizemultiplier:Math.sqrt(.5),scriptminsize:"8pt",mathbackground:t.INHERIT,mathcolor:t.INHERIT,dir:t.INHERIT,infixlinebreakstyle:t.LINEBREAKSTYLE.BEFORE,decimalseparator:"."},adjustChild_scriptlevel:function(t){var e=this.scriptlevel;if(null==e)e=this.Get("scriptlevel");else if(String(e).match(/^ *[-+]/)){e=this.Get("scriptlevel",null,!0)+parseInt(e)}return e},inheritFromMe:!0,noInherit:{mpadded:{width:!0,height:!0,depth:!0,lspace:!0,voffset:!0},mtable:{width:!0,height:!0,depth:!0,align:!0}},getRemoved:{fontfamily:"fontFamily",fontweight:"fontWeight",fontstyle:"fontStyle",fontsize:"fontSize"},setTeXclass:t.mbase.setChildTeXclass}),t.merror=t.mbase.Subclass({type:"merror",inferRow:!0,linebreakContainer:!0,texClass:t.TEXCLASS.ORD}),t.mpadded=t.mbase.Subclass({type:"mpadded",inferRow:!0,isSpacelike:t.mbase.childrenSpacelike,isEmbellished:t.mbase.childEmbellished,Core:t.mbase.childCore,CoreMO:t.mbase.childCoreMO,defaults:{mathbackground:t.INHERIT,mathcolor:t.INHERIT,width:"",height:"",depth:"",lspace:0,voffset:0},setTeXclass:t.mbase.setChildTeXclass}),t.mphantom=t.mbase.Subclass({type:"mphantom",texClass:t.TEXCLASS.ORD,inferRow:!0,isSpacelike:t.mbase.childrenSpacelike,isEmbellished:t.mbase.childEmbellished,Core:t.mbase.childCore,CoreMO:t.mbase.childCoreMO,setTeXclass:t.mbase.setChildTeXclass}),t.mfenced=t.mbase.Subclass({type:"mfenced",defaults:{mathbackground:t.INHERIT,mathcolor:t.INHERIT,open:"(",close:")",separators:","},addFakeNodes:function(){var e=this.getValues("open","close","separators");if(e.open=e.open.replace(/[ \t\n\r]/g,""),e.close=e.close.replace(/[ \t\n\r]/g,""),e.separators=e.separators.replace(/[ \t\n\r]/g,""),""!==e.open&&(this.SetData("open",t.mo(e.open).With({fence:!0,form:t.FORM.PREFIX,texClass:t.TEXCLASS.OPEN})),this.data.open.useMMLspacing=0),""!==e.separators){for(;e.separators.length0)&&this.Get("displaystyle")},adjustChild_scriptlevel:function(t){var e=this.Get("scriptlevel");return t>0&&e++,e},adjustChild_texprimestyle:function(t){return t===this.sub||this.Get("texprimestyle")},setTeXclass:t.mbase.setBaseTeXclasses}),t.msub=t.msubsup.Subclass({type:"msub"}),t.msup=t.msubsup.Subclass({type:"msup",sub:2,sup:1}),t.mmultiscripts=t.msubsup.Subclass({type:"mmultiscripts",adjustChild_texprimestyle:function(t){return t%2==1||this.Get("texprimestyle")}}),t.mprescripts=t.mbase.Subclass({type:"mprescripts"}),t.none=t.mbase.Subclass({type:"none"}),t.munderover=t.mbase.Subclass({type:"munderover",base:0,under:1,over:2,sub:1,sup:2,ACCENTS:["","accentunder","accent"],linebreakContainer:!0,isEmbellished:t.mbase.childEmbellished,Core:t.mbase.childCore,CoreMO:t.mbase.childCoreMO,defaults:{mathbackground:t.INHERIT,mathcolor:t.INHERIT,accent:t.AUTO,accentunder:t.AUTO,align:t.ALIGN.CENTER,texClass:t.AUTO,subscriptshift:"",superscriptshift:""},autoDefault:function(e){return"texClass"===e?this.isEmbellished()?this.CoreMO().Get(e):t.TEXCLASS.ORD:"accent"===e&&this.data[this.over]?this.data[this.over].CoreMO().Get("accent"):!("accentunder"!==e||!this.data[this.under])&&this.data[this.under].CoreMO().Get("accent")},adjustChild_displaystyle:function(t){return!(t>0)&&this.Get("displaystyle")},adjustChild_scriptlevel:function(t){var e=this.Get("scriptlevel"),i=this.data[this.base]&&!this.Get("displaystyle")&&this.data[this.base].CoreMO().Get("movablelimits");return t!=this.under||!i&&this.Get("accentunder")||e++,t!=this.over||!i&&this.Get("accent")||e++,e},adjustChild_texprimestyle:function(t){return!(t!==this.base||!this.data[this.over])||this.Get("texprimestyle")},setTeXclass:t.mbase.setBaseTeXclasses}),t.munder=t.munderover.Subclass({type:"munder"}),t.mover=t.munderover.Subclass({type:"mover",over:1,under:2,sup:1,sub:2,ACCENTS:["","accent","accentunder"]}),t.mtable=t.mbase.Subclass({type:"mtable",defaults:{mathbackground:t.INHERIT,mathcolor:t.INHERIT,align:t.ALIGN.AXIS,rowalign:t.ALIGN.BASELINE,columnalign:t.ALIGN.CENTER,groupalign:"{left}",alignmentscope:!0,columnwidth:t.WIDTH.AUTO,width:t.WIDTH.AUTO,rowspacing:"1ex",columnspacing:".8em",rowlines:t.LINES.NONE,columnlines:t.LINES.NONE,frame:t.LINES.NONE,framespacing:"0.4em 0.5ex",equalrows:!1,equalcolumns:!1,displaystyle:!1,side:t.SIDE.RIGHT,minlabelspacing:"0.8em",texClass:t.TEXCLASS.ORD,useHeight:1},adjustChild_displaystyle:function(){return null!=this.displaystyle?this.displaystyle:this.defaults.displaystyle},inheritFromMe:!0,noInherit:{mover:{align:!0},munder:{align:!0},munderover:{align:!0},mtable:{align:!0,rowalign:!0,columnalign:!0,groupalign:!0,alignmentscope:!0,columnwidth:!0,width:!0,rowspacing:!0,columnspacing:!0,rowlines:!0,columnlines:!0,frame:!0,framespacing:!0,equalrows:!0,equalcolumns:!0,displaystyle:!0,side:!0,minlabelspacing:!0,texClass:!0,useHeight:1}},linebreakContainer:!0,Append:function(){for(var e=0,i=arguments.length;e>10),56320+(1023&t)))}}),t.xml=t.mbase.Subclass({type:"xml",Init:function(){return this.div=document.createElement("div"),this.SUPER(arguments).Init.apply(this,arguments)},Append:function(){for(var t=0,e=arguments.length;t":i.REL,"?":[1,1,e.CLOSE],"\\":i.ORD,"^":i.ORD11,_:i.ORD11,"|":[2,2,e.ORD,{fence:!0,stretchy:!0,symmetric:!0}],"#":i.ORD,$:i.ORD,".":[0,3,e.PUNCT,{separator:!0}],"\u02b9":i.ORD,"\u0300":i.ACCENT,"\u0301":i.ACCENT,"\u0303":i.WIDEACCENT,"\u0304":i.ACCENT,"\u0306":i.ACCENT,"\u0307":i.ACCENT,"\u0308":i.ACCENT,"\u030c":i.ACCENT,"\u0332":i.WIDEACCENT,"\u0338":i.REL4,"\u2015":[0,0,e.ORD,{stretchy:!0}],"\u2017":[0,0,e.ORD,{stretchy:!0}],"\u2020":i.BIN3,"\u2021":i.BIN3,"\u20d7":i.ACCENT,"\u2111":i.ORD,"\u2113":i.ORD,"\u2118":i.ORD,"\u211c":i.ORD,"\u2205":i.ORD,"\u221e":i.ORD,"\u2305":i.BIN3,"\u2306":i.BIN3,"\u2322":i.REL4,"\u2323":i.REL4,"\u2329":i.OPEN,"\u232a":i.CLOSE,"\u23aa":i.ORD,"\u23af":[0,0,e.ORD,{stretchy:!0}],"\u23b0":i.OPEN,"\u23b1":i.CLOSE,"\u2500":i.ORD,"\u25ef":i.BIN3,"\u2660":i.ORD,"\u2661":i.ORD,"\u2662":i.ORD,"\u2663":i.ORD,"\u3008":i.OPEN,"\u3009":i.CLOSE,"\ufe37":i.WIDEACCENT,"\ufe38":i.WIDEACCENT}}},{OPTYPES:i});var n=t.mo.prototype.OPTABLE;n.infix["^"]=i.WIDEREL,n.infix._=i.WIDEREL,n.prefix["\u2223"]=i.OPEN,n.prefix["\u2225"]=i.OPEN,n.postfix["\u2223"]=i.CLOSE,n.postfix["\u2225"]=i.CLOSE}(MathJax.ElementJax.mml),MathJax.ElementJax.mml.loadComplete("jax.js")},315:function(){MathJax.InputJax.AsciiMath=MathJax.InputJax({id:"AsciiMath",version:"2.7.2",directory:MathJax.InputJax.directory+"/AsciiMath",extensionDir:MathJax.InputJax.extensionDir+"/AsciiMath",config:{fixphi:!0,useMathMLspacing:!0,displaystyle:!0,decimalsign:"."}}),MathJax.InputJax.AsciiMath.Register("math/asciimath"),MathJax.InputJax.AsciiMath.loadComplete("config.js")},247:function(){var t,e;!function(t){var e,i=MathJax.Object.Subclass({firstChild:null,lastChild:null,Init:function(){this.childNodes=[]},appendChild:function(t){return t.parent&&t.parent.removeChild(t),this.lastChild&&(this.lastChild.nextSibling=t),this.firstChild||(this.firstChild=t),this.childNodes.push(t),t.parent=this,this.lastChild=t,t},removeChild:function(t){for(var e=0,i=this.childNodes.length;e=n-1&&(this.lastChild=t),this.childNodes[i]=t,t.nextSibling=e.nextSibling,e.nextSibling=e.parent=null,e},hasChildNodes:function(t){return this.childNodes.length>0},toString:function(){return"{"+this.childNodes.join("")+"}"}}),n={getElementById:!0,createElementNS:function(i,n){var a=e[n]();return"mo"===n&&t.config.useMathMLspacing&&(a.useMMLspacing=128),a},createTextNode:function(t){return e.chars(t).With({nodeValue:t})},createDocumentFragment:function(){return i()}},a={appName:"MathJax"},s="blue",r=!0,o=!0,l=".",u="Microsoft"==a.appName.slice(0,9);function h(t){return u?n.createElement(t):n.createElementNS("http://www.w3.org/1999/xhtml",t)}var p="http://www.w3.org/1998/Math/MathML";function c(t){return u?n.createElement("m:"+t):n.createElementNS(p,t)}function d(t,e){var i;return i=u?n.createElement("m:"+t):n.createElementNS(p,t),e&&i.appendChild(e),i}var m=["\ud835\udc9c","\u212c","\ud835\udc9e","\ud835\udc9f","\u2130","\u2131","\ud835\udca2","\u210b","\u2110","\ud835\udca5","\ud835\udca6","\u2112","\u2133","\ud835\udca9","\ud835\udcaa","\ud835\udcab","\ud835\udcac","\u211b","\ud835\udcae","\ud835\udcaf","\ud835\udcb0","\ud835\udcb1","\ud835\udcb2","\ud835\udcb3","\ud835\udcb4","\ud835\udcb5","\ud835\udcb6","\ud835\udcb7","\ud835\udcb8","\ud835\udcb9","\u212f","\ud835\udcbb","\u210a","\ud835\udcbd","\ud835\udcbe","\ud835\udcbf","\ud835\udcc0","\ud835\udcc1","\ud835\udcc2","\ud835\udcc3","\u2134","\ud835\udcc5","\ud835\udcc6","\ud835\udcc7","\ud835\udcc8","\ud835\udcc9","\ud835\udcca","\ud835\udccb","\ud835\udccc","\ud835\udccd","\ud835\udcce","\ud835\udccf"],f=["\ud835\udd04","\ud835\udd05","\u212d","\ud835\udd07","\ud835\udd08","\ud835\udd09","\ud835\udd0a","\u210c","\u2111","\ud835\udd0d","\ud835\udd0e","\ud835\udd0f","\ud835\udd10","\ud835\udd11","\ud835\udd12","\ud835\udd13","\ud835\udd14","\u211c","\ud835\udd16","\ud835\udd17","\ud835\udd18","\ud835\udd19","\ud835\udd1a","\ud835\udd1b","\ud835\udd1c","\u2128","\ud835\udd1e","\ud835\udd1f","\ud835\udd20","\ud835\udd21","\ud835\udd22","\ud835\udd23","\ud835\udd24","\ud835\udd25","\ud835\udd26","\ud835\udd27","\ud835\udd28","\ud835\udd29","\ud835\udd2a","\ud835\udd2b","\ud835\udd2c","\ud835\udd2d","\ud835\udd2e","\ud835\udd2f","\ud835\udd30","\ud835\udd31","\ud835\udd32","\ud835\udd33","\ud835\udd34","\ud835\udd35","\ud835\udd36","\ud835\udd37"],g=["\ud835\udd38","\ud835\udd39","\u2102","\ud835\udd3b","\ud835\udd3c","\ud835\udd3d","\ud835\udd3e","\u210d","\ud835\udd40","\ud835\udd41","\ud835\udd42","\ud835\udd43","\ud835\udd44","\u2115","\ud835\udd46","\u2119","\u211a","\u211d","\ud835\udd4a","\ud835\udd4b","\ud835\udd4c","\ud835\udd4d","\ud835\udd4e","\ud835\udd4f","\ud835\udd50","\u2124","\ud835\udd52","\ud835\udd53","\ud835\udd54","\ud835\udd55","\ud835\udd56","\ud835\udd57","\ud835\udd58","\ud835\udd59","\ud835\udd5a","\ud835\udd5b","\ud835\udd5c","\ud835\udd5d","\ud835\udd5e","\ud835\udd5f","\ud835\udd60","\ud835\udd61","\ud835\udd62","\ud835\udd63","\ud835\udd64","\ud835\udd65","\ud835\udd66","\ud835\udd67","\ud835\udd68","\ud835\udd69","\ud835\udd6a","\ud835\udd6b"],y=8,E={input:'"',tag:"mtext",output:"mbox",tex:null,ttype:10},x=[{input:"alpha",tag:"mi",output:"\u03b1",tex:null,ttype:0},{input:"beta",tag:"mi",output:"\u03b2",tex:null,ttype:0},{input:"chi",tag:"mi",output:"\u03c7",tex:null,ttype:0},{input:"delta",tag:"mi",output:"\u03b4",tex:null,ttype:0},{input:"Delta",tag:"mo",output:"\u0394",tex:null,ttype:0},{input:"epsi",tag:"mi",output:"\u03b5",tex:"epsilon",ttype:0},{input:"varepsilon",tag:"mi",output:"\u025b",tex:null,ttype:0},{input:"eta",tag:"mi",output:"\u03b7",tex:null,ttype:0},{input:"gamma",tag:"mi",output:"\u03b3",tex:null,ttype:0},{input:"Gamma",tag:"mo",output:"\u0393",tex:null,ttype:0},{input:"iota",tag:"mi",output:"\u03b9",tex:null,ttype:0},{input:"kappa",tag:"mi",output:"\u03ba",tex:null,ttype:0},{input:"lambda",tag:"mi",output:"\u03bb",tex:null,ttype:0},{input:"Lambda",tag:"mo",output:"\u039b",tex:null,ttype:0},{input:"lamda",tag:"mi",output:"\u03bb",tex:null,ttype:0},{input:"Lamda",tag:"mo",output:"\u039b",tex:null,ttype:0},{input:"mu",tag:"mi",output:"\u03bc",tex:null,ttype:0},{input:"nu",tag:"mi",output:"\u03bd",tex:null,ttype:0},{input:"omega",tag:"mi",output:"\u03c9",tex:null,ttype:0},{input:"Omega",tag:"mo",output:"\u03a9",tex:null,ttype:0},{input:"phi",tag:"mi",output:"\u03d5",tex:null,ttype:0},{input:"varphi",tag:"mi",output:"\u03c6",tex:null,ttype:0},{input:"Phi",tag:"mo",output:"\u03a6",tex:null,ttype:0},{input:"pi",tag:"mi",output:"\u03c0",tex:null,ttype:0},{input:"Pi",tag:"mo",output:"\u03a0",tex:null,ttype:0},{input:"psi",tag:"mi",output:"\u03c8",tex:null,ttype:0},{input:"Psi",tag:"mi",output:"\u03a8",tex:null,ttype:0},{input:"rho",tag:"mi",output:"\u03c1",tex:null,ttype:0},{input:"sigma",tag:"mi",output:"\u03c3",tex:null,ttype:0},{input:"Sigma",tag:"mo",output:"\u03a3",tex:null,ttype:0},{input:"tau",tag:"mi",output:"\u03c4",tex:null,ttype:0},{input:"theta",tag:"mi",output:"\u03b8",tex:null,ttype:0},{input:"vartheta",tag:"mi",output:"\u03d1",tex:null,ttype:0},{input:"Theta",tag:"mo",output:"\u0398",tex:null,ttype:0},{input:"upsilon",tag:"mi",output:"\u03c5",tex:null,ttype:0},{input:"xi",tag:"mi",output:"\u03be",tex:null,ttype:0},{input:"Xi",tag:"mo",output:"\u039e",tex:null,ttype:0},{input:"zeta",tag:"mi",output:"\u03b6",tex:null,ttype:0},{input:"*",tag:"mo",output:"\u22c5",tex:"cdot",ttype:0},{input:"**",tag:"mo",output:"\u2217",tex:"ast",ttype:0},{input:"***",tag:"mo",output:"\u22c6",tex:"star",ttype:0},{input:"//",tag:"mo",output:"/",tex:null,ttype:0},{input:"\\\\",tag:"mo",output:"\\",tex:"backslash",ttype:0},{input:"setminus",tag:"mo",output:"\\",tex:null,ttype:0},{input:"xx",tag:"mo",output:"\xd7",tex:"times",ttype:0},{input:"|><",tag:"mo",output:"\u22c9",tex:"ltimes",ttype:0},{input:"><|",tag:"mo",output:"\u22ca",tex:"rtimes",ttype:0},{input:"|><|",tag:"mo",output:"\u22c8",tex:"bowtie",ttype:0},{input:"-:",tag:"mo",output:"\xf7",tex:"div",ttype:0},{input:"divide",tag:"mo",output:"-:",tex:null,ttype:y},{input:"@",tag:"mo",output:"\u2218",tex:"circ",ttype:0},{input:"o+",tag:"mo",output:"\u2295",tex:"oplus",ttype:0},{input:"ox",tag:"mo",output:"\u2297",tex:"otimes",ttype:0},{input:"o.",tag:"mo",output:"\u2299",tex:"odot",ttype:0},{input:"sum",tag:"mo",output:"\u2211",tex:null,ttype:7},{input:"prod",tag:"mo",output:"\u220f",tex:null,ttype:7},{input:"^^",tag:"mo",output:"\u2227",tex:"wedge",ttype:0},{input:"^^^",tag:"mo",output:"\u22c0",tex:"bigwedge",ttype:7},{input:"vv",tag:"mo",output:"\u2228",tex:"vee",ttype:0},{input:"vvv",tag:"mo",output:"\u22c1",tex:"bigvee",ttype:7},{input:"nn",tag:"mo",output:"\u2229",tex:"cap",ttype:0},{input:"nnn",tag:"mo",output:"\u22c2",tex:"bigcap",ttype:7},{input:"uu",tag:"mo",output:"\u222a",tex:"cup",ttype:0},{input:"uuu",tag:"mo",output:"\u22c3",tex:"bigcup",ttype:7},{input:"!=",tag:"mo",output:"\u2260",tex:"ne",ttype:0},{input:":=",tag:"mo",output:":=",tex:null,ttype:0},{input:"lt",tag:"mo",output:"<",tex:null,ttype:0},{input:"<=",tag:"mo",output:"\u2264",tex:"le",ttype:0},{input:"lt=",tag:"mo",output:"\u2264",tex:"leq",ttype:0},{input:"gt",tag:"mo",output:">",tex:null,ttype:0},{input:">=",tag:"mo",output:"\u2265",tex:"ge",ttype:0},{input:"gt=",tag:"mo",output:"\u2265",tex:"geq",ttype:0},{input:"-<",tag:"mo",output:"\u227a",tex:"prec",ttype:0},{input:"-lt",tag:"mo",output:"\u227a",tex:null,ttype:0},{input:">-",tag:"mo",output:"\u227b",tex:"succ",ttype:0},{input:"-<=",tag:"mo",output:"\u2aaf",tex:"preceq",ttype:0},{input:">-=",tag:"mo",output:"\u2ab0",tex:"succeq",ttype:0},{input:"in",tag:"mo",output:"\u2208",tex:null,ttype:0},{input:"!in",tag:"mo",output:"\u2209",tex:"notin",ttype:0},{input:"sub",tag:"mo",output:"\u2282",tex:"subset",ttype:0},{input:"sup",tag:"mo",output:"\u2283",tex:"supset",ttype:0},{input:"sube",tag:"mo",output:"\u2286",tex:"subseteq",ttype:0},{input:"supe",tag:"mo",output:"\u2287",tex:"supseteq",ttype:0},{input:"-=",tag:"mo",output:"\u2261",tex:"equiv",ttype:0},{input:"~=",tag:"mo",output:"\u2245",tex:"cong",ttype:0},{input:"~~",tag:"mo",output:"\u2248",tex:"approx",ttype:0},{input:"~",tag:"mo",output:"\u223c",tex:"sim",ttype:0},{input:"prop",tag:"mo",output:"\u221d",tex:"propto",ttype:0},{input:"and",tag:"mtext",output:"and",tex:null,ttype:6},{input:"or",tag:"mtext",output:"or",tex:null,ttype:6},{input:"not",tag:"mo",output:"\xac",tex:"neg",ttype:0},{input:"=>",tag:"mo",output:"\u21d2",tex:"implies",ttype:0},{input:"if",tag:"mo",output:"if",tex:null,ttype:6},{input:"<=>",tag:"mo",output:"\u21d4",tex:"iff",ttype:0},{input:"AA",tag:"mo",output:"\u2200",tex:"forall",ttype:0},{input:"EE",tag:"mo",output:"\u2203",tex:"exists",ttype:0},{input:"_|_",tag:"mo",output:"\u22a5",tex:"bot",ttype:0},{input:"TT",tag:"mo",output:"\u22a4",tex:"top",ttype:0},{input:"|--",tag:"mo",output:"\u22a2",tex:"vdash",ttype:0},{input:"|==",tag:"mo",output:"\u22a8",tex:"models",ttype:0},{input:"(",tag:"mo",output:"(",tex:"left(",ttype:4},{input:")",tag:"mo",output:")",tex:"right)",ttype:5},{input:"[",tag:"mo",output:"[",tex:"left[",ttype:4},{input:"]",tag:"mo",output:"]",tex:"right]",ttype:5},{input:"{",tag:"mo",output:"{",tex:null,ttype:4},{input:"}",tag:"mo",output:"}",tex:null,ttype:5},{input:"|",tag:"mo",output:"|",tex:null,ttype:9},{input:":|:",tag:"mo",output:"|",tex:null,ttype:0},{input:"|:",tag:"mo",output:"|",tex:null,ttype:4},{input:":|",tag:"mo",output:"|",tex:null,ttype:5},{input:"(:",tag:"mo",output:"\u2329",tex:"langle",ttype:4},{input:":)",tag:"mo",output:"\u232a",tex:"rangle",ttype:5},{input:"<<",tag:"mo",output:"\u2329",tex:null,ttype:4},{input:">>",tag:"mo",output:"\u232a",tex:null,ttype:5},{input:"{:",tag:"mo",output:"{:",tex:null,ttype:4,invisible:!0},{input:":}",tag:"mo",output:":}",tex:null,ttype:5,invisible:!0},{input:"int",tag:"mo",output:"\u222b",tex:null,ttype:0},{input:"dx",tag:"mi",output:"{:d x:}",tex:null,ttype:y},{input:"dy",tag:"mi",output:"{:d y:}",tex:null,ttype:y},{input:"dz",tag:"mi",output:"{:d z:}",tex:null,ttype:y},{input:"dt",tag:"mi",output:"{:d t:}",tex:null,ttype:y},{input:"oint",tag:"mo",output:"\u222e",tex:null,ttype:0},{input:"del",tag:"mo",output:"\u2202",tex:"partial",ttype:0},{input:"grad",tag:"mo",output:"\u2207",tex:"nabla",ttype:0},{input:"+-",tag:"mo",output:"\xb1",tex:"pm",ttype:0},{input:"-+",tag:"mo",output:"\u2213",tex:"mp",ttype:0},{input:"O/",tag:"mo",output:"\u2205",tex:"emptyset",ttype:0},{input:"oo",tag:"mo",output:"\u221e",tex:"infty",ttype:0},{input:"aleph",tag:"mo",output:"\u2135",tex:null,ttype:0},{input:"...",tag:"mo",output:"...",tex:"ldots",ttype:0},{input:":.",tag:"mo",output:"\u2234",tex:"therefore",ttype:0},{input:":'",tag:"mo",output:"\u2235",tex:"because",ttype:0},{input:"/_",tag:"mo",output:"\u2220",tex:"angle",ttype:0},{input:"/_\\",tag:"mo",output:"\u25b3",tex:"triangle",ttype:0},{input:"'",tag:"mo",output:"\u2032",tex:"prime",ttype:0},{input:"tilde",tag:"mover",output:"~",tex:null,ttype:1,acc:!0},{input:"\\ ",tag:"mo",output:"\xa0",tex:null,ttype:0},{input:"frown",tag:"mo",output:"\u2322",tex:null,ttype:0},{input:"quad",tag:"mo",output:"\xa0\xa0",tex:null,ttype:0},{input:"qquad",tag:"mo",output:"\xa0\xa0\xa0\xa0",tex:null,ttype:0},{input:"cdots",tag:"mo",output:"\u22ef",tex:null,ttype:0},{input:"vdots",tag:"mo",output:"\u22ee",tex:null,ttype:0},{input:"ddots",tag:"mo",output:"\u22f1",tex:null,ttype:0},{input:"diamond",tag:"mo",output:"\u22c4",tex:null,ttype:0},{input:"square",tag:"mo",output:"\u25a1",tex:null,ttype:0},{input:"|__",tag:"mo",output:"\u230a",tex:"lfloor",ttype:0},{input:"__|",tag:"mo",output:"\u230b",tex:"rfloor",ttype:0},{input:"|~",tag:"mo",output:"\u2308",tex:"lceiling",ttype:0},{input:"~|",tag:"mo",output:"\u2309",tex:"rceiling",ttype:0},{input:"CC",tag:"mo",output:"\u2102",tex:null,ttype:0},{input:"NN",tag:"mo",output:"\u2115",tex:null,ttype:0},{input:"QQ",tag:"mo",output:"\u211a",tex:null,ttype:0},{input:"RR",tag:"mo",output:"\u211d",tex:null,ttype:0},{input:"ZZ",tag:"mo",output:"\u2124",tex:null,ttype:0},{input:"f",tag:"mi",output:"f",tex:null,ttype:1,func:!0},{input:"g",tag:"mi",output:"g",tex:null,ttype:1,func:!0},{input:"lim",tag:"mo",output:"lim",tex:null,ttype:7},{input:"Lim",tag:"mo",output:"Lim",tex:null,ttype:7},{input:"sin",tag:"mo",output:"sin",tex:null,ttype:1,func:!0},{input:"cos",tag:"mo",output:"cos",tex:null,ttype:1,func:!0},{input:"tan",tag:"mo",output:"tan",tex:null,ttype:1,func:!0},{input:"sinh",tag:"mo",output:"sinh",tex:null,ttype:1,func:!0},{input:"cosh",tag:"mo",output:"cosh",tex:null,ttype:1,func:!0},{input:"tanh",tag:"mo",output:"tanh",tex:null,ttype:1,func:!0},{input:"cot",tag:"mo",output:"cot",tex:null,ttype:1,func:!0},{input:"sec",tag:"mo",output:"sec",tex:null,ttype:1,func:!0},{input:"csc",tag:"mo",output:"csc",tex:null,ttype:1,func:!0},{input:"arcsin",tag:"mo",output:"arcsin",tex:null,ttype:1,func:!0},{input:"arccos",tag:"mo",output:"arccos",tex:null,ttype:1,func:!0},{input:"arctan",tag:"mo",output:"arctan",tex:null,ttype:1,func:!0},{input:"coth",tag:"mo",output:"coth",tex:null,ttype:1,func:!0},{input:"sech",tag:"mo",output:"sech",tex:null,ttype:1,func:!0},{input:"csch",tag:"mo",output:"csch",tex:null,ttype:1,func:!0},{input:"exp",tag:"mo",output:"exp",tex:null,ttype:1,func:!0},{input:"abs",tag:"mo",output:"abs",tex:null,ttype:1,rewriteleftright:["|","|"]},{input:"norm",tag:"mo",output:"norm",tex:null,ttype:1,rewriteleftright:["\u2225","\u2225"]},{input:"floor",tag:"mo",output:"floor",tex:null,ttype:1,rewriteleftright:["\u230a","\u230b"]},{input:"ceil",tag:"mo",output:"ceil",tex:null,ttype:1,rewriteleftright:["\u2308","\u2309"]},{input:"log",tag:"mo",output:"log",tex:null,ttype:1,func:!0},{input:"ln",tag:"mo",output:"ln",tex:null,ttype:1,func:!0},{input:"det",tag:"mo",output:"det",tex:null,ttype:1,func:!0},{input:"dim",tag:"mo",output:"dim",tex:null,ttype:0},{input:"mod",tag:"mo",output:"mod",tex:null,ttype:0},{input:"gcd",tag:"mo",output:"gcd",tex:null,ttype:1,func:!0},{input:"lcm",tag:"mo",output:"lcm",tex:null,ttype:1,func:!0},{input:"lub",tag:"mo",output:"lub",tex:null,ttype:0},{input:"glb",tag:"mo",output:"glb",tex:null,ttype:0},{input:"min",tag:"mo",output:"min",tex:null,ttype:7},{input:"max",tag:"mo",output:"max",tex:null,ttype:7},{input:"Sin",tag:"mo",output:"Sin",tex:null,ttype:1,func:!0},{input:"Cos",tag:"mo",output:"Cos",tex:null,ttype:1,func:!0},{input:"Tan",tag:"mo",output:"Tan",tex:null,ttype:1,func:!0},{input:"Arcsin",tag:"mo",output:"Arcsin",tex:null,ttype:1,func:!0},{input:"Arccos",tag:"mo",output:"Arccos",tex:null,ttype:1,func:!0},{input:"Arctan",tag:"mo",output:"Arctan",tex:null,ttype:1,func:!0},{input:"Sinh",tag:"mo",output:"Sinh",tex:null,ttype:1,func:!0},{input:"Cosh",tag:"mo",output:"Cosh",tex:null,ttype:1,func:!0},{input:"Tanh",tag:"mo",output:"Tanh",tex:null,ttype:1,func:!0},{input:"Cot",tag:"mo",output:"Cot",tex:null,ttype:1,func:!0},{input:"Sec",tag:"mo",output:"Sec",tex:null,ttype:1,func:!0},{input:"Csc",tag:"mo",output:"Csc",tex:null,ttype:1,func:!0},{input:"Log",tag:"mo",output:"Log",tex:null,ttype:1,func:!0},{input:"Ln",tag:"mo",output:"Ln",tex:null,ttype:1,func:!0},{input:"Abs",tag:"mo",output:"abs",tex:null,ttype:1,notexcopy:!0,rewriteleftright:["|","|"]},{input:"uarr",tag:"mo",output:"\u2191",tex:"uparrow",ttype:0},{input:"darr",tag:"mo",output:"\u2193",tex:"downarrow",ttype:0},{input:"rarr",tag:"mo",output:"\u2192",tex:"rightarrow",ttype:0},{input:"->",tag:"mo",output:"\u2192",tex:"to",ttype:0},{input:">->",tag:"mo",output:"\u21a3",tex:"rightarrowtail",ttype:0},{input:"->>",tag:"mo",output:"\u21a0",tex:"twoheadrightarrow",ttype:0},{input:">->>",tag:"mo",output:"\u2916",tex:"twoheadrightarrowtail",ttype:0},{input:"|->",tag:"mo",output:"\u21a6",tex:"mapsto",ttype:0},{input:"larr",tag:"mo",output:"\u2190",tex:"leftarrow",ttype:0},{input:"harr",tag:"mo",output:"\u2194",tex:"leftrightarrow",ttype:0},{input:"rArr",tag:"mo",output:"\u21d2",tex:"Rightarrow",ttype:0},{input:"lArr",tag:"mo",output:"\u21d0",tex:"Leftarrow",ttype:0},{input:"hArr",tag:"mo",output:"\u21d4",tex:"Leftrightarrow",ttype:0},{input:"sqrt",tag:"msqrt",output:"sqrt",tex:null,ttype:1},{input:"root",tag:"mroot",output:"root",tex:null,ttype:2},{input:"frac",tag:"mfrac",output:"/",tex:null,ttype:2},{input:"/",tag:"mfrac",output:"/",tex:null,ttype:3},{input:"stackrel",tag:"mover",output:"stackrel",tex:null,ttype:2},{input:"overset",tag:"mover",output:"stackrel",tex:null,ttype:2},{input:"underset",tag:"munder",output:"stackrel",tex:null,ttype:2},{input:"_",tag:"msub",output:"_",tex:null,ttype:3},{input:"^",tag:"msup",output:"^",tex:null,ttype:3},{input:"hat",tag:"mover",output:"^",tex:null,ttype:1,acc:!0},{input:"bar",tag:"mover",output:"\xaf",tex:"overline",ttype:1,acc:!0},{input:"vec",tag:"mover",output:"\u2192",tex:null,ttype:1,acc:!0},{input:"dot",tag:"mover",output:".",tex:null,ttype:1,acc:!0},{input:"ddot",tag:"mover",output:"..",tex:null,ttype:1,acc:!0},{input:"overarc",tag:"mover",output:"\u23dc",tex:"overparen",ttype:1,acc:!0},{input:"ul",tag:"munder",output:"\u0332",tex:"underline",ttype:1,acc:!0},{input:"ubrace",tag:"munder",output:"\u23df",tex:"underbrace",ttype:15,acc:!0},{input:"obrace",tag:"mover",output:"\u23de",tex:"overbrace",ttype:15,acc:!0},{input:"text",tag:"mtext",output:"text",tex:null,ttype:10},{input:"mbox",tag:"mtext",output:"mbox",tex:null,ttype:10},{input:"color",tag:"mstyle",ttype:2},{input:"id",tag:"mrow",ttype:2},{input:"class",tag:"mrow",ttype:2},{input:"cancel",tag:"menclose",output:"cancel",tex:null,ttype:1},E,{input:"bb",tag:"mstyle",atname:"mathvariant",atval:"bold",output:"bb",tex:null,ttype:1},{input:"mathbf",tag:"mstyle",atname:"mathvariant",atval:"bold",output:"mathbf",tex:null,ttype:1},{input:"sf",tag:"mstyle",atname:"mathvariant",atval:"sans-serif",output:"sf",tex:null,ttype:1},{input:"mathsf",tag:"mstyle",atname:"mathvariant",atval:"sans-serif",output:"mathsf",tex:null,ttype:1},{input:"bbb",tag:"mstyle",atname:"mathvariant",atval:"double-struck",output:"bbb",tex:null,ttype:1,codes:g},{input:"mathbb",tag:"mstyle",atname:"mathvariant",atval:"double-struck",output:"mathbb",tex:null,ttype:1,codes:g},{input:"cc",tag:"mstyle",atname:"mathvariant",atval:"script",output:"cc",tex:null,ttype:1,codes:m},{input:"mathcal",tag:"mstyle",atname:"mathvariant",atval:"script",output:"mathcal",tex:null,ttype:1,codes:m},{input:"tt",tag:"mstyle",atname:"mathvariant",atval:"monospace",output:"tt",tex:null,ttype:1},{input:"mathtt",tag:"mstyle",atname:"mathvariant",atval:"monospace",output:"mathtt",tex:null,ttype:1},{input:"fr",tag:"mstyle",atname:"mathvariant",atval:"fraktur",output:"fr",tex:null,ttype:1,codes:f},{input:"mathfrak",tag:"mstyle",atname:"mathvariant",atval:"fraktur",output:"mathfrak",tex:null,ttype:1,codes:f}];function T(t,e){return t.input>e.input?1:-1}var C,b,S,I=[];function N(){var t,e=x.length;for(t=0;t>1]=I[a];if(b=S,""!=s)return S=x[e].ttype,x[e];S=0,a=1,i=t.slice(0,1);for(var u=!0;"0"<=i&&i<="9"&&a<=t.length;)i=t.slice(a,a+1),a++;if(i==l&&"0"<=(i=t.slice(a,a+1))&&i<="9")for(u=!1,a++;"0"<=i&&i<="9"&&a<=t.length;)i=t.slice(a,a+1),a++;return u&&a>1||a>2?(i=t.slice(0,a-1),n="mn"):(a=2,n=("A">(i=t.slice(0,1))||i>"Z")&&("a">i||i>"z")?"mo":"mi"),"-"==i&&" "!==t.charAt(1)&&3==b?(S=3,{input:i,tag:n,output:i,ttype:1,func:!0}):{input:i,tag:n,output:i,ttype:0}}function L(t){var e;t.hasChildNodes()&&(!t.firstChild.hasChildNodes()||"mrow"!=t.nodeName&&"M:MROW"!=t.nodeName||"("!=(e=t.firstChild.firstChild.nodeValue)&&"["!=e&&"{"!=e||t.removeChild(t.firstChild),!t.lastChild.hasChildNodes()||"mrow"!=t.nodeName&&"M:MROW"!=t.nodeName||")"!=(e=t.lastChild.firstChild.nodeValue)&&"]"!=e&&"}"!=e||t.removeChild(t.lastChild))}function M(t){var e,i,a,s,r,o=n.createDocumentFragment();if(null==(e=O(t=v(t,0)))||5==e.ttype&&C>0)return[null,t];switch(e.ttype==y&&(e=O(t=e.output+v(t,e.input.length))),e.ttype){case 7:case 0:return t=v(t,e.input.length),[d(e.tag,n.createTextNode(e.output)),t];case 4:return C++,a=k(t=v(t,e.input.length),!0),C--,"boolean"==typeof e.invisible&&e.invisible?i=d("mrow",a[0]):(i=d("mo",n.createTextNode(e.output)),(i=d("mrow",i)).appendChild(a[0])),[i,a[1]];case 10:return e!=E&&(t=v(t,e.input.length)),-1==(s="{"==t.charAt(0)?t.indexOf("}"):"("==t.charAt(0)?t.indexOf(")"):"["==t.charAt(0)?t.indexOf("]"):e==E?t.slice(1).indexOf('"')+1:0)&&(s=t.length)," "==(r=t.slice(1,s)).charAt(0)&&((i=d("mspace")).setAttribute("width","1ex"),o.appendChild(i)),o.appendChild(d(e.tag,n.createTextNode(r)))," "==r.charAt(r.length-1)&&((i=d("mspace")).setAttribute("width","1ex"),o.appendChild(i)),t=v(t,s+1),[d("mrow",o),t];case 15:case 1:if(null==(a=M(t=v(t,e.input.length)))[0])return[d(e.tag,n.createTextNode(e.output)),t];if("boolean"==typeof e.func&&e.func)return"^"==(r=t.charAt(0))||"_"==r||"/"==r||"|"==r||","==r||1==e.input.length&&e.input.match(/\w/)&&"("!=r?[d(e.tag,n.createTextNode(e.output)),t]:((i=d("mrow",d(e.tag,n.createTextNode(e.output)))).appendChild(a[0]),[i,a[1]]);if(L(a[0]),"sqrt"==e.input)return[d(e.tag,a[0]),a[1]];if(void 0!==e.rewriteleftright)return(i=d("mrow",d("mo",n.createTextNode(e.rewriteleftright[0])))).appendChild(a[0]),i.appendChild(d("mo",n.createTextNode(e.rewriteleftright[1]))),[i,a[1]];if("cancel"==e.input)return(i=d(e.tag,a[0])).setAttribute("notation","updiagonalstrike"),[i,a[1]];if("boolean"==typeof e.acc&&e.acc){i=d(e.tag,a[0]);var l=d("mo",n.createTextNode(e.output));return"vec"==e.input&&("mrow"==a[0].nodeName&&1==a[0].childNodes.length&&null!==a[0].firstChild.firstChild.nodeValue&&1==a[0].firstChild.firstChild.nodeValue.length||null!==a[0].firstChild.nodeValue&&1==a[0].firstChild.nodeValue.length)&&l.setAttribute("stretchy",!1),i.appendChild(l),[i,a[1]]}if(!u&&void 0!==e.codes)for(s=0;s64&&r.charCodeAt(p)<91?h+=e.codes[r.charCodeAt(p)-65]:r.charCodeAt(p)>96&&r.charCodeAt(p)<123?h+=e.codes[r.charCodeAt(p)-71]:h+=r.charAt(p);"mi"==a[0].nodeName?a[0]=d("mo").appendChild(n.createTextNode(h)):a[0].replaceChild(d("mo").appendChild(n.createTextNode(h)),a[0].childNodes[s])}return(i=d(e.tag,a[0])).setAttribute(e.atname,e.atval),[i,a[1]];case 2:if(null==(a=M(t=v(t,e.input.length)))[0])return[d("mo",n.createTextNode(e.input)),t];L(a[0]);var c=M(a[1]);return null==c[0]?[d("mo",n.createTextNode(e.input)),t]:(L(c[0]),["color","class","id"].indexOf(e.input)>=0?("{"==t.charAt(0)?s=t.indexOf("}"):"("==t.charAt(0)?s=t.indexOf(")"):"["==t.charAt(0)&&(s=t.indexOf("]")),r=t.slice(1,s),i=d(e.tag,c[0]),"color"===e.input?i.setAttribute("mathcolor",r):"class"===e.input?i.setAttribute("class",r):"id"===e.input&&i.setAttribute("id",r),[i,c[1]]):("root"!=e.input&&"stackrel"!=e.output||o.appendChild(c[0]),o.appendChild(a[0]),"frac"==e.input&&o.appendChild(c[0]),[d(e.tag,o),c[1]]));case 3:return t=v(t,e.input.length),[d("mo",n.createTextNode(e.output)),t];case 6:return t=v(t,e.input.length),(i=d("mspace")).setAttribute("width","1ex"),o.appendChild(i),o.appendChild(d(e.tag,n.createTextNode(e.output))),(i=d("mspace")).setAttribute("width","1ex"),o.appendChild(i),[d("mrow",o),t];case 9:return C++,a=k(t=v(t,e.input.length),!1),C--,r="",null!=a[0].lastChild&&(r=a[0].lastChild.firstChild.nodeValue),"|"==r&&","!==t.charAt(0)?(i=d("mo",n.createTextNode(e.output)),(i=d("mrow",i)).appendChild(a[0]),[i,a[1]]):(i=d("mo",n.createTextNode("\u2223")),[i=d("mrow",i),t]);default:return t=v(t,e.input.length),[d(e.tag,n.createTextNode(e.output)),t]}}function D(t){var e,i,a,s,r,o;if(i=O(t=v(t,0)),s=(r=M(t))[0],3==(e=O(t=r[1])).ttype&&"/"!=e.input){if(null==(r=M(t=v(t,e.input.length)))[0]?r[0]=d("mo",n.createTextNode("\u25a1")):L(r[0]),t=r[1],o=7==i.ttype||15==i.ttype,"_"==e.input)if("^"==(a=O(t)).input){var l=M(t=v(t,a.input.length));L(l[0]),t=l[1],(s=d(o?"munderover":"msubsup",s)).appendChild(r[0]),s.appendChild(l[0]),s=d("mrow",s)}else(s=d(o?"munder":"msub",s)).appendChild(r[0]);else"^"==e.input&&o?(s=d("mover",s)).appendChild(r[0]):(s=d(e.tag,s)).appendChild(r[0]);void 0!==i.func&&i.func&&3!=(a=O(t)).ttype&&5!=a.ttype&&(i.input.length>1||4==a.ttype)&&(r=D(t),(s=d("mrow",s)).appendChild(r[0]),t=r[1])}return[s,t]}function k(t,e){var i,a,s,r,o=n.createDocumentFragment();do{a=(s=D(t=v(t,0)))[0],3==(i=O(t=s[1])).ttype&&"/"==i.input?(null==(s=D(t=v(t,i.input.length)))[0]?s[0]=d("mo",n.createTextNode("\u25a1")):L(s[0]),t=s[1],L(a),(a=d(i.tag,a)).appendChild(s[0]),o.appendChild(a),i=O(t)):null!=a&&o.appendChild(a)}while((5!=i.ttype&&(9!=i.ttype||e)||0==C)&&null!=i&&""!=i.output);if(5==i.ttype||9==i.ttype){var l=o.childNodes.length;if(l>0&&"mrow"==o.childNodes[l-1].nodeName&&o.childNodes[l-1].lastChild&&o.childNodes[l-1].lastChild.firstChild){var u=o.childNodes[l-1].lastChild.firstChild.nodeValue;if(")"==u||"]"==u){var h=o.childNodes[l-1].firstChild.firstChild.nodeValue;if("("==h&&")"==u&&"}"!=i.output||"["==h&&"]"==u){var p=[],c=!0,m=o.childNodes.length;for(r=0;c&&r1&&(c=p[r].length==p[r-2].length)}var g=[];if(c=c&&(p.length>1||p[0].length>0)){var y,E,x,T,b=n.createDocumentFragment();for(r=0;r2&&(o.removeChild(o.firstChild),o.removeChild(o.firstChild)),b.appendChild(d("mtr",y))}(a=d("mtable",b)).setAttribute("columnlines",g.join(" ")),"boolean"==typeof i.invisible&&i.invisible&&a.setAttribute("columnalign","left"),o.replaceChild(a,o.firstChild)}}}}t=v(t,i.input.length),"boolean"==typeof i.invisible&&i.invisible||(a=d("mo",n.createTextNode(i.output)),o.appendChild(a))}return[o,t]}function P(t,e){var i;return C=0,i=d("mstyle",k((t=(t=(t=t.replace(/ /g,"")).replace(/>/g,">")).replace(/</g,"<")).replace(/^\s+/g,""),!1)[0]),""!=s&&i.setAttribute("mathcolor",s),""!=mathfontsize&&(i.setAttribute("fontsize",mathfontsize),i.setAttribute("mathsize",mathfontsize)),""!=mathfontfamily&&(i.setAttribute("fontfamily",mathfontfamily),i.setAttribute("mathvariant",mathfontfamily)),r&&i.setAttribute("displaystyle","true"),i=d("math",i),o&&i.setAttribute("title",t.replace(/\s+/g," ")),i}o=!1,mathfontfamily="",s="",mathfontsize="",function(){for(var t=0,e=x.length;t=n-1&&(this.lastChild=t),this.SetData(i,t),t.nextSibling=e.nextSibling,e.nextSibling=e.parent=null,e},hasChildNodes:function(t){return this.childNodes.length>0},setAttribute:function(t,e){this[t]=e}}),N()},Augment:function(t){for(var e in t)if(t.hasOwnProperty(e)){switch(e){case"displaystyle":r=t[e];break;case"decimal":decimal=t[e];break;case"parseMath":P=t[e];break;case"parseExpr":k=t[e];break;case"parseIexpr":D=t[e];break;case"parseSexpr":M=t[e];break;case"removeBrackets":L=t[e];break;case"getSymbol":O=t[e];break;case"position":R=t[e];break;case"removeCharsAndBlanks":v=t[e];break;case"createMmlNode":d=t[e];break;case"createElementMathML":c=t[e];break;case"createElementXHTML":h=t[e];break;case"initSymbols":N=t[e];break;case"refreshSymbols":A=t[e];break;case"compareNames":T=t[e]}this[e]=t[e]}},parseMath:P,parseExpr:k,parseIexpr:D,parseSexr:M,removeBrackets:L,getSymbol:O,position:R,removeCharsAndBlanks:v,createMmlNode:d,createElementMathML:c,createElementXHTML:h,initSymbols:N,refreshSymbols:A,compareNames:T,createDocumentFragment:i,document:n,define:function(t,e){x.push({input:t,tag:"mo",output:e,tex:null,ttype:y}),A()},newcommand:function(t,e){x.push({input:t,tag:"mo",output:e,tex:null,ttype:y}),A()},newsymbol:function(t){x.push(t),A()},symbols:x,names:I,TOKEN:{CONST:0,UNARY:1,BINARY:2,INFIX:3,LEFTBRACKET:4,RIGHTBRACKET:5,SPACE:6,UNDEROVER:7,DEFINITION:y,LEFTRIGHT:9,TEXT:10,UNARYUNDEROVER:15}}})}(MathJax.InputJax.AsciiMath),(t=MathJax.InputJax.AsciiMath).Augment({sourceMenuTitle:["AsciiMathInput","AsciiMath Input"],annotationEncoding:"text/x-asciimath",prefilterHooks:MathJax.Callback.Hooks(!0),postfilterHooks:MathJax.Callback.Hooks(!0),Translate:function(t){var i,n=MathJax.HTML.getScript(t),a={math:n,script:t},s=this.prefilterHooks.Execute(a);if(s)return s;n=a.math;try{i=this.AM.parseMath(n)}catch(t){if(!t.asciimathError)throw t;i=this.formatError(t,n)}return a.math=e(i),this.postfilterHooks.Execute(a),this.postfilterHooks.Execute(a)||a.math},formatError:function(t,i,n){var a=t.message.replace(/\n.*/,"");return MathJax.Hub.signal.Post(["AsciiMath Jax - parse error",a,i,n]),e.Error(a)},Error:function(t){throw MathJax.Hub.Insert(Error(t),{asciimathError:!0})},Startup:function(){e=MathJax.ElementJax.mml,this.AM.Init()}}),t.loadComplete("jax.js")},723:function(t,e){"use strict";MathJax._.components.global.isObject,MathJax._.components.global.combineConfig,MathJax._.components.global.combineDefaults,e.r8=MathJax._.components.global.combineWithMathJax,MathJax._.components.global.MathJax},649:function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractFindMath=MathJax._.core.FindMath.AbstractFindMath},309:function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractInputJax=MathJax._.core.InputJax.AbstractInputJax},769:function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.protoItem=MathJax._.core.MathItem.protoItem,e.AbstractMathItem=MathJax._.core.MathItem.AbstractMathItem,e.STATE=MathJax._.core.MathItem.STATE,e.newState=MathJax._.core.MathItem.newState},806:function(t,e){"use strict";e.g=MathJax._.core.MmlTree.MmlFactory.MmlFactory},77:function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.APPEND=MathJax._.util.Options.APPEND,e.REMOVE=MathJax._.util.Options.REMOVE,e.Expandable=MathJax._.util.Options.Expandable,e.expandable=MathJax._.util.Options.expandable,e.makeArray=MathJax._.util.Options.makeArray,e.keys=MathJax._.util.Options.keys,e.copy=MathJax._.util.Options.copy,e.insert=MathJax._.util.Options.insert,e.defaultOptions=MathJax._.util.Options.defaultOptions,e.userOptions=MathJax._.util.Options.userOptions,e.selectOptions=MathJax._.util.Options.selectOptions,e.selectOptionsFromKeys=MathJax._.util.Options.selectOptionsFromKeys,e.separateOptions=MathJax._.util.Options.separateOptions},720:function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.sortLength=MathJax._.util.string.sortLength,e.quotePattern=MathJax._.util.string.quotePattern,e.unicodeChars=MathJax._.util.string.unicodeChars,e.unicodeString=MathJax._.util.string.unicodeString,e.isPercent=MathJax._.util.string.isPercent,e.split=MathJax._.util.string.split}},e={};function i(n){var a=e[n];if(void 0!==a)return a.exports;var s=e[n]={exports:{}};return t[n].call(s.exports,s,s.exports,i),s.exports}i.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),function(){"use strict";var t=i(723),e=i(884),n=i(577);(0,t.r8)({_:{input:{asciimath_ts:e,asciimath:{FindAsciiMath:n}}}}),MathJax.startup&&(MathJax.startup.registerConstructor("asciimath",e.AsciiMath),MathJax.startup.useInput("asciimath"))}()}(); \ No newline at end of file diff --git a/docs/assets/vendor/mathjax/input/mml.js b/docs/assets/vendor/mathjax/input/mml.js new file mode 100644 index 0000000..077b42b --- /dev/null +++ b/docs/assets/vendor/mathjax/input/mml.js @@ -0,0 +1 @@ +!function(){"use strict";var t,e,r,a,o={236:function(t,e,r){var a,o=this&&this.__extends||(a=function(t,e){return(a=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}a(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var a,o,i=r.call(t),n=[];try{for(;(void 0===e||e-- >0)&&!(a=i.next()).done;)n.push(a.value)}catch(t){o={error:t}}finally{try{a&&!a.done&&(r=i.return)&&r.call(i)}finally{if(o)throw o.error}}return n};Object.defineProperty(e,"__esModule",{value:!0}),e.MathML=void 0;var n=r(309),s=r(77),l=r(898),h=r(794),p=r(332),c=function(t){function e(e){void 0===e&&(e={});var r=this,a=i(s.separateOptions(e,h.FindMathML.OPTIONS,p.MathMLCompile.OPTIONS),3),o=a[0],n=a[1],c=a[2];return(r=t.call(this,o)||this).findMathML=r.options.FindMathML||new h.FindMathML(n),r.mathml=r.options.MathMLCompile||new p.MathMLCompile(c),r.mmlFilters=new l.FunctionList,r}return o(e,t),e.prototype.setAdaptor=function(e){t.prototype.setAdaptor.call(this,e),this.findMathML.adaptor=e,this.mathml.adaptor=e},e.prototype.setMmlFactory=function(e){t.prototype.setMmlFactory.call(this,e),this.mathml.setMmlFactory(e)},Object.defineProperty(e.prototype,"processStrings",{get:function(){return!1},enumerable:!1,configurable:!0}),e.prototype.compile=function(t,e){var r=t.start.node;if(!r||!t.end.node||this.options.forceReparse||"#text"===this.adaptor.kind(r)){var a=this.executeFilters(this.preFilters,t,e,t.math||""),o=this.checkForErrors(this.adaptor.parse(a,"text/"+this.options.parseAs)),i=this.adaptor.body(o);1!==this.adaptor.childNodes(i).length&&this.error("MathML must consist of a single element"),r=this.adaptor.remove(this.adaptor.firstChild(i)),"math"!==this.adaptor.kind(r).replace(/^[a-z]+:/,"")&&this.error("MathML must be formed by a element, not <"+this.adaptor.kind(r)+">")}return r=this.executeFilters(this.mmlFilters,t,e,r),this.executeFilters(this.postFilters,t,e,this.mathml.compile(r))},e.prototype.checkForErrors=function(t){var e=this.adaptor.tags(this.adaptor.body(t),"parsererror")[0];return e&&(""===this.adaptor.textContent(e)&&this.error("Error processing MathML"),this.options.parseError.call(this,e)),t},e.prototype.error=function(t){throw new Error(t)},e.prototype.findMath=function(t){return this.findMathML.findMath(t)},e.NAME="MathML",e.OPTIONS=s.defaultOptions({parseAs:"html",forceReparse:!1,FindMathML:null,MathMLCompile:null,parseError:function(t){this.error(this.adaptor.textContent(t).replace(/\n.*/g,""))}},n.AbstractInputJax.OPTIONS),e}(n.AbstractInputJax);e.MathML=c},794:function(t,e,r){var a,o=this&&this.__extends||(a=function(t,e){return(a=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}a(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],a=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&a>=t.length&&(t=void 0),{value:t&&t[a++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.FindMathML=void 0;var n=r(649),s="http://www.w3.org/1998/Math/MathML",l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.findMath=function(t){var e=new Set;this.findMathNodes(t,e),this.findMathPrefixed(t,e);var r=this.adaptor.root(this.adaptor.document);return"html"===this.adaptor.kind(r)&&0===e.size&&this.findMathNS(t,e),this.processMath(e)},e.prototype.findMathNodes=function(t,e){var r,a;try{for(var o=i(this.adaptor.tags(t,"math")),n=o.next();!n.done;n=o.next()){var s=n.value;e.add(s)}}catch(t){r={error:t}}finally{try{n&&!n.done&&(a=o.return)&&a.call(o)}finally{if(r)throw r.error}}},e.prototype.findMathPrefixed=function(t,e){var r,a,o,n,l=this.adaptor.root(this.adaptor.document);try{for(var h=i(this.adaptor.allAttributes(l)),p=h.next();!p.done;p=h.next()){var c=p.value;if("xmlns:"===c.name.substr(0,6)&&c.value===s){var u=c.name.substr(6);try{for(var d=(o=void 0,i(this.adaptor.tags(t,u+":math"))),f=d.next();!f.done;f=d.next()){var M=f.value;e.add(M)}}catch(t){o={error:t}}finally{try{f&&!f.done&&(n=d.return)&&n.call(d)}finally{if(o)throw o.error}}}}}catch(t){r={error:t}}finally{try{p&&!p.done&&(a=h.return)&&a.call(h)}finally{if(r)throw r.error}}},e.prototype.findMathNS=function(t,e){var r,a;try{for(var o=i(this.adaptor.tags(t,"math",s)),n=o.next();!n.done;n=o.next()){var l=n.value;e.add(l)}}catch(t){r={error:t}}finally{try{n&&!n.done&&(a=o.return)&&a.call(o)}finally{if(r)throw r.error}}},e.prototype.processMath=function(t){var e,r,a=[];try{for(var o=i(Array.from(t)),n=o.next();!n.done;n=o.next()){var s=n.value,l="block"===this.adaptor.getAttribute(s,"display")||"display"===this.adaptor.getAttribute(s,"mode"),h={node:s,n:0,delim:""},p={node:s,n:0,delim:""};a.push({math:this.adaptor.outerHTML(s),start:h,end:p,display:l})}}catch(t){e={error:t}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(e)throw e.error}}return a},e.OPTIONS={},e}(n.AbstractFindMath);e.FindMathML=l},332:function(t,e,r){var a=this&&this.__assign||function(){return(a=Object.assign||function(t){for(var e,r=1,a=arguments.length;r=t.length&&(t=void 0),{value:t&&t[a++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.MathMLCompile=void 0;var i=r(921),n=r(77),s=r(29),l=function(){function t(t){void 0===t&&(t={});var e=this.constructor;this.options=n.userOptions(n.defaultOptions({},e.OPTIONS),t)}return t.prototype.setMmlFactory=function(t){this.factory=t},t.prototype.compile=function(t){var e=this.makeNode(t);return e.verifyTree(this.options.verify),e.setInheritedAttributes({},!1,0,!1),e.walkTree(this.markMrows),e},t.prototype.makeNode=function(t){var e,r,a=this.adaptor,n=!1,s=a.kind(t).replace(/^.*:/,""),l=a.getAttribute(t,"data-mjx-texclass")||"";l&&(l=this.filterAttribute("data-mjx-texclass",l)||"");var h=l&&"mrow"===s?"TeXAtom":s;try{for(var p=o(this.filterClassList(a.allClasses(t))),c=p.next();!c.done;c=p.next()){var u=c.value;u.match(/^MJX-TeXAtom-/)?(l=u.substr(12),h="TeXAtom"):"MJX-fixedlimits"===u&&(n=!0)}}catch(t){e={error:t}}finally{try{c&&!c.done&&(r=p.return)&&r.call(p)}finally{if(e)throw e.error}}this.factory.getNodeClass(h)||this.error('Unknown node type "'+h+'"');var d=this.factory.create(h);return"TeXAtom"!==h||"OP"!==l||n||(d.setProperty("movesupsub",!0),d.attributes.setInherited("movablelimits",!0)),l&&(d.texClass=i.TEXCLASS[l],d.setProperty("texClass",d.texClass)),this.addAttributes(d,t),this.checkClass(d,t),this.addChildren(d,t),d},t.prototype.addAttributes=function(t,e){var r,a,i=!1;try{for(var n=o(this.adaptor.allAttributes(e)),s=n.next();!s.done;s=n.next()){var l=s.value,h=l.name,p=this.filterAttribute(h,l.value);if(null!==p&&"xmlns"!==h)if("data-mjx-"===h.substr(0,9))"data-mjx-alternate"===h?t.setProperty("variantForm",!0):"data-mjx-variant"===h?(t.attributes.set("mathvariant",p),i=!0):"data-mjx-smallmatrix"===h?(t.setProperty("scriptlevel",1),t.setProperty("useHeight",!1)):"data-mjx-accent"===h?t.setProperty("mathaccent","true"===p):"data-mjx-auto-op"===h&&t.setProperty("autoOP","true"===p);else if("class"!==h){var c=p.toLowerCase();"true"===c||"false"===c?t.attributes.set(h,"true"===c):i&&"mathvariant"===h||t.attributes.set(h,p)}}}catch(t){r={error:t}}finally{try{s&&!s.done&&(a=n.return)&&a.call(n)}finally{if(r)throw r.error}}},t.prototype.filterAttribute=function(t,e){return e},t.prototype.filterClassList=function(t){return t},t.prototype.addChildren=function(t,e){var r,a;if(0!==t.arity){var i=this.adaptor;try{for(var n=o(i.childNodes(e)),s=n.next();!s.done;s=n.next()){var l=s.value,h=i.kind(l);if("#comment"!==h)if("#text"===h)this.addText(t,l);else if(t.isKind("annotation-xml"))t.appendChild(this.factory.create("XML").setXML(l,i));else{var p=t.appendChild(this.makeNode(l));0===p.arity&&i.childNodes(l).length&&(this.options.fixMisplacedChildren?this.addChildren(t,l):p.mError("There should not be children for "+p.kind+" nodes",this.options.verify,!0))}}}catch(t){r={error:t}}finally{try{s&&!s.done&&(a=n.return)&&a.call(n)}finally{if(r)throw r.error}}}},t.prototype.addText=function(t,e){var r=this.adaptor.value(e);(t.isToken||t.getProperty("isChars"))&&t.arity?(t.isToken&&(r=s.translate(r),r=this.trimSpace(r)),t.appendChild(this.factory.create("text").setText(r))):r.match(/\S/)&&this.error('Unexpected text node "'+r+'"')},t.prototype.checkClass=function(t,e){var r,a,i=[];try{for(var n=o(this.filterClassList(this.adaptor.allClasses(e))),s=n.next();!s.done;s=n.next()){var l=s.value;"MJX-"===l.substr(0,4)?"MJX-variant"===l?t.setProperty("variantForm",!0):"MJX-TeXAtom"!==l.substr(0,11)&&t.attributes.set("mathvariant",this.fixCalligraphic(l.substr(3))):i.push(l)}}catch(t){r={error:t}}finally{try{s&&!s.done&&(a=n.return)&&a.call(n)}finally{if(r)throw r.error}}i.length&&t.attributes.set("class",i.join(" "))},t.prototype.fixCalligraphic=function(t){return t.replace(/caligraphic/,"calligraphic")},t.prototype.markMrows=function(t){if(t.isKind("mrow")&&!t.isInferred&&t.childNodes.length>=2){var e=t.childNodes[0],r=t.childNodes[t.childNodes.length-1];e.isKind("mo")&&e.attributes.get("fence")&&e.attributes.get("stretchy")&&r.isKind("mo")&&r.attributes.get("fence")&&r.attributes.get("stretchy")&&(e.childNodes.length&&t.setProperty("open",e.getText()),r.childNodes.length&&t.setProperty("close",r.getText()))}},t.prototype.trimSpace=function(t){return t.replace(/[\t\n\r]/g," ").replace(/^ +/,"").replace(/ +$/,"").replace(/ +/g," ")},t.prototype.error=function(t){throw new Error(t)},t.OPTIONS={MmlFactory:null,fixMisplacedChildren:!0,verify:a({},i.AbstractMmlNode.verifyDefaults),translateEntities:!0},t}();e.MathMLCompile=l},723:function(t,e){MathJax._.components.global.isObject,MathJax._.components.global.combineConfig,MathJax._.components.global.combineDefaults,e.r8=MathJax._.components.global.combineWithMathJax,MathJax._.components.global.MathJax},649:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractFindMath=MathJax._.core.FindMath.AbstractFindMath},309:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractInputJax=MathJax._.core.InputJax.AbstractInputJax},921:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.TEXCLASS=MathJax._.core.MmlTree.MmlNode.TEXCLASS,e.TEXCLASSNAMES=MathJax._.core.MmlTree.MmlNode.TEXCLASSNAMES,e.indentAttributes=MathJax._.core.MmlTree.MmlNode.indentAttributes,e.AbstractMmlNode=MathJax._.core.MmlTree.MmlNode.AbstractMmlNode,e.AbstractMmlTokenNode=MathJax._.core.MmlTree.MmlNode.AbstractMmlTokenNode,e.AbstractMmlLayoutNode=MathJax._.core.MmlTree.MmlNode.AbstractMmlLayoutNode,e.AbstractMmlBaseNode=MathJax._.core.MmlTree.MmlNode.AbstractMmlBaseNode,e.AbstractMmlEmptyNode=MathJax._.core.MmlTree.MmlNode.AbstractMmlEmptyNode,e.TextNode=MathJax._.core.MmlTree.MmlNode.TextNode,e.XMLNode=MathJax._.core.MmlTree.MmlNode.XMLNode},29:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.options=MathJax._.util.Entities.options,e.entities=MathJax._.util.Entities.entities,e.add=MathJax._.util.Entities.add,e.remove=MathJax._.util.Entities.remove,e.translate=MathJax._.util.Entities.translate,e.numeric=MathJax._.util.Entities.numeric},898:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.FunctionList=MathJax._.util.FunctionList.FunctionList},77:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.APPEND=MathJax._.util.Options.APPEND,e.REMOVE=MathJax._.util.Options.REMOVE,e.Expandable=MathJax._.util.Options.Expandable,e.expandable=MathJax._.util.Options.expandable,e.makeArray=MathJax._.util.Options.makeArray,e.keys=MathJax._.util.Options.keys,e.copy=MathJax._.util.Options.copy,e.insert=MathJax._.util.Options.insert,e.defaultOptions=MathJax._.util.Options.defaultOptions,e.userOptions=MathJax._.util.Options.userOptions,e.selectOptions=MathJax._.util.Options.selectOptions,e.selectOptionsFromKeys=MathJax._.util.Options.selectOptionsFromKeys,e.separateOptions=MathJax._.util.Options.separateOptions}},i={};function n(t){var e=i[t];if(void 0!==e)return e.exports;var r=i[t]={exports:{}};return o[t].call(r.exports,r,r.exports,n),r.exports}t=n(723),e=n(236),r=n(794),a=n(332),(0,t.r8)({_:{input:{mathml_ts:e,mathml:{FindMathML:r,MathMLCompile:a}}}}),MathJax.startup&&(MathJax.startup.registerConstructor("mml",e.MathML),MathJax.startup.useInput("mml")),MathJax.loader&&MathJax.loader.pathFilters.add((function(t){return t.name=t.name.replace(/\/util\/entities\/.*?\.js/,"/input/mml/entities.js"),!0}))}(); \ No newline at end of file diff --git a/docs/assets/vendor/mathjax/input/mml/entities.js b/docs/assets/vendor/mathjax/input/mml/entities.js new file mode 100644 index 0000000..0bc7899 --- /dev/null +++ b/docs/assets/vendor/mathjax/input/mml/entities.js @@ -0,0 +1 @@ +!function(){"use strict";var r={170:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({AElig:"\xc6",AMP:"&",Aacute:"\xc1",Abreve:"\u0102",Acirc:"\xc2",Acy:"\u0410",Agrave:"\xc0",Alpha:"\u0391",Amacr:"\u0100",And:"\u2a53",Aogon:"\u0104",Aring:"\xc5",Assign:"\u2254",Atilde:"\xc3",Auml:"\xc4",aacute:"\xe1",abreve:"\u0103",ac:"\u223e",acE:"\u223e\u0333",acd:"\u223f",acirc:"\xe2",acy:"\u0430",aelig:"\xe6",af:"\u2061",agrave:"\xe0",alefsym:"\u2135",amacr:"\u0101",andand:"\u2a55",andd:"\u2a5c",andslope:"\u2a58",andv:"\u2a5a",ange:"\u29a4",angle:"\u2220",angmsdaa:"\u29a8",angmsdab:"\u29a9",angmsdac:"\u29aa",angmsdad:"\u29ab",angmsdae:"\u29ac",angmsdaf:"\u29ad",angmsdag:"\u29ae",angmsdah:"\u29af",angrt:"\u221f",angrtvb:"\u22be",angrtvbd:"\u299d",angst:"\xc5",angzarr:"\u237c",aogon:"\u0105",ap:"\u2248",apE:"\u2a70",apacir:"\u2a6f",apid:"\u224b",apos:"'",approx:"\u2248",approxeq:"\u224a",aring:"\xe5",ast:"*",asymp:"\u2248",asympeq:"\u224d",atilde:"\xe3",auml:"\xe4",awconint:"\u2233",awint:"\u2a11"},"a")},349:function(r,e,t){t(170),t(901),t(801),t(869),t(619),t(125),t(637),t(375),t(146),t(658),t(933),t(219),t(113),t(283),t(943),t(229),t(473),t(35),t(826),t(453),t(827),t(517),t(336),t(373),t(215),t(179),t(77),t(650),t(11)},901:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({Barv:"\u2ae7",Barwed:"\u2306",Bcy:"\u0411",Bernoullis:"\u212c",Beta:"\u0392",Bumpeq:"\u224e",bNot:"\u2aed",backcong:"\u224c",backepsilon:"\u03f6",barvee:"\u22bd",barwed:"\u2305",barwedge:"\u2305",bbrk:"\u23b5",bbrktbrk:"\u23b6",bcong:"\u224c",bcy:"\u0431",bdquo:"\u201e",becaus:"\u2235",because:"\u2235",bemptyv:"\u29b0",bepsi:"\u03f6",bernou:"\u212c",bigcap:"\u22c2",bigcup:"\u22c3",bigvee:"\u22c1",bigwedge:"\u22c0",bkarow:"\u290d",blacksquare:"\u25aa",blacktriangleright:"\u25b8",blank:"\u2423",blk12:"\u2592",blk14:"\u2591",blk34:"\u2593",block:"\u2588",bne:"=\u20e5",bnequiv:"\u2261\u20e5",bnot:"\u2310",bot:"\u22a5",bottom:"\u22a5",boxDL:"\u2557",boxDR:"\u2554",boxDl:"\u2556",boxDr:"\u2553",boxH:"\u2550",boxHD:"\u2566",boxHU:"\u2569",boxHd:"\u2564",boxHu:"\u2567",boxUL:"\u255d",boxUR:"\u255a",boxUl:"\u255c",boxUr:"\u2559",boxV:"\u2551",boxVH:"\u256c",boxVL:"\u2563",boxVR:"\u2560",boxVh:"\u256b",boxVl:"\u2562",boxVr:"\u255f",boxbox:"\u29c9",boxdL:"\u2555",boxdR:"\u2552",boxh:"\u2500",boxhD:"\u2565",boxhU:"\u2568",boxhd:"\u252c",boxhu:"\u2534",boxuL:"\u255b",boxuR:"\u2558",boxv:"\u2502",boxvH:"\u256a",boxvL:"\u2561",boxvR:"\u255e",boxvh:"\u253c",boxvl:"\u2524",boxvr:"\u251c",bprime:"\u2035",breve:"\u02d8",brvbar:"\xa6",bsemi:"\u204f",bsim:"\u223d",bsime:"\u22cd",bsolb:"\u29c5",bsolhsub:"\u27c8",bullet:"\u2022",bump:"\u224e",bumpE:"\u2aae",bumpe:"\u224f",bumpeq:"\u224f"},"b")},801:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({CHcy:"\u0427",COPY:"\xa9",Cacute:"\u0106",CapitalDifferentialD:"\u2145",Cayleys:"\u212d",Ccaron:"\u010c",Ccedil:"\xc7",Ccirc:"\u0108",Cconint:"\u2230",Cdot:"\u010a",Cedilla:"\xb8",Chi:"\u03a7",ClockwiseContourIntegral:"\u2232",CloseCurlyDoubleQuote:"\u201d",CloseCurlyQuote:"\u2019",Colon:"\u2237",Colone:"\u2a74",Conint:"\u222f",CounterClockwiseContourIntegral:"\u2233",cacute:"\u0107",capand:"\u2a44",capbrcup:"\u2a49",capcap:"\u2a4b",capcup:"\u2a47",capdot:"\u2a40",caps:"\u2229\ufe00",caret:"\u2041",caron:"\u02c7",ccaps:"\u2a4d",ccaron:"\u010d",ccedil:"\xe7",ccirc:"\u0109",ccups:"\u2a4c",ccupssm:"\u2a50",cdot:"\u010b",cedil:"\xb8",cemptyv:"\u29b2",cent:"\xa2",centerdot:"\xb7",chcy:"\u0447",checkmark:"\u2713",cir:"\u25cb",cirE:"\u29c3",cire:"\u2257",cirfnint:"\u2a10",cirmid:"\u2aef",cirscir:"\u29c2",clubsuit:"\u2663",colone:"\u2254",coloneq:"\u2254",comma:",",commat:"@",compfn:"\u2218",complement:"\u2201",complexes:"\u2102",cong:"\u2245",congdot:"\u2a6d",conint:"\u222e",coprod:"\u2210",copy:"\xa9",copysr:"\u2117",crarr:"\u21b5",cross:"\u2717",csub:"\u2acf",csube:"\u2ad1",csup:"\u2ad0",csupe:"\u2ad2",cudarrl:"\u2938",cudarrr:"\u2935",cularrp:"\u293d",cupbrcap:"\u2a48",cupcap:"\u2a46",cupcup:"\u2a4a",cupdot:"\u228d",cupor:"\u2a45",cups:"\u222a\ufe00",curarrm:"\u293c",curlyeqprec:"\u22de",curlyeqsucc:"\u22df",curren:"\xa4",curvearrowleft:"\u21b6",curvearrowright:"\u21b7",cuvee:"\u22ce",cuwed:"\u22cf",cwconint:"\u2232",cwint:"\u2231",cylcty:"\u232d"},"c")},869:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({DD:"\u2145",DDotrahd:"\u2911",DJcy:"\u0402",DScy:"\u0405",DZcy:"\u040f",Darr:"\u21a1",Dashv:"\u2ae4",Dcaron:"\u010e",Dcy:"\u0414",DiacriticalAcute:"\xb4",DiacriticalDot:"\u02d9",DiacriticalDoubleAcute:"\u02dd",DiacriticalGrave:"`",DiacriticalTilde:"\u02dc",Dot:"\xa8",DotDot:"\u20dc",DoubleContourIntegral:"\u222f",DoubleDownArrow:"\u21d3",DoubleLeftArrow:"\u21d0",DoubleLeftRightArrow:"\u21d4",DoubleLeftTee:"\u2ae4",DoubleLongLeftArrow:"\u27f8",DoubleLongLeftRightArrow:"\u27fa",DoubleLongRightArrow:"\u27f9",DoubleRightArrow:"\u21d2",DoubleUpArrow:"\u21d1",DoubleUpDownArrow:"\u21d5",DownArrowBar:"\u2913",DownArrowUpArrow:"\u21f5",DownBreve:"\u0311",DownLeftRightVector:"\u2950",DownLeftTeeVector:"\u295e",DownLeftVectorBar:"\u2956",DownRightTeeVector:"\u295f",DownRightVectorBar:"\u2957",DownTeeArrow:"\u21a7",Dstrok:"\u0110",dArr:"\u21d3",dHar:"\u2965",darr:"\u2193",dash:"\u2010",dashv:"\u22a3",dbkarow:"\u290f",dblac:"\u02dd",dcaron:"\u010f",dcy:"\u0434",dd:"\u2146",ddagger:"\u2021",ddotseq:"\u2a77",demptyv:"\u29b1",dfisht:"\u297f",dharl:"\u21c3",dharr:"\u21c2",diam:"\u22c4",diamond:"\u22c4",diamondsuit:"\u2666",diams:"\u2666",die:"\xa8",disin:"\u22f2",divide:"\xf7",divonx:"\u22c7",djcy:"\u0452",dlcorn:"\u231e",dlcrop:"\u230d",dollar:"$",doteq:"\u2250",dotminus:"\u2238",doublebarwedge:"\u2306",downarrow:"\u2193",downdownarrows:"\u21ca",downharpoonleft:"\u21c3",downharpoonright:"\u21c2",drbkarow:"\u2910",drcorn:"\u231f",drcrop:"\u230c",dscy:"\u0455",dsol:"\u29f6",dstrok:"\u0111",dtri:"\u25bf",dtrif:"\u25be",duarr:"\u21f5",duhar:"\u296f",dwangle:"\u29a6",dzcy:"\u045f",dzigrarr:"\u27ff"},"d")},619:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({ENG:"\u014a",ETH:"\xd0",Eacute:"\xc9",Ecaron:"\u011a",Ecirc:"\xca",Ecy:"\u042d",Edot:"\u0116",Egrave:"\xc8",Emacr:"\u0112",EmptySmallSquare:"\u25fb",EmptyVerySmallSquare:"\u25ab",Eogon:"\u0118",Epsilon:"\u0395",Equal:"\u2a75",Esim:"\u2a73",Eta:"\u0397",Euml:"\xcb",eDDot:"\u2a77",eDot:"\u2251",eacute:"\xe9",easter:"\u2a6e",ecaron:"\u011b",ecirc:"\xea",ecolon:"\u2255",ecy:"\u044d",edot:"\u0117",ee:"\u2147",eg:"\u2a9a",egrave:"\xe8",egsdot:"\u2a98",el:"\u2a99",elinters:"\u23e7",elsdot:"\u2a97",emacr:"\u0113",emptyset:"\u2205",emptyv:"\u2205",emsp:"\u2003",emsp13:"\u2004",emsp14:"\u2005",eng:"\u014b",ensp:"\u2002",eogon:"\u0119",epar:"\u22d5",eparsl:"\u29e3",eplus:"\u2a71",epsilon:"\u03b5",eqcirc:"\u2256",eqcolon:"\u2255",eqsim:"\u2242",eqslantgtr:"\u2a96",eqslantless:"\u2a95",equals:"=",equest:"\u225f",equiv:"\u2261",equivDD:"\u2a78",eqvparsl:"\u29e5",erarr:"\u2971",esdot:"\u2250",esim:"\u2242",euml:"\xeb",euro:"\u20ac",excl:"!",exist:"\u2203",expectation:"\u2130",exponentiale:"\u2147"},"e")},125:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({Fcy:"\u0424",FilledSmallSquare:"\u25fc",Fouriertrf:"\u2131",fallingdotseq:"\u2252",fcy:"\u0444",female:"\u2640",ffilig:"\ufb03",fflig:"\ufb00",ffllig:"\ufb04",filig:"\ufb01",fjlig:"fj",fllig:"\ufb02",fltns:"\u25b1",fnof:"\u0192",forall:"\u2200",forkv:"\u2ad9",fpartint:"\u2a0d",frac12:"\xbd",frac13:"\u2153",frac14:"\xbc",frac15:"\u2155",frac16:"\u2159",frac18:"\u215b",frac23:"\u2154",frac25:"\u2156",frac34:"\xbe",frac35:"\u2157",frac38:"\u215c",frac45:"\u2158",frac56:"\u215a",frac58:"\u215d",frac78:"\u215e",frasl:"\u2044"},"f")},77:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({Afr:"\ud835\udd04",Bfr:"\ud835\udd05",Cfr:"\u212d",Dfr:"\ud835\udd07",Efr:"\ud835\udd08",Ffr:"\ud835\udd09",Gfr:"\ud835\udd0a",Hfr:"\u210c",Ifr:"\u2111",Jfr:"\ud835\udd0d",Kfr:"\ud835\udd0e",Lfr:"\ud835\udd0f",Mfr:"\ud835\udd10",Nfr:"\ud835\udd11",Ofr:"\ud835\udd12",Pfr:"\ud835\udd13",Qfr:"\ud835\udd14",Rfr:"\u211c",Sfr:"\ud835\udd16",Tfr:"\ud835\udd17",Ufr:"\ud835\udd18",Vfr:"\ud835\udd19",Wfr:"\ud835\udd1a",Xfr:"\ud835\udd1b",Yfr:"\ud835\udd1c",Zfr:"\u2128",afr:"\ud835\udd1e",bfr:"\ud835\udd1f",cfr:"\ud835\udd20",dfr:"\ud835\udd21",efr:"\ud835\udd22",ffr:"\ud835\udd23",gfr:"\ud835\udd24",hfr:"\ud835\udd25",ifr:"\ud835\udd26",jfr:"\ud835\udd27",kfr:"\ud835\udd28",lfr:"\ud835\udd29",mfr:"\ud835\udd2a",nfr:"\ud835\udd2b",ofr:"\ud835\udd2c",pfr:"\ud835\udd2d",qfr:"\ud835\udd2e",rfr:"\ud835\udd2f",sfr:"\ud835\udd30",tfr:"\ud835\udd31",ufr:"\ud835\udd32",vfr:"\ud835\udd33",wfr:"\ud835\udd34",xfr:"\ud835\udd35",yfr:"\ud835\udd36",zfr:"\ud835\udd37"},"fr")},637:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({GJcy:"\u0403",GT:">",Gammad:"\u03dc",Gbreve:"\u011e",Gcedil:"\u0122",Gcirc:"\u011c",Gcy:"\u0413",Gdot:"\u0120",GreaterGreater:"\u2aa2",Gt:"\u226b",gE:"\u2267",gacute:"\u01f5",gammad:"\u03dd",gbreve:"\u011f",gcirc:"\u011d",gcy:"\u0433",gdot:"\u0121",ge:"\u2265",gel:"\u22db",geq:"\u2265",geqq:"\u2267",geqslant:"\u2a7e",ges:"\u2a7e",gescc:"\u2aa9",gesdot:"\u2a80",gesdoto:"\u2a82",gesdotol:"\u2a84",gesl:"\u22db\ufe00",gesles:"\u2a94",gg:"\u226b",ggg:"\u22d9",gjcy:"\u0453",gl:"\u2277",glE:"\u2a92",gla:"\u2aa5",glj:"\u2aa4",gnapprox:"\u2a8a",gneq:"\u2a88",gneqq:"\u2269",grave:"`",gsim:"\u2273",gsime:"\u2a8e",gsiml:"\u2a90",gtcc:"\u2aa7",gtcir:"\u2a7a",gtlPar:"\u2995",gtquest:"\u2a7c",gtrapprox:"\u2a86",gtrarr:"\u2978",gtrdot:"\u22d7",gtreqless:"\u22db",gtreqqless:"\u2a8c",gtrless:"\u2277",gtrsim:"\u2273",gvertneqq:"\u2269\ufe00",gvnE:"\u2269\ufe00"},"g")},375:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({HARDcy:"\u042a",Hcirc:"\u0124",HilbertSpace:"\u210b",HorizontalLine:"\u2500",Hstrok:"\u0126",hArr:"\u21d4",hairsp:"\u200a",half:"\xbd",hamilt:"\u210b",hardcy:"\u044a",harr:"\u2194",harrcir:"\u2948",hcirc:"\u0125",hearts:"\u2665",heartsuit:"\u2665",hercon:"\u22b9",hksearow:"\u2925",hkswarow:"\u2926",hoarr:"\u21ff",homtht:"\u223b",horbar:"\u2015",hslash:"\u210f",hstrok:"\u0127",hybull:"\u2043",hyphen:"\u2010"},"h")},146:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({IEcy:"\u0415",IJlig:"\u0132",IOcy:"\u0401",Iacute:"\xcd",Icirc:"\xce",Icy:"\u0418",Idot:"\u0130",Igrave:"\xcc",Imacr:"\u012a",Implies:"\u21d2",Int:"\u222c",Iogon:"\u012e",Iota:"\u0399",Itilde:"\u0128",Iukcy:"\u0406",Iuml:"\xcf",iacute:"\xed",ic:"\u2063",icirc:"\xee",icy:"\u0438",iecy:"\u0435",iexcl:"\xa1",iff:"\u21d4",igrave:"\xec",ii:"\u2148",iiiint:"\u2a0c",iiint:"\u222d",iinfin:"\u29dc",iiota:"\u2129",ijlig:"\u0133",imacr:"\u012b",image:"\u2111",imagline:"\u2110",imagpart:"\u2111",imof:"\u22b7",imped:"\u01b5",in:"\u2208",incare:"\u2105",infintie:"\u29dd",inodot:"\u0131",int:"\u222b",integers:"\u2124",intercal:"\u22ba",intlarhk:"\u2a17",intprod:"\u2a3c",iocy:"\u0451",iogon:"\u012f",iprod:"\u2a3c",iquest:"\xbf",isin:"\u2208",isinE:"\u22f9",isindot:"\u22f5",isins:"\u22f4",isinsv:"\u22f3",isinv:"\u2208",it:"\u2062",itilde:"\u0129",iukcy:"\u0456",iuml:"\xef"},"i")},658:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({Jcirc:"\u0134",Jcy:"\u0419",Jsercy:"\u0408",Jukcy:"\u0404",jcirc:"\u0135",jcy:"\u0439",jsercy:"\u0458",jukcy:"\u0454"},"j")},933:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({KHcy:"\u0425",KJcy:"\u040c",Kappa:"\u039a",Kcedil:"\u0136",Kcy:"\u041a",kcedil:"\u0137",kcy:"\u043a",kgreen:"\u0138",khcy:"\u0445",kjcy:"\u045c"},"k")},219:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({LJcy:"\u0409",LT:"<",Lacute:"\u0139",Lang:"\u27ea",Laplacetrf:"\u2112",Lcaron:"\u013d",Lcedil:"\u013b",Lcy:"\u041b",LeftArrowBar:"\u21e4",LeftDoubleBracket:"\u27e6",LeftDownTeeVector:"\u2961",LeftDownVectorBar:"\u2959",LeftRightVector:"\u294e",LeftTeeArrow:"\u21a4",LeftTeeVector:"\u295a",LeftTriangleBar:"\u29cf",LeftUpDownVector:"\u2951",LeftUpTeeVector:"\u2960",LeftUpVectorBar:"\u2958",LeftVectorBar:"\u2952",LessLess:"\u2aa1",Lmidot:"\u013f",LowerLeftArrow:"\u2199",LowerRightArrow:"\u2198",Lstrok:"\u0141",Lt:"\u226a",lAarr:"\u21da",lArr:"\u21d0",lAtail:"\u291b",lBarr:"\u290e",lE:"\u2266",lHar:"\u2962",lacute:"\u013a",laemptyv:"\u29b4",lagran:"\u2112",lang:"\u27e8",langd:"\u2991",langle:"\u27e8",laquo:"\xab",larr:"\u2190",larrb:"\u21e4",larrbfs:"\u291f",larrfs:"\u291d",larrhk:"\u21a9",larrpl:"\u2939",larrsim:"\u2973",lat:"\u2aab",latail:"\u2919",late:"\u2aad",lates:"\u2aad\ufe00",lbarr:"\u290c",lbbrk:"\u2772",lbrke:"\u298b",lbrksld:"\u298f",lbrkslu:"\u298d",lcaron:"\u013e",lcedil:"\u013c",lceil:"\u2308",lcub:"{",lcy:"\u043b",ldca:"\u2936",ldquo:"\u201c",ldquor:"\u201e",ldrdhar:"\u2967",ldrushar:"\u294b",ldsh:"\u21b2",leftarrow:"\u2190",leftarrowtail:"\u21a2",leftharpoondown:"\u21bd",leftharpoonup:"\u21bc",leftrightarrow:"\u2194",leftrightarrows:"\u21c6",leftrightharpoons:"\u21cb",leftrightsquigarrow:"\u21ad",leg:"\u22da",leq:"\u2264",leqq:"\u2266",leqslant:"\u2a7d",les:"\u2a7d",lescc:"\u2aa8",lesdot:"\u2a7f",lesdoto:"\u2a81",lesdotor:"\u2a83",lesg:"\u22da\ufe00",lesges:"\u2a93",lessapprox:"\u2a85",lesseqgtr:"\u22da",lesseqqgtr:"\u2a8b",lessgtr:"\u2276",lesssim:"\u2272",lfisht:"\u297c",lfloor:"\u230a",lg:"\u2276",lgE:"\u2a91",lhard:"\u21bd",lharu:"\u21bc",lharul:"\u296a",lhblk:"\u2584",ljcy:"\u0459",ll:"\u226a",llarr:"\u21c7",llcorner:"\u231e",llhard:"\u296b",lltri:"\u25fa",lmidot:"\u0140",lmoustache:"\u23b0",lnapprox:"\u2a89",lneq:"\u2a87",lneqq:"\u2268",loang:"\u27ec",loarr:"\u21fd",lobrk:"\u27e6",longleftarrow:"\u27f5",longleftrightarrow:"\u27f7",longrightarrow:"\u27f6",looparrowleft:"\u21ab",lopar:"\u2985",loplus:"\u2a2d",lotimes:"\u2a34",lowbar:"_",lozenge:"\u25ca",lozf:"\u29eb",lpar:"(",lparlt:"\u2993",lrarr:"\u21c6",lrcorner:"\u231f",lrhar:"\u21cb",lrhard:"\u296d",lrm:"\u200e",lrtri:"\u22bf",lsaquo:"\u2039",lsh:"\u21b0",lsim:"\u2272",lsime:"\u2a8d",lsimg:"\u2a8f",lsqb:"[",lsquo:"\u2018",lsquor:"\u201a",lstrok:"\u0142",ltcc:"\u2aa6",ltcir:"\u2a79",ltdot:"\u22d6",lthree:"\u22cb",ltlarr:"\u2976",ltquest:"\u2a7b",ltrPar:"\u2996",ltrie:"\u22b4",ltrif:"\u25c2",lurdshar:"\u294a",luruhar:"\u2966",lvertneqq:"\u2268\ufe00",lvnE:"\u2268\ufe00"},"l")},113:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({Map:"\u2905",Mcy:"\u041c",MediumSpace:"\u205f",Mellintrf:"\u2133",Mu:"\u039c",mDDot:"\u223a",male:"\u2642",maltese:"\u2720",map:"\u21a6",mapsto:"\u21a6",mapstodown:"\u21a7",mapstoleft:"\u21a4",mapstoup:"\u21a5",marker:"\u25ae",mcomma:"\u2a29",mcy:"\u043c",mdash:"\u2014",measuredangle:"\u2221",micro:"\xb5",mid:"\u2223",midast:"*",midcir:"\u2af0",middot:"\xb7",minus:"\u2212",minusb:"\u229f",minusd:"\u2238",minusdu:"\u2a2a",mlcp:"\u2adb",mldr:"\u2026",mnplus:"\u2213",models:"\u22a7",mp:"\u2213",mstpos:"\u223e",mumap:"\u22b8"},"m")},283:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({NJcy:"\u040a",Nacute:"\u0143",Ncaron:"\u0147",Ncedil:"\u0145",Ncy:"\u041d",NegativeMediumSpace:"\u200b",NegativeThickSpace:"\u200b",NegativeThinSpace:"\u200b",NegativeVeryThinSpace:"\u200b",NewLine:"\n",NoBreak:"\u2060",NonBreakingSpace:"\xa0",Not:"\u2aec",NotCongruent:"\u2262",NotCupCap:"\u226d",NotEqualTilde:"\u2242\u0338",NotGreaterFullEqual:"\u2267\u0338",NotGreaterGreater:"\u226b\u0338",NotGreaterLess:"\u2279",NotGreaterSlantEqual:"\u2a7e\u0338",NotGreaterTilde:"\u2275",NotHumpDownHump:"\u224e\u0338",NotHumpEqual:"\u224f\u0338",NotLeftTriangleBar:"\u29cf\u0338",NotLessGreater:"\u2278",NotLessLess:"\u226a\u0338",NotLessSlantEqual:"\u2a7d\u0338",NotLessTilde:"\u2274",NotNestedGreaterGreater:"\u2aa2\u0338",NotNestedLessLess:"\u2aa1\u0338",NotPrecedesEqual:"\u2aaf\u0338",NotReverseElement:"\u220c",NotRightTriangleBar:"\u29d0\u0338",NotSquareSubset:"\u228f\u0338",NotSquareSubsetEqual:"\u22e2",NotSquareSuperset:"\u2290\u0338",NotSquareSupersetEqual:"\u22e3",NotSubset:"\u2282\u20d2",NotSucceedsEqual:"\u2ab0\u0338",NotSucceedsTilde:"\u227f\u0338",NotSuperset:"\u2283\u20d2",NotTildeEqual:"\u2244",NotTildeFullEqual:"\u2247",NotTildeTilde:"\u2249",Ntilde:"\xd1",Nu:"\u039d",nGg:"\u22d9\u0338",nGt:"\u226b\u20d2",nGtv:"\u226b\u0338",nLl:"\u22d8\u0338",nLt:"\u226a\u20d2",nLtv:"\u226a\u0338",nabla:"\u2207",nacute:"\u0144",nang:"\u2220\u20d2",nap:"\u2249",napE:"\u2a70\u0338",napid:"\u224b\u0338",napos:"\u0149",napprox:"\u2249",natural:"\u266e",naturals:"\u2115",nbsp:"\xa0",nbump:"\u224e\u0338",nbumpe:"\u224f\u0338",ncap:"\u2a43",ncaron:"\u0148",ncedil:"\u0146",ncong:"\u2247",ncongdot:"\u2a6d\u0338",ncup:"\u2a42",ncy:"\u043d",ndash:"\u2013",ne:"\u2260",neArr:"\u21d7",nearhk:"\u2924",nearrow:"\u2197",nedot:"\u2250\u0338",nequiv:"\u2262",nesear:"\u2928",nesim:"\u2242\u0338",nexist:"\u2204",nexists:"\u2204",ngE:"\u2267\u0338",nge:"\u2271",ngeq:"\u2271",ngeqq:"\u2267\u0338",ngeqslant:"\u2a7e\u0338",nges:"\u2a7e\u0338",ngsim:"\u2275",ngt:"\u226f",ngtr:"\u226f",nhArr:"\u21ce",nhpar:"\u2af2",ni:"\u220b",nis:"\u22fc",nisd:"\u22fa",niv:"\u220b",njcy:"\u045a",nlArr:"\u21cd",nlE:"\u2266\u0338",nldr:"\u2025",nle:"\u2270",nleftarrow:"\u219a",nleftrightarrow:"\u21ae",nleq:"\u2270",nleqq:"\u2266\u0338",nleqslant:"\u2a7d\u0338",nles:"\u2a7d\u0338",nless:"\u226e",nlsim:"\u2274",nlt:"\u226e",nltri:"\u22ea",nltrie:"\u22ec",nmid:"\u2224",notin:"\u2209",notinE:"\u22f9\u0338",notindot:"\u22f5\u0338",notinva:"\u2209",notinvb:"\u22f7",notinvc:"\u22f6",notni:"\u220c",notniva:"\u220c",notnivb:"\u22fe",notnivc:"\u22fd",npar:"\u2226",nparallel:"\u2226",nparsl:"\u2afd\u20e5",npart:"\u2202\u0338",npolint:"\u2a14",npr:"\u2280",nprcue:"\u22e0",npre:"\u2aaf\u0338",nprec:"\u2280",npreceq:"\u2aaf\u0338",nrArr:"\u21cf",nrarrc:"\u2933\u0338",nrarrw:"\u219d\u0338",nrightarrow:"\u219b",nrtri:"\u22eb",nrtrie:"\u22ed",nsc:"\u2281",nsccue:"\u22e1",nsce:"\u2ab0\u0338",nshortmid:"\u2224",nshortparallel:"\u2226",nsim:"\u2241",nsime:"\u2244",nsimeq:"\u2244",nsmid:"\u2224",nspar:"\u2226",nsqsube:"\u22e2",nsqsupe:"\u22e3",nsub:"\u2284",nsubE:"\u2ac5\u0338",nsube:"\u2288",nsubset:"\u2282\u20d2",nsubseteq:"\u2288",nsubseteqq:"\u2ac5\u0338",nsucc:"\u2281",nsucceq:"\u2ab0\u0338",nsup:"\u2285",nsupE:"\u2ac6\u0338",nsupe:"\u2289",nsupset:"\u2283\u20d2",nsupseteq:"\u2289",nsupseteqq:"\u2ac6\u0338",ntgl:"\u2279",ntilde:"\xf1",ntlg:"\u2278",ntriangleleft:"\u22ea",ntrianglelefteq:"\u22ec",ntriangleright:"\u22eb",ntrianglerighteq:"\u22ed",num:"#",numero:"\u2116",numsp:"\u2007",nvHarr:"\u2904",nvap:"\u224d\u20d2",nvge:"\u2265\u20d2",nvgt:">\u20d2",nvinfin:"\u29de",nvlArr:"\u2902",nvle:"\u2264\u20d2",nvlt:"<\u20d2",nvltrie:"\u22b4\u20d2",nvrArr:"\u2903",nvrtrie:"\u22b5\u20d2",nvsim:"\u223c\u20d2",nwArr:"\u21d6",nwarhk:"\u2923",nwarrow:"\u2196",nwnear:"\u2927"},"n")},943:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({OElig:"\u0152",Oacute:"\xd3",Ocirc:"\xd4",Ocy:"\u041e",Odblac:"\u0150",Ograve:"\xd2",Omacr:"\u014c",Omicron:"\u039f",OpenCurlyDoubleQuote:"\u201c",OpenCurlyQuote:"\u2018",Or:"\u2a54",Oslash:"\xd8",Otilde:"\xd5",Otimes:"\u2a37",Ouml:"\xd6",OverBracket:"\u23b4",OverParenthesis:"\u23dc",oS:"\u24c8",oacute:"\xf3",oast:"\u229b",ocir:"\u229a",ocirc:"\xf4",ocy:"\u043e",odash:"\u229d",odblac:"\u0151",odiv:"\u2a38",odot:"\u2299",odsold:"\u29bc",oelig:"\u0153",ofcir:"\u29bf",ogon:"\u02db",ograve:"\xf2",ogt:"\u29c1",ohbar:"\u29b5",ohm:"\u03a9",oint:"\u222e",olarr:"\u21ba",olcir:"\u29be",olcross:"\u29bb",oline:"\u203e",olt:"\u29c0",omacr:"\u014d",omid:"\u29b6",ominus:"\u2296",opar:"\u29b7",operp:"\u29b9",oplus:"\u2295",orarr:"\u21bb",ord:"\u2a5d",order:"\u2134",orderof:"\u2134",ordf:"\xaa",ordm:"\xba",origof:"\u22b6",oror:"\u2a56",orslope:"\u2a57",orv:"\u2a5b",oslash:"\xf8",otilde:"\xf5",otimes:"\u2297",otimesas:"\u2a36",ouml:"\xf6",ovbar:"\u233d"},"o")},650:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({Aopf:"\ud835\udd38",Bopf:"\ud835\udd39",Copf:"\u2102",Dopf:"\ud835\udd3b",Eopf:"\ud835\udd3c",Fopf:"\ud835\udd3d",Gopf:"\ud835\udd3e",Hopf:"\u210d",Iopf:"\ud835\udd40",Jopf:"\ud835\udd41",Kopf:"\ud835\udd42",Lopf:"\ud835\udd43",Mopf:"\ud835\udd44",Nopf:"\u2115",Oopf:"\ud835\udd46",Popf:"\u2119",Qopf:"\u211a",Ropf:"\u211d",Sopf:"\ud835\udd4a",Topf:"\ud835\udd4b",Uopf:"\ud835\udd4c",Vopf:"\ud835\udd4d",Wopf:"\ud835\udd4e",Xopf:"\ud835\udd4f",Yopf:"\ud835\udd50",Zopf:"\u2124",aopf:"\ud835\udd52",bopf:"\ud835\udd53",copf:"\ud835\udd54",dopf:"\ud835\udd55",eopf:"\ud835\udd56",fopf:"\ud835\udd57",gopf:"\ud835\udd58",hopf:"\ud835\udd59",iopf:"\ud835\udd5a",jopf:"\ud835\udd5b",kopf:"\ud835\udd5c",lopf:"\ud835\udd5d",mopf:"\ud835\udd5e",nopf:"\ud835\udd5f",oopf:"\ud835\udd60",popf:"\ud835\udd61",qopf:"\ud835\udd62",ropf:"\ud835\udd63",sopf:"\ud835\udd64",topf:"\ud835\udd65",uopf:"\ud835\udd66",vopf:"\ud835\udd67",wopf:"\ud835\udd68",xopf:"\ud835\udd69",yopf:"\ud835\udd6a",zopf:"\ud835\udd6b"},"opf")},229:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({Pcy:"\u041f",Poincareplane:"\u210c",Pr:"\u2abb",Prime:"\u2033",Proportion:"\u2237",par:"\u2225",para:"\xb6",parallel:"\u2225",parsim:"\u2af3",parsl:"\u2afd",part:"\u2202",pcy:"\u043f",percnt:"%",permil:"\u2030",perp:"\u22a5",pertenk:"\u2031",phmmat:"\u2133",phone:"\u260e",pitchfork:"\u22d4",planck:"\u210f",planckh:"\u210e",plankv:"\u210f",plus:"+",plusacir:"\u2a23",plusb:"\u229e",pluscir:"\u2a22",plusdo:"\u2214",plusdu:"\u2a25",pluse:"\u2a72",plusmn:"\xb1",plussim:"\u2a26",plustwo:"\u2a27",pm:"\xb1",pointint:"\u2a15",pound:"\xa3",pr:"\u227a",prE:"\u2ab3",prcue:"\u227c",pre:"\u2aaf",prec:"\u227a",precapprox:"\u2ab7",preccurlyeq:"\u227c",preceq:"\u2aaf",precsim:"\u227e",primes:"\u2119",prnE:"\u2ab5",prnap:"\u2ab9",prnsim:"\u22e8",prod:"\u220f",profalar:"\u232e",profline:"\u2312",profsurf:"\u2313",prop:"\u221d",propto:"\u221d",prsim:"\u227e",prurel:"\u22b0",puncsp:"\u2008"},"p")},473:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({QUOT:'"',qint:"\u2a0c",qprime:"\u2057",quaternions:"\u210d",quatint:"\u2a16",quest:"?",questeq:"\u225f"},"q")},35:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({RBarr:"\u2910",REG:"\xae",Racute:"\u0154",Rang:"\u27eb",Rarrtl:"\u2916",Rcaron:"\u0158",Rcedil:"\u0156",Rcy:"\u0420",ReverseElement:"\u220b",ReverseUpEquilibrium:"\u296f",Rho:"\u03a1",RightArrowBar:"\u21e5",RightDoubleBracket:"\u27e7",RightDownTeeVector:"\u295d",RightDownVectorBar:"\u2955",RightTeeVector:"\u295b",RightTriangleBar:"\u29d0",RightUpDownVector:"\u294f",RightUpTeeVector:"\u295c",RightUpVectorBar:"\u2954",RightVectorBar:"\u2953",RoundImplies:"\u2970",RuleDelayed:"\u29f4",rAarr:"\u21db",rArr:"\u21d2",rAtail:"\u291c",rBarr:"\u290f",rHar:"\u2964",race:"\u223d\u0331",racute:"\u0155",radic:"\u221a",raemptyv:"\u29b3",rang:"\u27e9",rangd:"\u2992",range:"\u29a5",rangle:"\u27e9",raquo:"\xbb",rarr:"\u2192",rarrap:"\u2975",rarrb:"\u21e5",rarrbfs:"\u2920",rarrc:"\u2933",rarrfs:"\u291e",rarrhk:"\u21aa",rarrlp:"\u21ac",rarrpl:"\u2945",rarrsim:"\u2974",rarrw:"\u219d",ratail:"\u291a",ratio:"\u2236",rationals:"\u211a",rbarr:"\u290d",rbbrk:"\u2773",rbrke:"\u298c",rbrksld:"\u298e",rbrkslu:"\u2990",rcaron:"\u0159",rcedil:"\u0157",rceil:"\u2309",rcub:"}",rcy:"\u0440",rdca:"\u2937",rdldhar:"\u2969",rdquo:"\u201d",rdquor:"\u201d",rdsh:"\u21b3",real:"\u211c",realine:"\u211b",realpart:"\u211c",reals:"\u211d",rect:"\u25ad",reg:"\xae",rfisht:"\u297d",rfloor:"\u230b",rhard:"\u21c1",rharu:"\u21c0",rharul:"\u296c",rightarrow:"\u2192",rightarrowtail:"\u21a3",rightharpoondown:"\u21c1",rightharpoonup:"\u21c0",rightleftarrows:"\u21c4",rightleftharpoons:"\u21cc",rightsquigarrow:"\u219d",risingdotseq:"\u2253",rlarr:"\u21c4",rlhar:"\u21cc",rlm:"\u200f",rmoustache:"\u23b1",rnmid:"\u2aee",roang:"\u27ed",roarr:"\u21fe",robrk:"\u27e7",ropar:"\u2986",roplus:"\u2a2e",rotimes:"\u2a35",rpar:")",rpargt:"\u2994",rppolint:"\u2a12",rrarr:"\u21c9",rsaquo:"\u203a",rsh:"\u21b1",rsqb:"]",rsquo:"\u2019",rsquor:"\u2019",rthree:"\u22cc",rtrie:"\u22b5",rtrif:"\u25b8",rtriltri:"\u29ce",ruluhar:"\u2968",rx:"\u211e"},"r")},826:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({SHCHcy:"\u0429",SHcy:"\u0428",SOFTcy:"\u042c",Sacute:"\u015a",Sc:"\u2abc",Scaron:"\u0160",Scedil:"\u015e",Scirc:"\u015c",Scy:"\u0421",ShortDownArrow:"\u2193",ShortLeftArrow:"\u2190",ShortRightArrow:"\u2192",ShortUpArrow:"\u2191",Sub:"\u22d0",Sup:"\u22d1",sacute:"\u015b",sbquo:"\u201a",sc:"\u227b",scE:"\u2ab4",scaron:"\u0161",sccue:"\u227d",sce:"\u2ab0",scedil:"\u015f",scirc:"\u015d",scpolint:"\u2a13",scsim:"\u227f",scy:"\u0441",sdotb:"\u22a1",sdote:"\u2a66",seArr:"\u21d8",searhk:"\u2925",searrow:"\u2198",semi:";",seswar:"\u2929",setminus:"\u2216",setmn:"\u2216",sext:"\u2736",sfrown:"\u2322",shchcy:"\u0449",shcy:"\u0448",shortmid:"\u2223",shortparallel:"\u2225",shy:"\xad",sigmaf:"\u03c2",sim:"\u223c",simdot:"\u2a6a",sime:"\u2243",simeq:"\u2243",simg:"\u2a9e",simgE:"\u2aa0",siml:"\u2a9d",simlE:"\u2a9f",simplus:"\u2a24",simrarr:"\u2972",slarr:"\u2190",smallsetminus:"\u2216",smashp:"\u2a33",smeparsl:"\u29e4",smid:"\u2223",smt:"\u2aaa",smte:"\u2aac",smtes:"\u2aac\ufe00",softcy:"\u044c",sol:"/",solb:"\u29c4",solbar:"\u233f",spadesuit:"\u2660",spar:"\u2225",sqcap:"\u2293",sqcaps:"\u2293\ufe00",sqcup:"\u2294",sqcups:"\u2294\ufe00",sqsub:"\u228f",sqsube:"\u2291",sqsubset:"\u228f",sqsubseteq:"\u2291",sqsup:"\u2290",sqsupe:"\u2292",sqsupset:"\u2290",sqsupseteq:"\u2292",squ:"\u25a1",square:"\u25a1",squarf:"\u25aa",squf:"\u25aa",srarr:"\u2192",ssetmn:"\u2216",ssmile:"\u2323",sstarf:"\u22c6",star:"\u2606",starf:"\u2605",straightepsilon:"\u03f5",straightphi:"\u03d5",strns:"\xaf",subdot:"\u2abd",sube:"\u2286",subedot:"\u2ac3",submult:"\u2ac1",subplus:"\u2abf",subrarr:"\u2979",subset:"\u2282",subseteq:"\u2286",subseteqq:"\u2ac5",subsetneq:"\u228a",subsetneqq:"\u2acb",subsim:"\u2ac7",subsub:"\u2ad5",subsup:"\u2ad3",succ:"\u227b",succapprox:"\u2ab8",succcurlyeq:"\u227d",succeq:"\u2ab0",succnapprox:"\u2aba",succneqq:"\u2ab6",succnsim:"\u22e9",succsim:"\u227f",sum:"\u2211",sung:"\u266a",sup:"\u2283",sup1:"\xb9",sup2:"\xb2",sup3:"\xb3",supdot:"\u2abe",supdsub:"\u2ad8",supe:"\u2287",supedot:"\u2ac4",suphsol:"\u27c9",suphsub:"\u2ad7",suplarr:"\u297b",supmult:"\u2ac2",supplus:"\u2ac0",supset:"\u2283",supseteq:"\u2287",supseteqq:"\u2ac6",supsetneq:"\u228b",supsetneqq:"\u2acc",supsim:"\u2ac8",supsub:"\u2ad4",supsup:"\u2ad6",swArr:"\u21d9",swarhk:"\u2926",swarrow:"\u2199",swnwar:"\u292a",szlig:"\xdf"},"s")},11:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({Ascr:"\ud835\udc9c",Bscr:"\u212c",Cscr:"\ud835\udc9e",Dscr:"\ud835\udc9f",Escr:"\u2130",Fscr:"\u2131",Gscr:"\ud835\udca2",Hscr:"\u210b",Iscr:"\u2110",Jscr:"\ud835\udca5",Kscr:"\ud835\udca6",Lscr:"\u2112",Mscr:"\u2133",Nscr:"\ud835\udca9",Oscr:"\ud835\udcaa",Pscr:"\ud835\udcab",Qscr:"\ud835\udcac",Rscr:"\u211b",Sscr:"\ud835\udcae",Tscr:"\ud835\udcaf",Uscr:"\ud835\udcb0",Vscr:"\ud835\udcb1",Wscr:"\ud835\udcb2",Xscr:"\ud835\udcb3",Yscr:"\ud835\udcb4",Zscr:"\ud835\udcb5",ascr:"\ud835\udcb6",bscr:"\ud835\udcb7",cscr:"\ud835\udcb8",dscr:"\ud835\udcb9",escr:"\u212f",fscr:"\ud835\udcbb",gscr:"\u210a",hscr:"\ud835\udcbd",iscr:"\ud835\udcbe",jscr:"\ud835\udcbf",kscr:"\ud835\udcc0",lscr:"\ud835\udcc1",mscr:"\ud835\udcc2",nscr:"\ud835\udcc3",oscr:"\u2134",pscr:"\ud835\udcc5",qscr:"\ud835\udcc6",rscr:"\ud835\udcc7",sscr:"\ud835\udcc8",tscr:"\ud835\udcc9",uscr:"\ud835\udcca",vscr:"\ud835\udccb",wscr:"\ud835\udccc",xscr:"\ud835\udccd",yscr:"\ud835\udcce",zscr:"\ud835\udccf"},"scr")},453:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({THORN:"\xde",TRADE:"\u2122",TSHcy:"\u040b",TScy:"\u0426",Tab:"\t",Tau:"\u03a4",Tcaron:"\u0164",Tcedil:"\u0162",Tcy:"\u0422",ThickSpace:"\u205f\u200a",ThinSpace:"\u2009",TripleDot:"\u20db",Tstrok:"\u0166",target:"\u2316",tbrk:"\u23b4",tcaron:"\u0165",tcedil:"\u0163",tcy:"\u0442",tdot:"\u20db",telrec:"\u2315",there4:"\u2234",therefore:"\u2234",thetasym:"\u03d1",thickapprox:"\u2248",thicksim:"\u223c",thinsp:"\u2009",thkap:"\u2248",thksim:"\u223c",thorn:"\xfe",timesb:"\u22a0",timesbar:"\u2a31",timesd:"\u2a30",tint:"\u222d",toea:"\u2928",top:"\u22a4",topbot:"\u2336",topcir:"\u2af1",topfork:"\u2ada",tosa:"\u2929",tprime:"\u2034",trade:"\u2122",triangledown:"\u25bf",triangleleft:"\u25c3",trianglelefteq:"\u22b4",triangleright:"\u25b9",trianglerighteq:"\u22b5",tridot:"\u25ec",trie:"\u225c",triminus:"\u2a3a",triplus:"\u2a39",trisb:"\u29cd",tritime:"\u2a3b",trpezium:"\u23e2",tscy:"\u0446",tshcy:"\u045b",tstrok:"\u0167",twixt:"\u226c",twoheadleftarrow:"\u219e",twoheadrightarrow:"\u21a0"},"t")},827:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({Uacute:"\xda",Uarr:"\u219f",Uarrocir:"\u2949",Ubrcy:"\u040e",Ubreve:"\u016c",Ucirc:"\xdb",Ucy:"\u0423",Udblac:"\u0170",Ugrave:"\xd9",Umacr:"\u016a",UnderBracket:"\u23b5",UnderParenthesis:"\u23dd",Uogon:"\u0172",UpArrowBar:"\u2912",UpArrowDownArrow:"\u21c5",UpEquilibrium:"\u296e",UpTeeArrow:"\u21a5",UpperLeftArrow:"\u2196",UpperRightArrow:"\u2197",Upsi:"\u03d2",Uring:"\u016e",Utilde:"\u0168",Uuml:"\xdc",uArr:"\u21d1",uHar:"\u2963",uacute:"\xfa",uarr:"\u2191",ubrcy:"\u045e",ubreve:"\u016d",ucirc:"\xfb",ucy:"\u0443",udarr:"\u21c5",udblac:"\u0171",udhar:"\u296e",ufisht:"\u297e",ugrave:"\xf9",uharl:"\u21bf",uharr:"\u21be",uhblk:"\u2580",ulcorn:"\u231c",ulcorner:"\u231c",ulcrop:"\u230f",ultri:"\u25f8",umacr:"\u016b",uml:"\xa8",uogon:"\u0173",uparrow:"\u2191",updownarrow:"\u2195",upharpoonleft:"\u21bf",upharpoonright:"\u21be",uplus:"\u228e",upsih:"\u03d2",upsilon:"\u03c5",urcorn:"\u231d",urcorner:"\u231d",urcrop:"\u230e",uring:"\u016f",urtri:"\u25f9",utdot:"\u22f0",utilde:"\u0169",utri:"\u25b5",utrif:"\u25b4",uuarr:"\u21c8",uuml:"\xfc",uwangle:"\u29a7"},"u")},517:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({VDash:"\u22ab",Vbar:"\u2aeb",Vcy:"\u0412",Vdashl:"\u2ae6",Verbar:"\u2016",Vert:"\u2016",VerticalLine:"|",VerticalSeparator:"\u2758",VeryThinSpace:"\u200a",vArr:"\u21d5",vBar:"\u2ae8",vBarv:"\u2ae9",vDash:"\u22a8",vangrt:"\u299c",varepsilon:"\u03f5",varkappa:"\u03f0",varnothing:"\u2205",varphi:"\u03d5",varpi:"\u03d6",varpropto:"\u221d",varr:"\u2195",varrho:"\u03f1",varsigma:"\u03c2",varsubsetneq:"\u228a\ufe00",varsubsetneqq:"\u2acb\ufe00",varsupsetneq:"\u228b\ufe00",varsupsetneqq:"\u2acc\ufe00",vartheta:"\u03d1",vartriangleleft:"\u22b2",vartriangleright:"\u22b3",vcy:"\u0432",vdash:"\u22a2",vee:"\u2228",veeeq:"\u225a",verbar:"|",vert:"|",vltri:"\u22b2",vnsub:"\u2282\u20d2",vnsup:"\u2283\u20d2",vprop:"\u221d",vrtri:"\u22b3",vsubnE:"\u2acb\ufe00",vsubne:"\u228a\ufe00",vsupnE:"\u2acc\ufe00",vsupne:"\u228b\ufe00",vzigzag:"\u299a"},"v")},336:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({Wcirc:"\u0174",wcirc:"\u0175",wedbar:"\u2a5f",wedge:"\u2227",wedgeq:"\u2259",wp:"\u2118",wr:"\u2240",wreath:"\u2240"},"w")},373:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({xcap:"\u22c2",xcirc:"\u25ef",xcup:"\u22c3",xdtri:"\u25bd",xhArr:"\u27fa",xharr:"\u27f7",xlArr:"\u27f8",xlarr:"\u27f5",xmap:"\u27fc",xnis:"\u22fb",xodot:"\u2a00",xoplus:"\u2a01",xotime:"\u2a02",xrArr:"\u27f9",xrarr:"\u27f6",xsqcup:"\u2a06",xuplus:"\u2a04",xutri:"\u25b3",xvee:"\u22c1",xwedge:"\u22c0"},"x")},215:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({YAcy:"\u042f",YIcy:"\u0407",YUcy:"\u042e",Yacute:"\xdd",Ycirc:"\u0176",Ycy:"\u042b",Yuml:"\u0178",yacute:"\xfd",yacy:"\u044f",ycirc:"\u0177",ycy:"\u044b",yicy:"\u0457",yucy:"\u044e",yuml:"\xff"},"y")},179:function(r,e,t){Object.defineProperty(e,"__esModule",{value:!0}),t(884).add({ZHcy:"\u0416",Zacute:"\u0179",Zcaron:"\u017d",Zcy:"\u0417",Zdot:"\u017b",ZeroWidthSpace:"\u200b",Zeta:"\u0396",zacute:"\u017a",zcaron:"\u017e",zcy:"\u0437",zdot:"\u017c",zeetrf:"\u2128",zhcy:"\u0436",zwj:"\u200d",zwnj:"\u200c"},"z")},884:function(r,e){Object.defineProperty(e,"__esModule",{value:!0}),e.options=MathJax._.util.Entities.options,e.entities=MathJax._.util.Entities.entities,e.add=MathJax._.util.Entities.add,e.remove=MathJax._.util.Entities.remove,e.translate=MathJax._.util.Entities.translate,e.numeric=MathJax._.util.Entities.numeric}},e={};function t(o){var a=e[o];if(void 0!==a)return a.exports;var s=e[o]={exports:{}};return r[o](s,s.exports,t),s.exports}t(349)}(); \ No newline at end of file diff --git a/docs/assets/vendor/mathjax/input/tex-full.js b/docs/assets/vendor/mathjax/input/tex-full.js new file mode 100644 index 0000000..de98c3a --- /dev/null +++ b/docs/assets/vendor/mathjax/input/tex-full.js @@ -0,0 +1,34 @@ +!function(){"use strict";var t={7205:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),a=this&&this.__assign||function(){return(a=Object.assign||function(t){for(var e,r=1,n=arguments.length;r0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i};Object.defineProperty(e,"__esModule",{value:!0}),e.TeX=void 0;var s=r(3309),l=r(9077),c=r(2982),u=r(199),p=r(8321),f=r(810),d=r(3466),h=r(6394),m=r(7251),g=r(6552);r(3606);var y=function(t){function e(r){void 0===r&&(r={});var n=this,o=i(l.separateOptions(r,e.OPTIONS,c.FindTeX.OPTIONS),3),a=o[0],s=o[1],p=o[2];(n=t.call(this,s)||this).findTeX=n.options.FindTeX||new c.FindTeX(p);var f=n.options.packages,d=n.configuration=e.configure(f),g=n._parseOptions=new h.default(d,[n.options,m.TagsFactory.OPTIONS]);return l.userOptions(g.options,a),d.config(n),e.tags(g,d),n.postFilters.add(u.default.cleanSubSup,-6),n.postFilters.add(u.default.setInherited,-5),n.postFilters.add(u.default.moveLimits,-4),n.postFilters.add(u.default.cleanStretchy,-3),n.postFilters.add(u.default.cleanAttributes,-2),n.postFilters.add(u.default.combineRelations,-1),n}return o(e,t),e.configure=function(t){var e=new g.ParserConfiguration(t);return e.init(),e},e.tags=function(t,e){m.TagsFactory.addTags(e.tags),m.TagsFactory.setDefault(t.options.tags),t.tags=m.TagsFactory.getDefault(),t.tags.configuration=t},e.prototype.setMmlFactory=function(e){t.prototype.setMmlFactory.call(this,e),this._parseOptions.nodeFactory.setMmlFactory(e)},Object.defineProperty(e.prototype,"parseOptions",{get:function(){return this._parseOptions},enumerable:!1,configurable:!0}),e.prototype.reset=function(t){void 0===t&&(t=0),this.parseOptions.tags.reset(t)},e.prototype.compile=function(t,e){this.parseOptions.clear(),this.executeFilters(this.preFilters,t,e,this.parseOptions);var r,n=t.display;this.latex=t.math,this.parseOptions.tags.startEquation(t);try{r=new f.default(this.latex,{display:n,isInner:!1},this.parseOptions).mml()}catch(t){if(!(t instanceof d.default))throw t;this.parseOptions.error=!0,r=this.options.formatError(this,t)}return r=this.parseOptions.nodeFactory.create("node","math",[r]),n&&p.default.setAttribute(r,"display","block"),this.parseOptions.tags.finishEquation(t),this.parseOptions.root=r,this.executeFilters(this.postFilters,t,e,this.parseOptions),this.mathNode=this.parseOptions.root,this.mathNode},e.prototype.findMath=function(t){return this.findTeX.findMath(t)},e.prototype.formatError=function(t){var e=t.message.replace(/\n.*/,"");return this.parseOptions.nodeFactory.create("error",e,t.id,this.latex)},e.NAME="TeX",e.OPTIONS=a(a({},s.AbstractInputJax.OPTIONS),{FindTeX:null,packages:["base"],digits:/^(?:[0-9]+(?:\{,\}[0-9]{3})*(?:\.[0-9]*)?|\.[0-9]+)/,maxBuffer:5120,formatError:function(t,e){return t.formatError(e)}}),e}(s.AbstractInputJax);e.TeX=y},2160:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.AllPackages=void 0,r(3606),r(1313),r(3946),r(6701),r(3067),r(9267),r(1677),r(7404),r(9489),r(4151),r(2298),r(3274),r(6755),r(5246),r(153),r(1323),r(2200),r(9569),r(8405),r(9589),r(7368),r(82),r(1158),r(4325),"undefined"!=typeof MathJax&&MathJax.loader&&MathJax.loader.preLoad("[tex]/action","[tex]/ams","[tex]/amscd","[tex]/bbox","[tex]/boldsymbol","[tex]/braket","[tex]/bussproofs","[tex]/cancel","[tex]/color","[tex]/colorv2","[tex]/enclose","[tex]/extpfeil","[tex]/html","[tex]/mhchem","[tex]/newcommand","[tex]/noerrors","[tex]/noundefined","[tex]/physics","[tex]/unicode","[tex]/verb","[tex]/configmacros","[tex]/tagformat","[tex]/textmacros"),e.AllPackages=["base","action","ams","amscd","bbox","boldsymbol","braket","bussproofs","cancel","color","enclose","extpfeil","html","mhchem","newcommand","noerrors","noundefined","unicode","verb","configmacros","tagformat","textmacros"]},6552:function(t,e,r){var n=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},o=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i};Object.defineProperty(e,"__esModule",{value:!0}),e.ParserConfiguration=e.ConfigurationHandler=e.Configuration=void 0;var a,i=r(9077),s=r(2910),l=r(6898),c=r(4297),u=r(7251),p=function(){function t(t,e,r,n,o,a,i,s,l,c,u,p){void 0===e&&(e={}),void 0===r&&(r={}),void 0===n&&(n={}),void 0===o&&(o={}),void 0===a&&(a={}),void 0===i&&(i={}),void 0===s&&(s=[]),void 0===l&&(l=[]),void 0===c&&(c=null),void 0===u&&(u=null),this.name=t,this.handler=e,this.fallback=r,this.items=n,this.tags=o,this.options=a,this.nodes=i,this.preprocessors=s,this.postprocessors=l,this.initMethod=c,this.configMethod=u,this.priority=p,this.handler=Object.assign({character:[],delimiter:[],macro:[],environment:[]},e)}return t.makeProcessor=function(t,e){return Array.isArray(t)?t:[t,e]},t._create=function(e,r){var n=this;void 0===r&&(r={});var o=r.priority||c.PrioritizedList.DEFAULTPRIORITY,a=r.init?this.makeProcessor(r.init,o):null,i=r.config?this.makeProcessor(r.config,o):null,s=(r.preprocessors||[]).map((function(t){return n.makeProcessor(t,o)})),l=(r.postprocessors||[]).map((function(t){return n.makeProcessor(t,o)}));return new t(e,r.handler||{},r.fallback||{},r.items||{},r.tags||{},r.options||{},r.nodes||{},s,l,a,i,o)},t.create=function(e,r){void 0===r&&(r={});var n=t._create(e,r);return a.set(e,n),n},t.local=function(e){return void 0===e&&(e={}),t._create("",e)},Object.defineProperty(t.prototype,"init",{get:function(){return this.initMethod?this.initMethod[0]:null},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"config",{get:function(){return this.configMethod?this.configMethod[0]:null},enumerable:!1,configurable:!0}),t}();e.Configuration=p,function(t){var e=new Map;t.set=function(t,r){e.set(t,r)},t.get=function(t){return e.get(t)},t.keys=function(){return e.keys()}}(a=e.ConfigurationHandler||(e.ConfigurationHandler={}));var f=function(){function t(t){var e,r,o,a;this.initMethod=new l.FunctionList,this.configMethod=new l.FunctionList,this.configurations=new c.PrioritizedList,this.handlers=new s.SubHandlers,this.items={},this.tags={},this.options={},this.nodes={};try{for(var i=n(t.slice().reverse()),u=i.next();!u.done;u=i.next()){var p=u.value;this.addPackage(p)}}catch(t){e={error:t}}finally{try{u&&!u.done&&(r=i.return)&&r.call(i)}finally{if(e)throw e.error}}try{for(var f=n(this.configurations),d=f.next();!d.done;d=f.next()){var h=d.value,m=h.item,g=h.priority;this.append(m,g)}}catch(t){o={error:t}}finally{try{d&&!d.done&&(a=f.return)&&a.call(f)}finally{if(o)throw o.error}}}return t.prototype.init=function(){this.initMethod.execute(this)},t.prototype.config=function(t){var e,r;this.configMethod.execute(this,t);try{for(var o=n(this.configurations),a=o.next();!a.done;a=o.next()){var i=a.value;this.addFilters(t,i.item)}}catch(t){e={error:t}}finally{try{a&&!a.done&&(r=o.return)&&r.call(o)}finally{if(e)throw e.error}}},t.prototype.addPackage=function(t){var e="string"==typeof t?t:t[0],r=a.get(e);r&&this.configurations.add(r,"string"==typeof t?r.priority:t[1])},t.prototype.add=function(t,e,r){var o,a;void 0===r&&(r={}),this.append(t),this.configurations.add(t,t.priority),this.init();var s=e.parseOptions;s.nodeFactory.setCreators(t.nodes);try{for(var l=n(Object.keys(t.items)),c=l.next();!c.done;c=l.next()){var p=c.value;s.itemFactory.setNodeClass(p,t.items[p])}}catch(t){o={error:t}}finally{try{c&&!c.done&&(a=l.return)&&a.call(l)}finally{if(o)throw o.error}}u.TagsFactory.addTags(t.tags),i.defaultOptions(s.options,t.options),i.userOptions(s.options,r),this.addFilters(e,t),t.config&&t.config(this,e)},t.prototype.append=function(t,e){e=e||t.priority,t.initMethod&&this.initMethod.add(t.initMethod[0],t.initMethod[1]),t.configMethod&&this.configMethod.add(t.configMethod[0],t.configMethod[1]),this.handlers.add(t.handler,t.fallback,e),Object.assign(this.items,t.items),Object.assign(this.tags,t.tags),i.defaultOptions(this.options,t.options),Object.assign(this.nodes,t.nodes)},t.prototype.addFilters=function(t,e){var r,a,i,s;try{for(var l=n(e.preprocessors),c=l.next();!c.done;c=l.next()){var u=o(c.value,2),p=u[0],f=u[1];t.preFilters.add(p,f)}}catch(t){r={error:t}}finally{try{c&&!c.done&&(a=l.return)&&a.call(l)}finally{if(r)throw r.error}}try{for(var d=n(e.postprocessors),h=d.next();!h.done;h=d.next()){var m=o(h.value,2),g=m[0];f=m[1];t.postFilters.add(g,f)}}catch(t){i={error:t}}finally{try{h&&!h.done&&(s=d.return)&&s.call(d)}finally{if(i)throw i.error}}},t}();e.ParserConfiguration=f},199:function(t,e,r){var n=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0});var o,a=r(8921),i=r(8321);!function(t){t.cleanStretchy=function(t){var e,r,o=t.data;try{for(var a=n(o.getList("fixStretchy")),s=a.next();!s.done;s=a.next()){var l=s.value;if(i.default.getProperty(l,"fixStretchy")){var c=i.default.getForm(l);c&&c[3]&&c[3].stretchy&&i.default.setAttribute(l,"stretchy",!1);var u=l.parent;if(!(i.default.getTexClass(l)||c&&c[2])){var p=o.nodeFactory.create("node","TeXAtom",[l]);u.replaceChild(p,l),p.inheritAttributesFrom(l)}i.default.removeProperties(l,"fixStretchy")}}}catch(t){e={error:t}}finally{try{s&&!s.done&&(r=a.return)&&r.call(a)}finally{if(e)throw e.error}}},t.cleanAttributes=function(t){t.data.root.walkTree((function(t,e){var r,o,a=t.attributes;if(a)try{for(var i=n(a.getExplicitNames()),s=i.next();!s.done;s=i.next()){var l=s.value;a.attributes[l]===t.attributes.getInherited(l)&&delete a.attributes[l]}}catch(t){r={error:t}}finally{try{s&&!s.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}}),{})},t.combineRelations=function(t){var o,s;try{for(var l=n(t.data.getList("mo")),c=l.next();!c.done;c=l.next()){var u=c.value;if(!u.getProperty("relationsCombined")&&u.parent&&(!u.parent||i.default.isType(u.parent,"mrow"))&&i.default.getTexClass(u)===a.TEXCLASS.REL){for(var p=u.parent,f=void 0,d=p.childNodes,h=d.indexOf(u)+1,m=i.default.getProperty(u,"variantForm");h0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i};Object.defineProperty(e,"__esModule",{value:!0}),e.FindTeX=void 0;var i=r(9649),s=r(6720),l=r(4769),c=function(t){function e(e){var r=t.call(this,e)||this;return r.getPatterns(),r}return o(e,t),e.prototype.getPatterns=function(){var t=this,e=this.options,r=[],n=[],o=[];this.end={},this.env=this.sub=0;var a=1;e.inlineMath.forEach((function(e){return t.addPattern(r,e,!1)})),e.displayMath.forEach((function(e){return t.addPattern(r,e,!0)})),r.length&&n.push(r.sort(s.sortLength).join("|")),e.processEnvironments&&(n.push("\\\\begin\\s*\\{([^}]*)\\}"),this.env=a,a++),e.processEscapes&&o.push("\\\\([\\\\$])"),e.processRefs&&o.push("(\\\\(?:eq)?ref\\s*\\{[^}]*\\})"),o.length&&(n.push("("+o.join("|")+")"),this.sub=a),this.start=new RegExp(n.join("|"),"g"),this.hasPatterns=n.length>0},e.prototype.addPattern=function(t,e,r){var n=a(e,2),o=n[0],i=n[1];t.push(s.quotePattern(o)),this.end[o]=[i,r,this.endPattern(i)]},e.prototype.endPattern=function(t,e){return new RegExp((e||s.quotePattern(t))+"|\\\\(?:[a-zA-Z]|.)|[{}]","g")},e.prototype.findEnd=function(t,e,r,n){for(var o,i=a(n,3),s=i[0],c=i[1],u=i[2],p=u.lastIndex=r.index+r[0].length,f=0;o=u.exec(t);){if((o[1]||o[0])===s&&0===f)return l.protoItem(r[0],t.substr(p,o.index-p),o[0],e,r.index,o.index+o[0].length,c);"{"===o[0]?f++:"}"===o[0]&&f&&f--}return null},e.prototype.findMathInString=function(t,e,r){var n,o;for(this.start.lastIndex=0;n=this.start.exec(r);){if(void 0!==n[this.env]&&this.env){var a="\\\\end\\s*(\\{"+s.quotePattern(n[this.env])+"\\})";(o=this.findEnd(r,e,n,["{"+n[this.env]+"}",!0,this.endPattern(null,a)]))&&(o.math=o.open+o.math+o.close,o.open=o.close="")}else if(void 0!==n[this.sub]&&this.sub){var i=n[this.sub];a=n.index+n[this.sub].length;o=2===i.length?l.protoItem("",i.substr(1),"",e,n.index,a):l.protoItem("",i,"",e,n.index,a,!1)}else o=this.findEnd(r,e,n,this.end[n[0]]);o&&(t.push(o),this.start.lastIndex=o.end.n)}},e.prototype.findMath=function(t){var e=[];if(this.hasPatterns)for(var r=0,n=t.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},o=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i};Object.defineProperty(e,"__esModule",{value:!0}),e.SubHandlers=e.SubHandler=e.MapHandler=void 0;var a,i=r(4297),s=r(6898);!function(t){var e=new Map;t.register=function(t){e.set(t.name,t)},t.getMap=function(t){return e.get(t)}}(a=e.MapHandler||(e.MapHandler={}));var l=function(){function t(){this._configuration=new i.PrioritizedList,this._fallback=new s.FunctionList}return t.prototype.add=function(t,e,r){var o,s;void 0===r&&(r=i.PrioritizedList.DEFAULTPRIORITY);try{for(var l=n(t.slice().reverse()),c=l.next();!c.done;c=l.next()){var u=c.value,p=a.getMap(u);if(!p)return void this.warn("Configuration "+u+" not found! Omitted.");this._configuration.add(p,r)}}catch(t){o={error:t}}finally{try{c&&!c.done&&(s=l.return)&&s.call(l)}finally{if(o)throw o.error}}e&&this._fallback.add(e,r)},t.prototype.parse=function(t){var e,r;try{for(var a=n(this._configuration),i=a.next();!i.done;i=a.next()){var s=i.value.item.parse(t);if(s)return s}}catch(t){e={error:t}}finally{try{i&&!i.done&&(r=a.return)&&r.call(a)}finally{if(e)throw e.error}}var l=o(t,2),c=l[0],u=l[1];this._fallback.toArray()[0].item(c,u)},t.prototype.lookup=function(t){var e=this.applicable(t);return e?e.lookup(t):null},t.prototype.contains=function(t){return!!this.applicable(t)},t.prototype.toString=function(){var t,e,r=[];try{for(var o=n(this._configuration),a=o.next();!a.done;a=o.next()){var i=a.value.item;r.push(i.name)}}catch(e){t={error:e}}finally{try{a&&!a.done&&(e=o.return)&&e.call(o)}finally{if(t)throw t.error}}return r.join(", ")},t.prototype.applicable=function(t){var e,r;try{for(var o=n(this._configuration),a=o.next();!a.done;a=o.next()){var i=a.value.item;if(i.contains(t))return i}}catch(t){e={error:t}}finally{try{a&&!a.done&&(r=o.return)&&r.call(o)}finally{if(e)throw e.error}}return null},t.prototype.retrieve=function(t){var e,r;try{for(var o=n(this._configuration),a=o.next();!a.done;a=o.next()){var i=a.value.item;if(i.name===t)return i}}catch(t){e={error:t}}finally{try{a&&!a.done&&(r=o.return)&&r.call(o)}finally{if(e)throw e.error}}return null},t.prototype.warn=function(t){console.log("TexParser Warning: "+t)},t}();e.SubHandler=l;var c=function(){function t(){this.map=new Map}return t.prototype.add=function(t,e,r){var o,a;void 0===r&&(r=i.PrioritizedList.DEFAULTPRIORITY);try{for(var s=n(Object.keys(t)),c=s.next();!c.done;c=s.next()){var u=c.value,p=this.get(u);p||(p=new l,this.set(u,p)),p.add(t[u],e[u],r)}}catch(t){o={error:t}}finally{try{c&&!c.done&&(a=s.return)&&a.call(s)}finally{if(o)throw o.error}}},t.prototype.set=function(t,e){this.map.set(t,e)},t.prototype.get=function(t){return this.map.get(t)},t.prototype.retrieve=function(t){var e,r;try{for(var o=n(this.map.values()),a=o.next();!a.done;a=o.next()){var i=a.value.retrieve(t);if(i)return i}}catch(t){e={error:t}}finally{try{a&&!a.done&&(r=o.return)&&r.call(o)}finally{if(e)throw e.error}}return null},t.prototype.keys=function(){return this.map.keys()},t}();e.SubHandlers=c},8644:function(t,e,r){var n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},o=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},o=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},a=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},o=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},o=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0});var i=r(3239),s=r(8644),l=r(9077),c=function(){function t(t,e){void 0===e&&(e=[]),this.options={},this.packageData=new Map,this.parsers=[],this.root=null,this.nodeLists={},this.error=!1,this.handlers=t.handlers,this.nodeFactory=new s.NodeFactory,this.nodeFactory.configuration=this,this.nodeFactory.setCreators(t.nodes),this.itemFactory=new i.default(t.items),this.itemFactory.configuration=this,l.defaultOptions.apply(void 0,o([this.options],n(e))),l.defaultOptions(this.options,t.options)}return t.prototype.pushParser=function(t){this.parsers.unshift(t)},t.prototype.popParser=function(){this.parsers.shift()},Object.defineProperty(t.prototype,"parser",{get:function(){return this.parsers[0]},enumerable:!1,configurable:!0}),t.prototype.clear=function(){this.parsers=[],this.root=null,this.nodeLists={},this.error=!1,this.tags.resetTag()},t.prototype.addNode=function(t,e){var r=this.nodeLists[t];r||(r=this.nodeLists[t]=[]),r.push(e)},t.prototype.getList=function(t){var e,r,n=this.nodeLists[t]||[],o=[];try{for(var i=a(n),s=i.next();!s.done;s=i.next()){var l=s.value;this.inTree(l)&&o.push(l)}}catch(t){e={error:t}}finally{try{s&&!s.done&&(r=i.return)&&r.call(i)}finally{if(e)throw e.error}}return this.nodeLists[t]=o,o},t.prototype.inTree=function(t){for(;t&&t!==this.root;)t=t.parent;return!!t},t}();e.default=c},7702:function(t,e,r){var n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},o=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0});var a,i=r(8921),s=r(8321),l=r(810),c=r(3466),u=r(9029);!function(t){var e=7.2,r={em:function(t){return t},ex:function(t){return.43*t},pt:function(t){return t/10},pc:function(t){return 1.2*t},px:function(t){return t*e/72},in:function(t){return t*e},cm:function(t){return t*e/2.54},mm:function(t){return t*e/25.4},mu:function(t){return t/18}},a="([-+]?([.,]\\d+|\\d+([.,]\\d*)?))",p="(pt|em|ex|mu|px|mm|cm|in|pc)",f=RegExp("^\\s*"+a+"\\s*"+p+"\\s*$"),d=RegExp("^\\s*"+a+"\\s*"+p+" ?");function h(t,e){void 0===e&&(e=!1);var o=t.match(e?d:f);return o?function(t){var e=n(t,3),o=e[0],a=e[1],i=e[2];if("mu"!==a)return[o,a,i];return[m(r[a](parseFloat(o||"1"))).slice(0,-2),"em",i]}([o[1].replace(/,/,"."),o[4],o[0].length]):[null,null,0]}function m(t){return Math.abs(t)<6e-4?"0em":t.toFixed(3).replace(/\.?0+$/,"")+"em"}function g(t,e,r){"{"!==e&&"}"!==e||(e="\\"+e);var n="{\\bigg"+r+" "+e+"}",o="{\\big"+r+" "+e+"}";return new l.default("\\mathchoice"+n+o+o+o,{},t).mml()}function y(t,e,r){e=e.replace(/^\s+/,u.entities.nbsp).replace(/\s+$/,u.entities.nbsp);var n=t.create("text",e);return t.create("node","mtext",[],r,n)}function v(t,e,r){if(r.match(/^[a-z]/i)&&e.match(/(^|[^\\])(\\\\)*\\[a-z]+$/i)&&(e+=" "),e.length+r.length>t.configuration.options.maxBuffer)throw new c.default("MaxBufferSize","MathJax internal buffer size exceeded; is there a recursive macro call?");return e+r}function b(t,e){for(;e>0;)t=t.trim().slice(1,-1),e--;return t.trim()}function x(t,e){for(var r=t.length,n=0,o="",a=0,i=0,s=!0,l=!1;an&&(i=n)),n++;break;case"}":n&&n--,(s||l)&&(i--,l=!0),s=!1;break;default:if(!n&&-1!==e.indexOf(u))return[l?"true":b(o,i),u,t.slice(a)];s=!1,l=!1}o+=u}if(n)throw new c.default("ExtraOpenMissingClose","Extra open brace or missing close brace");return[l?"true":b(o,i),"",t.slice(a)]}t.matchDimen=h,t.dimen2em=function(t){var e=n(h(t),2),o=e[0],a=e[1],i=parseFloat(o||"1"),s=r[a];return s?s(i):0},t.Em=m,t.fenced=function(t,e,r,n,o,a){void 0===o&&(o=""),void 0===a&&(a="");var c,u=t.nodeFactory,p=u.create("node","mrow",[],{open:e,close:n,texClass:i.TEXCLASS.INNER});if(o)c=new l.default("\\"+o+"l"+e,t.parser.stack.env,t).mml();else{var f=u.create("text",e);c=u.create("node","mo",[],{fence:!0,stretchy:!0,symmetric:!0,texClass:i.TEXCLASS.OPEN},f)}if(s.default.appendChildren(p,[c,r]),o)c=new l.default("\\"+o+"r"+n,t.parser.stack.env,t).mml();else{var d=u.create("text",n);c=u.create("node","mo",[],{fence:!0,stretchy:!0,symmetric:!0,texClass:i.TEXCLASS.CLOSE},d)}return a&&c.attributes.set("mathcolor",a),s.default.appendChildren(p,[c]),p},t.fixedFence=function(t,e,r,n){var o=t.nodeFactory.create("node","mrow",[],{open:e,close:n,texClass:i.TEXCLASS.ORD});return e&&s.default.appendChildren(o,[g(t,e,"l")]),s.default.isType(r,"mrow")?s.default.appendChildren(o,s.default.getChildren(r)):s.default.appendChildren(o,[r]),n&&s.default.appendChildren(o,[g(t,n,"r")]),o},t.mathPalette=g,t.fixInitialMO=function(t,e){for(var r=0,n=e.length;r1&&(u=[t.create("node","mrow",u)]),u},t.internalText=y,t.trimSpaces=function(t){if("string"!=typeof t)return t;var e=t.trim();return e.match(/\\$/)&&t.match(/ $/)&&(e+=" "),e},t.setArrayAlign=function(e,r){return"t"===(r=t.trimSpaces(r||""))?e.arraydef.align="baseline 1":"b"===r?e.arraydef.align="baseline -1":"c"===r?e.arraydef.align="center":r&&(e.arraydef.align=r),e},t.substituteArgs=function(t,e,r){for(var n="",o="",a=0;ae.length)throw new c.default("IllegalMacroParam","Illegal macro parameter reference");o=v(t,v(t,o,n),e[parseInt(i,10)-1]),n=""}else n+=i}return v(t,o,n)},t.addArgs=v,t.checkEqnEnv=function(t){if(t.stack.global.eqnenv)throw new c.default("ErroneousNestingEq","Erroneous nesting of equation structures");t.stack.global.eqnenv=!0},t.MmlFilterAttribute=function(t,e,r){return r},t.getFontDef=function(t){var e=t.stack.env.font;return e?{mathvariant:e}:{}},t.keyvalOptions=function(t,e,r){var a,i;void 0===e&&(e=null),void 0===r&&(r=!1);var s=function(t){var e,r,o,a,i,s={},l=t;for(;l;)a=(e=n(x(l,["=",","]),3))[0],o=e[1],l=e[2],"="===o?(i=(r=n(x(l,[","]),3))[0],o=r[1],l=r[2],i="false"===i||"true"===i?JSON.parse(i):i,s[a]=i):a&&(s[a]=!0);return s}(t);if(e)try{for(var l=o(Object.keys(s)),u=l.next();!u.done;u=l.next()){var p=u.value;if(!e.hasOwnProperty(p)){if(r)throw new c.default("InvalidOption","Invalid optional argument: %1",p);delete s[p]}}}catch(t){a={error:t}}finally{try{u&&!u.done&&(i=l.return)&&i.call(l)}finally{if(a)throw a.error}}return s}}(a||(a={})),e.default=a},9874:function(t,e,r){var n=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},o=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},a=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},i=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.BaseItem=e.MmlStack=void 0;var l=r(3466),c=function(){function t(t){this._nodes=t}return Object.defineProperty(t.prototype,"nodes",{get:function(){return this._nodes},enumerable:!1,configurable:!0}),t.prototype.Push=function(){for(var t,e=[],r=0;r0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},s=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.TagsFactory=e.AllTags=e.NoTags=e.AbstractTags=e.TagInfo=e.Label=void 0;var i=r(810),s=function(t,e){void 0===t&&(t="???"),void 0===e&&(e=""),this.tag=t,this.id=e};e.Label=s;var l=function(t,e,r,n,o,a,i,s){void 0===t&&(t=""),void 0===e&&(e=!1),void 0===r&&(r=!1),void 0===n&&(n=null),void 0===o&&(o=""),void 0===a&&(a=""),void 0===i&&(i=!1),void 0===s&&(s=""),this.env=t,this.taggable=e,this.defaultTags=r,this.tag=n,this.tagId=o,this.tagFormat=a,this.noTag=i,this.labelId=s};e.TagInfo=l;var c=function(){function t(){this.counter=0,this.allCounter=0,this.configuration=null,this.ids={},this.allIds={},this.labels={},this.allLabels={},this.redo=!1,this.refUpdate=!1,this.currentTag=new l,this.history=[],this.stack=[],this.enTag=function(t,e){var r=this.configuration.nodeFactory,n=r.create("node","mtd",[t]),o=r.create("node","mlabeledtr",[e,n]);return r.create("node","mtable",[o],{side:this.configuration.options.tagSide,minlabelspacing:this.configuration.options.tagIndent,displaystyle:!0})}}return t.prototype.start=function(t,e,r){this.currentTag&&this.stack.push(this.currentTag),this.currentTag=new l(t,e,r)},Object.defineProperty(t.prototype,"env",{get:function(){return this.currentTag.env},enumerable:!1,configurable:!0}),t.prototype.end=function(){this.history.push(this.currentTag),this.currentTag=this.stack.pop()},t.prototype.tag=function(t,e){this.currentTag.tag=t,this.currentTag.tagFormat=e?t:this.formatTag(t),this.currentTag.noTag=!1},t.prototype.notag=function(){this.tag("",!0),this.currentTag.noTag=!0},Object.defineProperty(t.prototype,"noTag",{get:function(){return this.currentTag.noTag},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"label",{get:function(){return this.currentTag.labelId},set:function(t){this.currentTag.labelId=t},enumerable:!1,configurable:!0}),t.prototype.formatUrl=function(t,e){return e+"#"+encodeURIComponent(t)},t.prototype.formatTag=function(t){return"("+t+")"},t.prototype.formatId=function(t){return"mjx-eqn:"+t.replace(/\s/g,"_")},t.prototype.formatNumber=function(t){return t.toString()},t.prototype.autoTag=function(){null==this.currentTag.tag&&(this.counter++,this.tag(this.formatNumber(this.counter),!1))},t.prototype.clearTag=function(){this.label="",this.tag(null,!0),this.currentTag.tagId=""},t.prototype.getTag=function(t){if(void 0===t&&(t=!1),t)return this.autoTag(),this.makeTag();var e=this.currentTag;return e.taggable&&!e.noTag&&(e.defaultTags&&this.autoTag(),e.tag)?this.makeTag():null},t.prototype.resetTag=function(){this.history=[],this.redo=!1,this.refUpdate=!1,this.clearTag()},t.prototype.reset=function(t){void 0===t&&(t=0),this.resetTag(),this.counter=this.allCounter=t,this.allLabels={},this.allIds={}},t.prototype.startEquation=function(t){this.history=[],this.stack=[],this.clearTag(),this.currentTag=new l("",void 0,void 0),this.labels={},this.ids={},this.counter=this.allCounter,this.redo=!1;var e=t.inputData.recompile;e&&(this.refUpdate=!0,this.counter=e.counter)},t.prototype.finishEquation=function(t){this.redo&&(t.inputData.recompile={state:t.state(),counter:this.allCounter}),this.refUpdate||(this.allCounter=this.counter),Object.assign(this.allIds,this.ids),Object.assign(this.allLabels,this.labels)},t.prototype.finalize=function(t,e){if(!e.display||this.currentTag.env||null==this.currentTag.tag)return t;var r=this.makeTag();return this.enTag(t,r)},t.prototype.makeId=function(){this.currentTag.tagId=this.formatId(this.configuration.options.useLabelIds&&this.label||this.currentTag.tag)},t.prototype.makeTag=function(){this.makeId(),this.label&&(this.labels[this.label]=new s(this.currentTag.tag,this.currentTag.tagId));var t=new i.default("\\text{"+this.currentTag.tagFormat+"}",{},this.configuration).mml();return this.configuration.nodeFactory.create("node","mtd",[t],{id:this.currentTag.tagId})},t}();e.AbstractTags=c;var u=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.autoTag=function(){},e.prototype.getTag=function(){return this.currentTag.tag?t.prototype.getTag.call(this):null},e}(c);e.NoTags=u;var p=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.finalize=function(t,e){if(!e.display||this.history.find((function(t){return t.taggable})))return t;var r=this.getTag(!0);return this.enTag(t,r)},e}(c);e.AllTags=p,function(t){var e=new Map([["none",u],["all",p]]),r="none";t.OPTIONS={tags:r,tagSide:"right",tagIndent:"0.8em",multlineWidth:"85%",useLabelIds:!0,ignoreDuplicateLabels:!1},t.add=function(t,r){e.set(t,r)},t.addTags=function(e){var r,n;try{for(var o=a(Object.keys(e)),i=o.next();!i.done;i=o.next()){var s=i.value;t.add(s,e[s])}}catch(t){r={error:t}}finally{try{i&&!i.done&&(n=o.return)&&n.call(o)}finally{if(r)throw r.error}}},t.create=function(t){var n=e.get(t)||e.get(r);if(!n)throw Error("Unknown tags class");return new n},t.setDefault=function(t){r=t},t.getDefault=function(){return t.create(r)}}(e.TagsFactory||(e.TagsFactory={}))},7007:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.TexConstant=void 0,function(t){t.Variant={NORMAL:"normal",BOLD:"bold",ITALIC:"italic",BOLDITALIC:"bold-italic",DOUBLESTRUCK:"double-struck",FRAKTUR:"fraktur",BOLDFRAKTUR:"bold-fraktur",SCRIPT:"script",BOLDSCRIPT:"bold-script",SANSSERIF:"sans-serif",BOLDSANSSERIF:"bold-sans-serif",SANSSERIFITALIC:"sans-serif-italic",SANSSERIFBOLDITALIC:"sans-serif-bold-italic",MONOSPACE:"monospace",INITIAL:"inital",TAILED:"tailed",LOOPED:"looped",STRETCHED:"stretched",CALLIGRAPHIC:"-tex-calligraphic",BOLDCALLIGRAPHIC:"-tex-bold-calligraphic",OLDSTYLE:"-tex-oldstyle",BOLDOLDSTYLE:"-tex-bold-oldstyle",MATHITALIC:"-tex-mathit"},t.Form={PREFIX:"prefix",INFIX:"infix",POSTFIX:"postfix"},t.LineBreak={AUTO:"auto",NEWLINE:"newline",NOBREAK:"nobreak",GOODBREAK:"goodbreak",BADBREAK:"badbreak"},t.LineBreakStyle={BEFORE:"before",AFTER:"after",DUPLICATE:"duplicate",INFIXLINBREAKSTYLE:"infixlinebreakstyle"},t.IndentAlign={LEFT:"left",CENTER:"center",RIGHT:"right",AUTO:"auto",ID:"id",INDENTALIGN:"indentalign"},t.IndentShift={INDENTSHIFT:"indentshift"},t.LineThickness={THIN:"thin",MEDIUM:"medium",THICK:"thick"},t.Notation={LONGDIV:"longdiv",ACTUARIAL:"actuarial",PHASORANGLE:"phasorangle",RADICAL:"radical",BOX:"box",ROUNDEDBOX:"roundedbox",CIRCLE:"circle",LEFT:"left",RIGHT:"right",TOP:"top",BOTTOM:"bottom",UPDIAGONALSTRIKE:"updiagonalstrike",DOWNDIAGONALSTRIKE:"downdiagonalstrike",VERTICALSTRIKE:"verticalstrike",HORIZONTALSTRIKE:"horizontalstrike",NORTHEASTARROW:"northeastarrow",MADRUWB:"madruwb",UPDIAGONALARROW:"updiagonalarrow"},t.Align={TOP:"top",BOTTOM:"bottom",CENTER:"center",BASELINE:"baseline",AXIS:"axis",LEFT:"left",RIGHT:"right"},t.Lines={NONE:"none",SOLID:"solid",DASHED:"dashed"},t.Side={LEFT:"left",RIGHT:"right",LEFTOVERLAP:"leftoverlap",RIGHTOVERLAP:"rightoverlap"},t.Width={AUTO:"auto",FIT:"fit"},t.Actiontype={TOGGLE:"toggle",STATUSLINE:"statusline",TOOLTIP:"tooltip",INPUT:"input"},t.Overflow={LINBREAK:"linebreak",SCROLL:"scroll",ELIDE:"elide",TRUNCATE:"truncate",SCALE:"scale"},t.Unit={EM:"em",EX:"ex",PX:"px",IN:"in",CM:"cm",MM:"mm",PT:"pt",PC:"pc"}}(e.TexConstant||(e.TexConstant={}))},3466:function(t,e){Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(e,r){for(var n=[],o=2;o="0"&&i<="9")n[o]=r[parseInt(n[o],10)-1],"number"==typeof n[o]&&(n[o]=n[o].toString());else if("{"===i){if((i=n[o].substr(1))>="0"&&i<="9")n[o]=r[parseInt(n[o].substr(1,n[o].length-2),10)-1],"number"==typeof n[o]&&(n[o]=n[o].toString());else n[o].match(/^\{([a-z]+):%(\d+)\|(.*)\}$/)&&(n[o]="%"+n[o])}null==n[o]&&(n[o]="???")}return n.join("")},t.pattern=/%(\d+|\{\d+\}|\{[a-z]+:\%\d+(?:\|(?:%\{\d+\}|%.|[^\}])*)+\}|.)/g,t}();e.default=r},810:function(t,e,r){var n=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},o=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},a=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r0;)u+="rl",p.push("0em 0em"),f--;var d=p.join(" ");if(a)return e.AmsMethods.EqnArray(t,r,o,a,u,d);var h=e.AmsMethods.EqnArray(t,r,o,a,u,d);return n.default.setArrayAlign(h,l)},e.AmsMethods.Multline=function(t,e,r){t.Push(e),n.default.checkEqnEnv(t);var o=t.itemFactory.create("multline",r,t.stack);return o.arraydef={displaystyle:!0,rowspacing:".5em",columnwidth:"100%",width:t.options.multlineWidth,side:t.options.tagSide,minlabelspacing:t.options.tagIndent},o},e.NEW_OPS="ams-declare-ops",e.AmsMethods.HandleDeclareOp=function(t,r){var o=t.GetStar()?"":"\\nolimits\\SkipLimits",a=n.default.trimSpaces(t.GetArgument(r));"\\"===a.charAt(0)&&(a=a.substr(1));var i=t.GetArgument(r);i.match(/\\text/)||(i=i.replace(/\*/g,"\\text{*}").replace(/-/g,"\\text{-}")),t.configuration.handlers.retrieve(e.NEW_OPS).add(a,new l.Macro(a,e.AmsMethods.Macro,["\\mathop{\\rm "+i+"}"+o]))},e.AmsMethods.HandleOperatorName=function(t,e){var r=t.GetStar()?"":"\\nolimits\\SkipLimits",o=n.default.trimSpaces(t.GetArgument(e));o.match(/\\text/)||(o=o.replace(/\*/g,"\\text{*}").replace(/-/g,"\\text{-}")),t.string="\\mathop{\\rm "+o+"}"+r+" "+t.string.slice(t.i),t.i=0},e.AmsMethods.SkipLimits=function(t,e){var r=t.GetNext(),n=t.i;"\\"===r&&++t.i&&"limits"!==t.GetCS()&&(t.i=n)},e.AmsMethods.MultiIntegral=function(t,e,r){var n=t.GetNext();if("\\"===n){var o=t.i;n=t.GetArgument(e),t.i=o,"\\limits"===n&&(r="\\idotsint"===e?"\\!\\!\\mathop{\\,\\,"+r+"}":"\\!\\!\\!\\mathop{\\,\\,\\,"+r+"}")}t.string=r+" "+t.string.slice(t.i),t.i=0},e.AmsMethods.xArrow=function(t,e,r,a,s){var l={width:"+"+n.default.Em((a+s)/18),lspace:n.default.Em(a/18)},c=t.GetBrackets(e),p=t.ParseArg(e),f=t.create("node","mspace",[],{depth:".25em"}),d=t.create("token","mo",{stretchy:!0,texClass:u.TEXCLASS.REL},String.fromCodePoint(r));d=t.create("node","mstyle",[d],{scriptlevel:0});var h=t.create("node","munderover",[d]),m=t.create("node","mpadded",[p,f],l);if(o.default.setAttribute(m,"voffset","-.2em"),o.default.setAttribute(m,"height","-.2em"),o.default.setChild(h,h.over,m),c){var g=new i.default(c,t.stack.env,t.configuration).mml(),y=t.create("node","mspace",[],{height:".75em"});m=t.create("node","mpadded",[g,y],l),o.default.setAttribute(m,"voffset",".15em"),o.default.setAttribute(m,"depth","-.15em"),o.default.setChild(h,h.under,m)}o.default.setProperty(h,"subsupOK",!0),t.Push(h)},e.AmsMethods.HandleShove=function(t,e,r){var n=t.stack.Top();if("multline"!==n.kind)throw new s.default("CommandOnlyAllowedInEnv","%1 only allowed in %2 environment",t.currentCS,"multline");if(n.Size())throw new s.default("CommandAtTheBeginingOfLine","%1 must come at the beginning of the line",t.currentCS);n.setProperty("shove",r)},e.AmsMethods.CFrac=function(t,e){var r=n.default.trimSpaces(t.GetBrackets(e,"")),l=t.GetArgument(e),c=t.GetArgument(e),u={l:a.TexConstant.Align.LEFT,r:a.TexConstant.Align.RIGHT,"":""},p=new i.default("\\strut\\textstyle{"+l+"}",t.stack.env,t.configuration).mml(),f=new i.default("\\strut\\textstyle{"+c+"}",t.stack.env,t.configuration).mml(),d=t.create("node","mfrac",[p,f]);if(null==(r=u[r]))throw new s.default("IllegalAlign","Illegal alignment specified in %1",t.currentCS);r&&o.default.setProperties(d,{numalign:r,denomalign:r}),t.Push(d)},e.AmsMethods.Genfrac=function(t,e,r,a,i,l){null==r&&(r=t.GetDelimiterArg(e)),null==a&&(a=t.GetDelimiterArg(e)),null==i&&(i=t.GetArgument(e)),null==l&&(l=n.default.trimSpaces(t.GetArgument(e)));var c=t.ParseArg(e),u=t.ParseArg(e),p=t.create("node","mfrac",[c,u]);if(""!==i&&o.default.setAttribute(p,"linethickness",i),(r||a)&&(o.default.setProperty(p,"withDelims",!0),p=n.default.fixedFence(t.configuration,r,p,a)),""!==l){var f=parseInt(l,10),d=["D","T","S","SS"][f];if(null==d)throw new s.default("BadMathStyleFor","Bad math style for %1",t.currentCS);p=t.create("node","mstyle",[p]),"D"===d?o.default.setProperties(p,{displaystyle:!0,scriptlevel:0}):o.default.setProperties(p,{displaystyle:!1,scriptlevel:f-1})}t.Push(p)},e.AmsMethods.HandleTag=function(t,e){if(!t.tags.currentTag.taggable&&t.tags.env)throw new s.default("CommandNotAllowedInEnv","%1 not allowed in %2 environment",t.currentCS,t.tags.env);if(t.tags.currentTag.tag)throw new s.default("MultipleCommand","Multiple %1",t.currentCS);var r=t.GetStar(),o=n.default.trimSpaces(t.GetArgument(e));t.tags.tag(o,r)},e.AmsMethods.HandleNoTag=c.default.HandleNoTag,e.AmsMethods.HandleRef=c.default.HandleRef,e.AmsMethods.Macro=c.default.Macro,e.AmsMethods.Accent=c.default.Accent,e.AmsMethods.Tilde=c.default.Tilde,e.AmsMethods.Array=c.default.Array,e.AmsMethods.Spacer=c.default.Spacer,e.AmsMethods.NamedOp=c.default.NamedOp,e.AmsMethods.EqnArray=c.default.EqnArray},6701:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.AmsCdConfiguration=void 0;var n=r(6552);r(7673),e.AmsCdConfiguration=n.Configuration.create("amscd",{handler:{character:["amscd_special"],macro:["amscd_macros"],environment:["amscd_environment"]},options:{amscd:{colspace:"5pt",rowspace:"5pt",harrowsize:"2.75em",varrowsize:"1.75em",hideHorizontalLabels:!1}}})},7673:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0});var n=r(7628),o=r(4708),a=r(7215);new n.EnvironmentMap("amscd_environment",o.default.environment,{CD:"CD"},a.default),new n.CommandMap("amscd_macros",{minCDarrowwidth:"minCDarrowwidth",minCDarrowheight:"minCDarrowheight"},a.default),new n.MacroMap("amscd_special",{"@":"arrow"},a.default)},7215:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0});var n=r(810),o=r(3606),a=r(8921),i=r(8321),s={CD:function(t,e){t.Push(e);var r=t.itemFactory.create("array"),n=t.configuration.options.amscd;return r.setProperties({minw:t.stack.env.CD_minw||n.harrowsize,minh:t.stack.env.CD_minh||n.varrowsize}),r.arraydef={columnalign:"center",columnspacing:n.colspace,rowspacing:n.rowspace,displaystyle:!0},r},arrow:function(t,e){var r=t.string.charAt(t.i);if(!r.match(/[>":"\u2192","<":"\u2190",V:"\u2193",A:"\u2191"}[r],g=t.GetUpTo(e+r,r),y=t.GetUpTo(e+r,r);if(">"===r||"<"===r){if(c=t.create("token","mo",d,m),g||(g="\\kern "+u.getProperty("minw")),g||y){var v={width:".67em",lspace:".33em"};if(c=t.create("node","munderover",[c]),g){var b=new n.default(g,t.stack.env,t.configuration).mml(),x=t.create("node","mpadded",[b],v);i.default.setAttribute(x,"voffset",".1em"),i.default.setChild(c,c.over,x)}if(y){var _=new n.default(y,t.stack.env,t.configuration).mml();i.default.setChild(c,c.under,t.create("node","mpadded",[_],v))}t.configuration.options.amscd.hideHorizontalLabels&&(c=t.create("node","mpadded",c,{depth:0,height:".67em"}))}}else{var A=t.create("token","mo",h,m);c=A,(g||y)&&(c=t.create("node","mrow"),g&&i.default.appendChildren(c,[new n.default("\\scriptstyle\\llap{"+g+"}",t.stack.env,t.configuration).mml()]),A.texClass=a.TEXCLASS.ORD,i.default.appendChildren(c,[A]),y&&i.default.appendChildren(c,[new n.default("\\scriptstyle\\rlap{"+y+"}",t.stack.env,t.configuration).mml()]))}}c&&t.Push(c),s.cell(t,e)},cell:function(t,e){var r=t.stack.Top();(r.table||[]).length%2==0&&0===(r.row||[]).length&&t.Push(t.create("node","mpadded",[],{height:"8.5pt",depth:"2pt"})),t.Push(t.itemFactory.create("cell").setProperties({isEntry:!0,name:e}))},minCDarrowwidth:function(t,e){t.stack.env.CD_minw=t.GetDimen(e)},minCDarrowheight:function(t,e){t.stack.env.CD_minh=t.GetDimen(e)}};e.default=s},1451:function(t,e,r){var n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},o=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.AutoloadConfiguration=void 0;var a=r(6552),i=r(7628),s=r(4237),l=r(4303),c=r(1993),u=r(9077);function p(t,e,r,a){var i,s,u,p;if(c.Package.packages.has(t.options.require.prefix+r)){var h=t.options.autoload[r],m=n(2===h.length&&Array.isArray(h[0])?h:[h,[]],2),g=m[0],y=m[1];try{for(var v=o(g),b=v.next();!b.done;b=v.next()){var x=b.value;f.remove(x)}}catch(t){i={error:t}}finally{try{b&&!b.done&&(s=v.return)&&s.call(v)}finally{if(i)throw i.error}}try{for(var _=o(y),A=_.next();!A.done;A=_.next()){var M=A.value;d.remove(M)}}catch(t){u={error:t}}finally{try{A&&!A.done&&(p=_.return)&&p.call(_)}finally{if(u)throw u.error}}t.string=(a?e+" ":"\\begin{"+e.slice(1)+"}")+t.string.slice(t.i),t.i=0}l.RequireLoad(t,r)}var f=new i.CommandMap("autoload-macros",{},{}),d=new i.CommandMap("autoload-environments",{},{});e.AutoloadConfiguration=a.Configuration.create("autoload",{handler:{macro:["autoload-macros"],environment:["autoload-environments"]},options:{autoload:u.expandable({action:["toggle","mathtip","texttip"],amscd:[[],["CD"]],bbox:["bbox"],boldsymbol:["boldsymbol"],braket:["bra","ket","braket","set","Bra","Ket","Braket","Set","ketbra","Ketbra"],bussproofs:[[],["prooftree"]],cancel:["cancel","bcancel","xcancel","cancelto"],color:["color","definecolor","textcolor","colorbox","fcolorbox"],enclose:["enclose"],extpfeil:["xtwoheadrightarrow","xtwoheadleftarrow","xmapsto","xlongequal","xtofrom","Newextarrow"],html:["href","class","style","cssId"],mhchem:["ce","pu"],newcommand:["newcommand","renewcommand","newenvironment","renewenvironment","def","let"],unicode:["unicode"],verb:["verb"]})},config:function(t,e){var r,a,i,c,u,h,m=e.parseOptions,g=m.handlers.get("macro"),y=m.handlers.get("environment"),v=m.options.autoload;m.packageData.set("autoload",{Autoload:p});try{for(var b=o(Object.keys(v)),x=b.next();!x.done;x=b.next()){var _=x.value,A=v[_],M=n(2===A.length&&Array.isArray(A[0])?A:[A,[]],2),C=M[0],w=M[1];try{for(var S=(i=void 0,o(C)),P=S.next();!P.done;P=S.next()){var T=P.value;g.lookup(T)&&"color"!==T||f.add(T,new s.Macro(T,p,[_,!0]))}}catch(t){i={error:t}}finally{try{P&&!P.done&&(c=S.return)&&c.call(S)}finally{if(i)throw i.error}}try{for(var O=(u=void 0,o(w)),k=O.next();!k.done;k=O.next()){var E=k.value;y.lookup(E)||d.add(E,new s.Macro(E,p,[_,!1]))}}catch(t){u={error:t}}finally{try{k&&!k.done&&(h=O.return)&&h.call(O)}finally{if(u)throw u.error}}}}catch(t){r={error:t}}finally{try{x&&!x.done&&(a=b.return)&&a.call(b)}finally{if(r)throw r.error}}m.packageData.get("require")||l.RequireConfiguration.config(t,e)},init:function(t){t.options.require||u.defaultOptions(t.options,l.RequireConfiguration.options)},priority:10})},3606:function(t,e,r){var n,o,a=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.BaseConfiguration=e.BaseTags=e.Other=void 0;var i=r(6552),s=r(2910),l=r(3466),c=r(8321),u=r(7628),p=r(8389),f=r(7251);function d(t,e){var r=t.stack.env.font?{mathvariant:t.stack.env.font}:{},n=s.MapHandler.getMap("remap").lookup(e),o=t.create("token","mo",r,n?n.char:e);c.default.setProperty(o,"fixStretchy",!0),t.configuration.addNode("fixStretchy",o),t.Push(o)}r(4962),new u.CharacterMap("remap",null,{"-":"\u2212","*":"\u2217","`":"\u2018"}),e.Other=d;var h=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return a(e,t),e}(f.AbstractTags);e.BaseTags=h,e.BaseConfiguration=i.Configuration.create("base",{handler:{character:["command","special","letter","digit"],delimiter:["delimiter"],macro:["delimiter","macros","mathchar0mi","mathchar0mo","mathchar7"],environment:["environment"]},fallback:{character:d,macro:function(t,e){throw new l.default("UndefinedControlSequence","Undefined control sequence %1","\\"+e)},environment:function(t,e){throw new l.default("UnknownEnv","Unknown environment '%1'",e)}},items:(o={},o[p.StartItem.prototype.kind]=p.StartItem,o[p.StopItem.prototype.kind]=p.StopItem,o[p.OpenItem.prototype.kind]=p.OpenItem,o[p.CloseItem.prototype.kind]=p.CloseItem,o[p.PrimeItem.prototype.kind]=p.PrimeItem,o[p.SubsupItem.prototype.kind]=p.SubsupItem,o[p.OverItem.prototype.kind]=p.OverItem,o[p.LeftItem.prototype.kind]=p.LeftItem,o[p.Middle.prototype.kind]=p.Middle,o[p.RightItem.prototype.kind]=p.RightItem,o[p.BeginItem.prototype.kind]=p.BeginItem,o[p.EndItem.prototype.kind]=p.EndItem,o[p.StyleItem.prototype.kind]=p.StyleItem,o[p.PositionItem.prototype.kind]=p.PositionItem,o[p.CellItem.prototype.kind]=p.CellItem,o[p.MmlItem.prototype.kind]=p.MmlItem,o[p.FnItem.prototype.kind]=p.FnItem,o[p.NotItem.prototype.kind]=p.NotItem,o[p.DotsItem.prototype.kind]=p.DotsItem,o[p.ArrayItem.prototype.kind]=p.ArrayItem,o[p.EqnArrayItem.prototype.kind]=p.EqnArrayItem,o[p.EquationItem.prototype.kind]=p.EquationItem,o),options:{maxMacros:1e3,baseURL:"undefined"==typeof document||0===document.getElementsByTagName("base").length?"":String(document.location).replace(/#.*$/,"")},tags:{base:h}})},8389:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),a=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},i=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r",succ:"\u227b",prec:"\u227a",approx:"\u2248",succeq:"\u2ab0",preceq:"\u2aaf",supset:"\u2283",subset:"\u2282",supseteq:"\u2287",subseteq:"\u2286",in:"\u2208",ni:"\u220b",notin:"\u2209",owns:"\u220b",gg:"\u226b",ll:"\u226a",sim:"\u223c",simeq:"\u2243",perp:"\u22a5",equiv:"\u2261",asymp:"\u224d",smile:"\u2323",frown:"\u2322",ne:"\u2260",neq:"\u2260",cong:"\u2245",doteq:"\u2250",bowtie:"\u22c8",models:"\u22a8",notChar:"\u29f8",Leftrightarrow:"\u21d4",Leftarrow:"\u21d0",Rightarrow:"\u21d2",leftrightarrow:"\u2194",leftarrow:"\u2190",gets:"\u2190",rightarrow:"\u2192",to:["\u2192",{accent:!1}],mapsto:"\u21a6",leftharpoonup:"\u21bc",leftharpoondown:"\u21bd",rightharpoonup:"\u21c0",rightharpoondown:"\u21c1",nearrow:"\u2197",searrow:"\u2198",nwarrow:"\u2196",swarrow:"\u2199",rightleftharpoons:"\u21cc",hookrightarrow:"\u21aa",hookleftarrow:"\u21a9",longleftarrow:"\u27f5",Longleftarrow:"\u27f8",longrightarrow:"\u27f6",Longrightarrow:"\u27f9",Longleftrightarrow:"\u27fa",longleftrightarrow:"\u27f7",longmapsto:"\u27fc",ldots:"\u2026",cdots:"\u22ef",vdots:"\u22ee",ddots:"\u22f1",dotsc:"\u2026",dotsb:"\u22ef",dotsm:"\u22ef",dotsi:"\u22ef",dotso:"\u2026",ldotp:[".",{texClass:s.TEXCLASS.PUNCT}],cdotp:["\u22c5",{texClass:s.TEXCLASS.PUNCT}],colon:[":",{texClass:s.TEXCLASS.PUNCT}]}),new n.CharacterMap("mathchar7",i.default.mathchar7,{Gamma:"\u0393",Delta:"\u0394",Theta:"\u0398",Lambda:"\u039b",Xi:"\u039e",Pi:"\u03a0",Sigma:"\u03a3",Upsilon:"\u03a5",Phi:"\u03a6",Psi:"\u03a8",Omega:"\u03a9",_:"_","#":"#",$:"$","%":"%","&":"&",And:"&"}),new n.DelimiterMap("delimiter",i.default.delimiter,{"(":"(",")":")","[":"[","]":"]","<":"\u27e8",">":"\u27e9","\\lt":"\u27e8","\\gt":"\u27e9","/":"/","|":["|",{texClass:s.TEXCLASS.ORD}],".":"","\\\\":"\\","\\lmoustache":"\u23b0","\\rmoustache":"\u23b1","\\lgroup":"\u27ee","\\rgroup":"\u27ef","\\arrowvert":"\u23d0","\\Arrowvert":"\u2016","\\bracevert":"\u23aa","\\Vert":["\u2016",{texClass:s.TEXCLASS.ORD}],"\\|":["\u2016",{texClass:s.TEXCLASS.ORD}],"\\vert":["|",{texClass:s.TEXCLASS.ORD}],"\\uparrow":"\u2191","\\downarrow":"\u2193","\\updownarrow":"\u2195","\\Uparrow":"\u21d1","\\Downarrow":"\u21d3","\\Updownarrow":"\u21d5","\\backslash":"\\","\\rangle":"\u27e9","\\langle":"\u27e8","\\rbrace":"}","\\lbrace":"{","\\}":"}","\\{":"{","\\rceil":"\u2309","\\lceil":"\u2308","\\rfloor":"\u230b","\\lfloor":"\u230a","\\lbrack":"[","\\rbrack":"]"}),new n.CommandMap("macros",{displaystyle:["SetStyle","D",!0,0],textstyle:["SetStyle","T",!1,0],scriptstyle:["SetStyle","S",!1,1],scriptscriptstyle:["SetStyle","SS",!1,2],rm:["SetFont",o.TexConstant.Variant.NORMAL],mit:["SetFont",o.TexConstant.Variant.ITALIC],oldstyle:["SetFont",o.TexConstant.Variant.OLDSTYLE],cal:["SetFont",o.TexConstant.Variant.CALLIGRAPHIC],it:["SetFont",o.TexConstant.Variant.MATHITALIC],bf:["SetFont",o.TexConstant.Variant.BOLD],bbFont:["SetFont",o.TexConstant.Variant.DOUBLESTRUCK],scr:["SetFont",o.TexConstant.Variant.SCRIPT],frak:["SetFont",o.TexConstant.Variant.FRAKTUR],sf:["SetFont",o.TexConstant.Variant.SANSSERIF],tt:["SetFont",o.TexConstant.Variant.MONOSPACE],mathrm:["MathFont",o.TexConstant.Variant.NORMAL],mathup:["MathFont",o.TexConstant.Variant.NORMAL],mathnormal:["MathFont",""],mathbf:["MathFont",o.TexConstant.Variant.BOLD],mathbfup:["MathFont",o.TexConstant.Variant.BOLD],mathit:["MathFont",o.TexConstant.Variant.MATHITALIC],mathbfit:["MathFont",o.TexConstant.Variant.BOLDITALIC],mathbb:["MathFont",o.TexConstant.Variant.DOUBLESTRUCK],Bbb:["MathFont",o.TexConstant.Variant.DOUBLESTRUCK],mathfrak:["MathFont",o.TexConstant.Variant.FRAKTUR],mathbffrak:["MathFont",o.TexConstant.Variant.BOLDFRAKTUR],mathscr:["MathFont",o.TexConstant.Variant.SCRIPT],mathbfscr:["MathFont",o.TexConstant.Variant.BOLDSCRIPT],mathsf:["MathFont",o.TexConstant.Variant.SANSSERIF],mathsfup:["MathFont",o.TexConstant.Variant.SANSSERIF],mathbfsf:["MathFont",o.TexConstant.Variant.BOLDSANSSERIF],mathbfsfup:["MathFont",o.TexConstant.Variant.BOLDSANSSERIF],mathsfit:["MathFont",o.TexConstant.Variant.SANSSERIFITALIC],mathbfsfit:["MathFont",o.TexConstant.Variant.SANSSERIFBOLDITALIC],mathtt:["MathFont",o.TexConstant.Variant.MONOSPACE],mathcal:["MathFont",o.TexConstant.Variant.CALLIGRAPHIC],mathbfcal:["MathFont",o.TexConstant.Variant.BOLDCALLIGRAPHIC],symrm:["MathFont",o.TexConstant.Variant.NORMAL],symup:["MathFont",o.TexConstant.Variant.NORMAL],symnormal:["MathFont",""],symbf:["MathFont",o.TexConstant.Variant.BOLD],symbfup:["MathFont",o.TexConstant.Variant.BOLD],symit:["MathFont",o.TexConstant.Variant.ITALIC],symbfit:["MathFont",o.TexConstant.Variant.BOLDITALIC],symbb:["MathFont",o.TexConstant.Variant.DOUBLESTRUCK],symfrak:["MathFont",o.TexConstant.Variant.FRAKTUR],symbffrak:["MathFont",o.TexConstant.Variant.BOLDFRAKTUR],symscr:["MathFont",o.TexConstant.Variant.SCRIPT],symbfscr:["MathFont",o.TexConstant.Variant.BOLDSCRIPT],symsf:["MathFont",o.TexConstant.Variant.SANSSERIF],symsfup:["MathFont",o.TexConstant.Variant.SANSSERIF],symbfsf:["MathFont",o.TexConstant.Variant.BOLDSANSSERIF],symbfsfup:["MathFont",o.TexConstant.Variant.BOLDSANSSERIF],symsfit:["MathFont",o.TexConstant.Variant.SANSSERIFITALIC],symbfsfit:["MathFont",o.TexConstant.Variant.SANSSERIFBOLDITALIC],symtt:["MathFont",o.TexConstant.Variant.MONOSPACE],symcal:["MathFont",o.TexConstant.Variant.CALLIGRAPHIC],symbfcal:["MathFont",o.TexConstant.Variant.BOLDCALLIGRAPHIC],textrm:["HBox",null,o.TexConstant.Variant.NORMAL],textup:["HBox",null,o.TexConstant.Variant.NORMAL],textnormal:["HBox"],textit:["HBox",null,o.TexConstant.Variant.ITALIC],textbf:["HBox",null,o.TexConstant.Variant.BOLD],textsf:["HBox",null,o.TexConstant.Variant.SANSSERIF],texttt:["HBox",null,o.TexConstant.Variant.MONOSPACE],tiny:["SetSize",.5],Tiny:["SetSize",.6],scriptsize:["SetSize",.7],small:["SetSize",.85],normalsize:["SetSize",1],large:["SetSize",1.2],Large:["SetSize",1.44],LARGE:["SetSize",1.73],huge:["SetSize",2.07],Huge:["SetSize",2.49],arcsin:"NamedFn",arccos:"NamedFn",arctan:"NamedFn",arg:"NamedFn",cos:"NamedFn",cosh:"NamedFn",cot:"NamedFn",coth:"NamedFn",csc:"NamedFn",deg:"NamedFn",det:"NamedOp",dim:"NamedFn",exp:"NamedFn",gcd:"NamedOp",hom:"NamedFn",inf:"NamedOp",ker:"NamedFn",lg:"NamedFn",lim:"NamedOp",liminf:["NamedOp","lim inf"],limsup:["NamedOp","lim sup"],ln:"NamedFn",log:"NamedFn",max:"NamedOp",min:"NamedOp",Pr:"NamedOp",sec:"NamedFn",sin:"NamedFn",sinh:"NamedFn",sup:"NamedOp",tan:"NamedFn",tanh:"NamedFn",limits:["Limits",1],nolimits:["Limits",0],overline:["UnderOver","2015"],underline:["UnderOver","2015"],overbrace:["UnderOver","23DE",1],underbrace:["UnderOver","23DF",1],overparen:["UnderOver","23DC"],underparen:["UnderOver","23DD"],overrightarrow:["UnderOver","2192"],underrightarrow:["UnderOver","2192"],overleftarrow:["UnderOver","2190"],underleftarrow:["UnderOver","2190"],overleftrightarrow:["UnderOver","2194"],underleftrightarrow:["UnderOver","2194"],overset:"Overset",underset:"Underset",stackrel:["Macro","\\mathrel{\\mathop{#2}\\limits^{#1}}",2],over:"Over",overwithdelims:"Over",atop:"Over",atopwithdelims:"Over",above:"Over",abovewithdelims:"Over",brace:["Over","{","}"],brack:["Over","[","]"],choose:["Over","(",")"],frac:"Frac",sqrt:"Sqrt",root:"Root",uproot:["MoveRoot","upRoot"],leftroot:["MoveRoot","leftRoot"],left:"LeftRight",right:"LeftRight",middle:"LeftRight",llap:"Lap",rlap:"Lap",raise:"RaiseLower",lower:"RaiseLower",moveleft:"MoveLeftRight",moveright:"MoveLeftRight",",":["Spacer",l.MATHSPACE.thinmathspace],":":["Spacer",l.MATHSPACE.mediummathspace],">":["Spacer",l.MATHSPACE.mediummathspace],";":["Spacer",l.MATHSPACE.thickmathspace],"!":["Spacer",l.MATHSPACE.negativethinmathspace],enspace:["Spacer",.5],quad:["Spacer",1],qquad:["Spacer",2],thinspace:["Spacer",l.MATHSPACE.thinmathspace],negthinspace:["Spacer",l.MATHSPACE.negativethinmathspace],hskip:"Hskip",hspace:"Hskip",kern:"Hskip",mskip:"Hskip",mspace:"Hskip",mkern:"Hskip",rule:"rule",Rule:["Rule"],Space:["Rule","blank"],big:["MakeBig",s.TEXCLASS.ORD,.85],Big:["MakeBig",s.TEXCLASS.ORD,1.15],bigg:["MakeBig",s.TEXCLASS.ORD,1.45],Bigg:["MakeBig",s.TEXCLASS.ORD,1.75],bigl:["MakeBig",s.TEXCLASS.OPEN,.85],Bigl:["MakeBig",s.TEXCLASS.OPEN,1.15],biggl:["MakeBig",s.TEXCLASS.OPEN,1.45],Biggl:["MakeBig",s.TEXCLASS.OPEN,1.75],bigr:["MakeBig",s.TEXCLASS.CLOSE,.85],Bigr:["MakeBig",s.TEXCLASS.CLOSE,1.15],biggr:["MakeBig",s.TEXCLASS.CLOSE,1.45],Biggr:["MakeBig",s.TEXCLASS.CLOSE,1.75],bigm:["MakeBig",s.TEXCLASS.REL,.85],Bigm:["MakeBig",s.TEXCLASS.REL,1.15],biggm:["MakeBig",s.TEXCLASS.REL,1.45],Biggm:["MakeBig",s.TEXCLASS.REL,1.75],mathord:["TeXAtom",s.TEXCLASS.ORD],mathop:["TeXAtom",s.TEXCLASS.OP],mathopen:["TeXAtom",s.TEXCLASS.OPEN],mathclose:["TeXAtom",s.TEXCLASS.CLOSE],mathbin:["TeXAtom",s.TEXCLASS.BIN],mathrel:["TeXAtom",s.TEXCLASS.REL],mathpunct:["TeXAtom",s.TEXCLASS.PUNCT],mathinner:["TeXAtom",s.TEXCLASS.INNER],vcenter:["TeXAtom",s.TEXCLASS.VCENTER],buildrel:"BuildRel",hbox:["HBox",0],text:"HBox",mbox:["HBox",0],fbox:"FBox",strut:"Strut",mathstrut:["Macro","\\vphantom{(}"],phantom:"Phantom",vphantom:["Phantom",1,0],hphantom:["Phantom",0,1],smash:"Smash",acute:["Accent","00B4"],grave:["Accent","0060"],ddot:["Accent","00A8"],tilde:["Accent","007E"],bar:["Accent","00AF"],breve:["Accent","02D8"],check:["Accent","02C7"],hat:["Accent","005E"],vec:["Accent","2192"],dot:["Accent","02D9"],widetilde:["Accent","007E",1],widehat:["Accent","005E",1],matrix:"Matrix",array:"Matrix",pmatrix:["Matrix","(",")"],cases:["Matrix","{","","left left",null,".1em",null,!0],eqalign:["Matrix",null,null,"right left",l.em(l.MATHSPACE.thickmathspace),".5em","D"],displaylines:["Matrix",null,null,"center",null,".5em","D"],cr:"Cr","\\":"CrLaTeX",newline:["CrLaTeX",!0],hline:["HLine","solid"],hdashline:["HLine","dashed"],eqalignno:["Matrix",null,null,"right left",l.em(l.MATHSPACE.thickmathspace),".5em","D",null,"right"],leqalignno:["Matrix",null,null,"right left",l.em(l.MATHSPACE.thickmathspace),".5em","D",null,"left"],hfill:"HFill",hfil:"HFill",hfilll:"HFill",bmod:["Macro",'\\mmlToken{mo}[lspace="thickmathspace" rspace="thickmathspace"]{mod}'],pmod:["Macro","\\pod{\\mmlToken{mi}{mod}\\kern 6mu #1}",1],mod:["Macro","\\mathchoice{\\kern18mu}{\\kern12mu}{\\kern12mu}{\\kern12mu}\\mmlToken{mi}{mod}\\,\\,#1",1],pod:["Macro","\\mathchoice{\\kern18mu}{\\kern8mu}{\\kern8mu}{\\kern8mu}(#1)",1],iff:["Macro","\\;\\Longleftrightarrow\\;"],skew:["Macro","{{#2{#3\\mkern#1mu}\\mkern-#1mu}{}}",3],pmb:["Macro","\\rlap{#1}\\kern1px{#1}",1],TeX:["Macro","T\\kern-.14em\\lower.5ex{E}\\kern-.115em X"],LaTeX:["Macro","L\\kern-.325em\\raise.21em{\\scriptstyle{A}}\\kern-.17em\\TeX"]," ":["Macro","\\text{ }"],not:"Not",dots:"Dots",space:"Tilde","\xa0":"Tilde",begin:"BeginEnd",end:"BeginEnd",label:"HandleLabel",ref:"HandleRef",nonumber:"HandleNoTag",mathchoice:"MathChoice",mmlToken:"MmlToken"},a.default),new n.EnvironmentMap("environment",i.default.environment,{array:["AlignedArray"],equation:["Equation",null,!0],"equation*":["Equation",null,!1],eqnarray:["EqnArray",null,!0,!0,"rcl","0 "+l.em(l.MATHSPACE.thickmathspace),".5em"]},a.default),new n.CharacterMap("not_remap",null,{"\u2190":"\u219a","\u2192":"\u219b","\u2194":"\u21ae","\u21d0":"\u21cd","\u21d2":"\u21cf","\u21d4":"\u21ce","\u2208":"\u2209","\u220b":"\u220c","\u2223":"\u2224","\u2225":"\u2226","\u223c":"\u2241","~":"\u2241","\u2243":"\u2244","\u2245":"\u2247","\u2248":"\u2249","\u224d":"\u226d","=":"\u2260","\u2261":"\u2262","<":"\u226e",">":"\u226f","\u2264":"\u2270","\u2265":"\u2271","\u2272":"\u2274","\u2273":"\u2275","\u2276":"\u2278","\u2277":"\u2279","\u227a":"\u2280","\u227b":"\u2281","\u2282":"\u2284","\u2283":"\u2285","\u2286":"\u2288","\u2287":"\u2289","\u22a2":"\u22ac","\u22a8":"\u22ad","\u22a9":"\u22ae","\u22ab":"\u22af","\u227c":"\u22e0","\u227d":"\u22e1","\u2291":"\u22e2","\u2292":"\u22e3","\u22b2":"\u22ea","\u22b3":"\u22eb","\u22b4":"\u22ec","\u22b5":"\u22ed","\u2203":"\u2204"})},724:function(t,e,r){var n=this&&this.__assign||function(){return(n=Object.assign||function(t){for(var e,r=1,n=arguments.length;r0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i};Object.defineProperty(e,"__esModule",{value:!0});var a=r(8389),i=r(8321),s=r(3466),l=r(810),c=r(7007),u=r(7702),p=r(8921),f=r(7251),d=r(6914),h=r(9029),m={},g={fontfamily:1,fontsize:1,fontweight:1,fontstyle:1,color:1,background:1,id:1,class:1,href:1,style:1};function y(t,e){var r=t.stack.env,n=r.inRoot;r.inRoot=!0;var o=new l.default(e,r,t.configuration),a=o.mml(),i=o.stack.global;if(i.leftRoot||i.upRoot){var s={};i.leftRoot&&(s.width=i.leftRoot),i.upRoot&&(s.voffset=i.upRoot,s.height=i.upRoot),a=t.create("node","mpadded",[a],s)}return r.inRoot=n,a}m.Open=function(t,e){t.Push(t.itemFactory.create("open"))},m.Close=function(t,e){t.Push(t.itemFactory.create("close"))},m.Tilde=function(t,e){t.Push(t.create("token","mtext",{},h.entities.nbsp))},m.Space=function(t,e){},m.Superscript=function(t,e){var r,n,a;t.GetNext().match(/\d/)&&(t.string=t.string.substr(0,t.i+1)+" "+t.string.substr(t.i+1));var l=t.stack.Top();l.isKind("prime")?(a=(r=o(l.Peek(2),2))[0],n=r[1],t.stack.Pop()):(a=t.stack.Prev())||(a=t.create("token","mi",{},""));var c=i.default.getProperty(a,"movesupsub"),u=i.default.isType(a,"msubsup")?a.sup:a.over;if(i.default.isType(a,"msubsup")&&!i.default.isType(a,"msup")&&i.default.getChildAt(a,a.sup)||i.default.isType(a,"munderover")&&!i.default.isType(a,"mover")&&i.default.getChildAt(a,a.over)&&!i.default.getProperty(a,"subsupOK"))throw new s.default("DoubleExponent","Double exponent: use braces to clarify");i.default.isType(a,"msubsup")&&!i.default.isType(a,"msup")||(c?((!i.default.isType(a,"munderover")||i.default.isType(a,"mover")||i.default.getChildAt(a,a.over))&&(a=t.create("node","munderover",[a],{movesupsub:!0})),u=a.over):u=(a=t.create("node","msubsup",[a])).sup),t.Push(t.itemFactory.create("subsup",a).setProperties({position:u,primes:n,movesupsub:c}))},m.Subscript=function(t,e){var r,n,a;t.GetNext().match(/\d/)&&(t.string=t.string.substr(0,t.i+1)+" "+t.string.substr(t.i+1));var l=t.stack.Top();l.isKind("prime")?(a=(r=o(l.Peek(2),2))[0],n=r[1],t.stack.Pop()):(a=t.stack.Prev())||(a=t.create("token","mi",{},""));var c=i.default.getProperty(a,"movesupsub"),u=i.default.isType(a,"msubsup")?a.sub:a.under;if(i.default.isType(a,"msubsup")&&!i.default.isType(a,"msup")&&i.default.getChildAt(a,a.sub)||i.default.isType(a,"munderover")&&!i.default.isType(a,"mover")&&i.default.getChildAt(a,a.under)&&!i.default.getProperty(a,"subsupOK"))throw new s.default("DoubleSubscripts","Double subscripts: use braces to clarify");i.default.isType(a,"msubsup")&&!i.default.isType(a,"msup")||(c?((!i.default.isType(a,"munderover")||i.default.isType(a,"mover")||i.default.getChildAt(a,a.under))&&(a=t.create("node","munderover",[a],{movesupsub:!0})),u=a.under):u=(a=t.create("node","msubsup",[a])).sub),t.Push(t.itemFactory.create("subsup",a).setProperties({position:u,primes:n,movesupsub:c}))},m.Prime=function(t,e){var r=t.stack.Prev();if(r||(r=t.create("node","mi")),i.default.isType(r,"msubsup")&&!i.default.isType(r,"msup")&&i.default.getChildAt(r,r.sup))throw new s.default("DoubleExponentPrime","Prime causes double exponent: use braces to clarify");var n="";t.i--;do{n+=h.entities.prime,t.i++,e=t.GetNext()}while("'"===e||e===h.entities.rsquo);n=["","\u2032","\u2033","\u2034","\u2057"][n.length]||n;var o=t.create("token","mo",{variantForm:!0},n);t.Push(t.itemFactory.create("prime",r,o))},m.Comment=function(t,e){for(;t.it.configuration.options.maxMacros)throw new s.default("MaxMacroSub2","MathJax maximum substitution count exceeded; is there a recursive latex environment?");t.parse("environment",[t,r])},m.Array=function(t,e,r,n,o,a,i,s,l){o||(o=t.GetArgument("\\begin{"+e.getName()+"}"));var c=("c"+o).replace(/[^clr|:]/g,"").replace(/[^|:]([|:])+/g,"$1");o=(o=o.replace(/[^clr]/g,"").split("").join(" ")).replace(/l/g,"left").replace(/r/g,"right").replace(/c/g,"center");var u=t.itemFactory.create("array");return u.arraydef={columnalign:o,columnspacing:a||"1em",rowspacing:i||"4pt"},c.match(/[|:]/)&&(c.charAt(0).match(/[|:]/)&&(u.frame.push("left"),u.dashed=":"===c.charAt(0)),c.charAt(c.length-1).match(/[|:]/)&&u.frame.push("right"),c=c.substr(1,c.length-2),u.arraydef.columnlines=c.split("").join(" ").replace(/[^|: ]/g,"none").replace(/\|/g,"solid").replace(/:/g,"dashed")),r&&u.setProperty("open",t.convertDelimiter(r)),n&&u.setProperty("close",t.convertDelimiter(n)),"D"===s?u.arraydef.displaystyle=!0:s&&(u.arraydef.displaystyle=!1),"S"===s&&(u.arraydef.scriptlevel=1),l&&(u.arraydef.useHeight=!1),t.Push(e),u},m.AlignedArray=function(t,e){var r=t.GetBrackets("\\begin{"+e.getName()+"}"),n=m.Array(t,e);return u.default.setArrayAlign(n,r)},m.Equation=function(t,e,r){return t.Push(e),u.default.checkEqnEnv(t),t.itemFactory.create("equation",r).setProperty("name",e.getName())},m.EqnArray=function(t,e,r,n,o,a){t.Push(e),n&&u.default.checkEqnEnv(t),o=(o=o.replace(/[^clr]/g,"").split("").join(" ")).replace(/l/g,"left").replace(/r/g,"right").replace(/c/g,"center");var i=t.itemFactory.create("eqnarray",e.getName(),r,n,t.stack.global);return i.arraydef={displaystyle:!0,columnalign:o,columnspacing:a||"1em",rowspacing:"3pt",side:t.options.tagSide,minlabelspacing:t.options.tagIndent},i},m.HandleNoTag=function(t,e){t.tags.notag()},m.HandleLabel=function(t,e){var r=t.GetArgument(e);if(""!==r&&!t.tags.refUpdate){if(t.tags.label)throw new s.default("MultipleCommand","Multiple %1",t.currentCS);if(t.tags.label=r,(t.tags.allLabels[r]||t.tags.labels[r])&&!t.options.ignoreDuplicateLabels)throw new s.default("MultipleLabel","Label '%1' multiply defined",r);t.tags.labels[r]=new f.Label}},m.HandleRef=function(t,e,r){var n=t.GetArgument(e),o=t.tags.allLabels[n]||t.tags.labels[n];o||(t.tags.refUpdate||(t.tags.redo=!0),o=new f.Label);var a=o.tag;r&&(a=t.tags.formatTag(a));var i=t.create("node","mrow",u.default.internalMath(t,a),{href:t.tags.formatUrl(o.id,t.options.baseURL),class:"MathJax_ref"});t.Push(i)},m.Macro=function(t,e,r,n,o){if(n){var a=[];if(null!=o){var i=t.GetBrackets(e);a.push(null==i?o:i)}for(var l=a.length;lt.configuration.options.maxMacros)throw new s.default("MaxMacroSub1","MathJax maximum macro substitution count exceeded; is there a recursive macro call?")},m.MathChoice=function(t,e){var r=t.ParseArg(e),n=t.ParseArg(e),o=t.ParseArg(e),a=t.ParseArg(e);t.Push(t.create("node","MathChoice",[r,n,o,a]))},e.default=m},3067:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.BboxConfiguration=e.BboxMethods=void 0;var n=r(6552),o=r(7628),a=r(3466);e.BboxMethods={},e.BboxMethods.BBox=function(t,e){for(var r,n,o,l=t.GetBrackets(e,""),c=t.ParseArg(e),u=l.split(/,/),p=0,f=u.length;p=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.BoldsymbolConfiguration=e.rewriteBoldTokens=e.createBoldToken=e.BoldsymbolMethods=void 0;var o=r(6552),a=r(8321),i=r(7007),s=r(7628),l=r(8644),c={};function u(t,e,r,n){var o=l.NodeFactory.createToken(t,e,r,n);return"mtext"!==e&&t.configuration.parser.stack.env.boldsymbol&&(a.default.setProperty(o,"fixBold",!0),t.configuration.addNode("fixBold",o)),o}function p(t){var e,r;try{for(var o=n(t.data.getList("fixBold")),s=o.next();!s.done;s=o.next()){var l=s.value;if(a.default.getProperty(l,"fixBold")){var u=a.default.getAttribute(l,"mathvariant");null==u?a.default.setAttribute(l,"mathvariant",i.TexConstant.Variant.BOLD):a.default.setAttribute(l,"mathvariant",c[u]||u),a.default.removeProperties(l,"fixBold")}}}catch(t){e={error:t}}finally{try{s&&!s.done&&(r=o.return)&&r.call(o)}finally{if(e)throw e.error}}}c[i.TexConstant.Variant.NORMAL]=i.TexConstant.Variant.BOLD,c[i.TexConstant.Variant.ITALIC]=i.TexConstant.Variant.BOLDITALIC,c[i.TexConstant.Variant.FRAKTUR]=i.TexConstant.Variant.BOLDFRAKTUR,c[i.TexConstant.Variant.SCRIPT]=i.TexConstant.Variant.BOLDSCRIPT,c[i.TexConstant.Variant.SANSSERIF]=i.TexConstant.Variant.BOLDSANSSERIF,c["-tex-calligraphic"]="-tex-bold-calligraphic",c["-tex-oldstyle"]="-tex-bold-oldstyle",c["-tex-mathit"]=i.TexConstant.Variant.BOLDITALIC,e.BoldsymbolMethods={},e.BoldsymbolMethods.Boldsymbol=function(t,e){var r=t.stack.env.boldsymbol;t.stack.env.boldsymbol=!0;var n=t.ParseArg(e);t.stack.env.boldsymbol=r,t.Push(n)},new s.CommandMap("boldsymbol",{boldsymbol:"Boldsymbol"},e.BoldsymbolMethods),e.createBoldToken=u,e.rewriteBoldTokens=p,e.BoldsymbolConfiguration=o.Configuration.create("boldsymbol",{handler:{macro:["boldsymbol"]},nodes:{token:u},postprocessors:[p]})},1677:function(t,e,r){var n;Object.defineProperty(e,"__esModule",{value:!0}),e.BraketConfiguration=void 0;var o=r(6552),a=r(9365);r(7076),e.BraketConfiguration=o.Configuration.create("braket",{handler:{character:["Braket-characters"],macro:["Braket-macros"]},items:(n={},n[a.BraketItem.prototype.kind]=a.BraketItem,n)})},9365:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.BraketItem=void 0;var a=r(7044),i=r(8921),s=r(7702),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"braket"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isOpen",{get:function(){return!0},enumerable:!1,configurable:!0}),e.prototype.checkItem=function(e){return e.isKind("close")?[[this.factory.create("mml",this.toMml())],!0]:e.isKind("mml")?(this.Push(e.toMml()),this.getProperty("single")?[[this.toMml()],!0]:a.BaseItem.fail):t.prototype.checkItem.call(this,e)},e.prototype.toMml=function(){var e=t.prototype.toMml.call(this),r=this.getProperty("open"),n=this.getProperty("close");if(this.getProperty("stretchy"))return s.default.fenced(this.factory.configuration,r,e,n);var o={fence:!0,stretchy:!1,symmetric:!0,texClass:i.TEXCLASS.OPEN},a=this.create("token","mo",o,r);o.texClass=i.TEXCLASS.CLOSE;var l=this.create("token","mo",o,n);return this.create("node","mrow",[a,e,l],{open:r,close:n,texClass:i.TEXCLASS.INNER})},e}(a.BaseItem);e.BraketItem=l},7076:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0});var n=r(7628),o=r(1990);new n.CommandMap("Braket-macros",{bra:["Macro","{\\langle {#1} \\vert}",1],ket:["Macro","{\\vert {#1} \\rangle}",1],braket:["Braket","\u27e8","\u27e9",!1,1/0],set:["Braket","{","}",!1,1],Bra:["Macro","{\\left\\langle {#1} \\right\\vert}",1],Ket:["Macro","{\\left\\vert {#1} \\right\\rangle}",1],Braket:["Braket","\u27e8","\u27e9",!0,1/0],Set:["Braket","{","}",!0,1],ketbra:["Macro","{\\vert {#1} \\rangle\\langle {#2} \\vert}",2],Ketbra:["Macro","{\\left\\vert {#1} \\right\\rangle\\left\\langle {#2} \\right\\vert}",2],"|":"Bar"},o.default),new n.MacroMap("Braket-characters",{"|":"Bar"},o.default)},1990:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0});var n=r(724),o=r(8921),a=r(3466),i={};i.Macro=n.default.Macro,i.Braket=function(t,e,r,n,o,i){var s=t.GetNext();if(""===s)throw new a.default("MissingArgFor","Missing argument for %1",t.currentCS);var l=!0;"{"===s&&(t.i++,l=!1),t.Push(t.itemFactory.create("braket").setProperties({barmax:i,barcount:0,open:r,close:n,stretchy:o,single:l}))},i.Bar=function(t,e){var r="|"===e?"|":"\u2225",n=t.stack.Top();if("braket"!==n.kind||n.getProperty("barcount")>=n.getProperty("barmax")){var a=t.create("token","mo",{texClass:o.TEXCLASS.ORD,stretchy:!1},r);t.Push(a)}else{if("|"===r&&"|"===t.GetNext()&&(t.i++,r="\u2225"),n.getProperty("stretchy")){var i=t.create("node","TeXAtom",[],{texClass:o.TEXCLASS.CLOSE});t.Push(i),n.setProperty("barcount",n.getProperty("barcount")+1),i=t.create("token","mo",{stretchy:!0,braketbar:!0},r),t.Push(i),i=t.create("node","TeXAtom",[],{texClass:o.TEXCLASS.OPEN}),t.Push(i)}else{var s=t.create("token","mo",{stretchy:!1,braketbar:!0},r);t.Push(s)}}},e.default=i},7404:function(t,e,r){var n;Object.defineProperty(e,"__esModule",{value:!0}),e.BussproofsConfiguration=void 0;var o=r(6552),a=r(2146),i=r(3118);r(1597),e.BussproofsConfiguration=o.Configuration.create("bussproofs",{handler:{macro:["Bussproofs-macros"],environment:["Bussproofs-environments"]},items:(n={},n[a.ProofTreeItem.prototype.kind]=a.ProofTreeItem,n),preprocessors:[[i.saveDocument,1]],postprocessors:[[i.clearDocument,3],[i.makeBsprAttributes,2],[i.balanceRules,1]]})},2146:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.ProofTreeItem=void 0;var a=r(3466),i=r(7044),s=r(9874),l=r(3118),c=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.leftLabel=null,e.rigthLabel=null,e.innerStack=new s.default(e.factory,{},!0),e}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"proofTree"},enumerable:!1,configurable:!0}),e.prototype.checkItem=function(t){if(t.isKind("end")&&"prooftree"===t.getName()){var e=this.toMml();return l.setProperty(e,"proof",!0),[[this.factory.create("mml",e),t],!0]}if(t.isKind("stop"))throw new a.default("EnvMissingEnd","Missing \\end{%1}",this.getName());return this.innerStack.Push(t),i.BaseItem.fail},e.prototype.toMml=function(){var e=t.prototype.toMml.call(this),r=this.innerStack.Top();if(r.isKind("start")&&!r.Size())return e;this.innerStack.Push(this.factory.create("stop"));var n=this.innerStack.Top().toMml();return this.create("node","mrow",[n,e],{})},e}(i.BaseItem);e.ProofTreeItem=c},1597:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0});var n=r(3583),o=r(4708),a=r(7628);new a.CommandMap("Bussproofs-macros",{AxiomC:"Axiom",UnaryInfC:["Inference",1],BinaryInfC:["Inference",2],TrinaryInfC:["Inference",3],QuaternaryInfC:["Inference",4],QuinaryInfC:["Inference",5],RightLabel:["Label","right"],LeftLabel:["Label","left"],AXC:"Axiom",UIC:["Inference",1],BIC:["Inference",2],TIC:["Inference",3],RL:["Label","right"],LL:["Label","left"],noLine:["SetLine","none",!1],singleLine:["SetLine","solid",!1],solidLine:["SetLine","solid",!1],dashedLine:["SetLine","dashed",!1],alwaysNoLine:["SetLine","none",!0],alwaysSingleLine:["SetLine","solid",!0],alwaysSolidLine:["SetLine","solid",!0],alwaysDashedLine:["SetLine","dashed",!0],rootAtTop:["RootAtTop",!0],alwaysRootAtTop:["RootAtTop",!0],rootAtBottom:["RootAtTop",!1],alwaysRootAtBottom:["RootAtTop",!1],fCenter:"FCenter",Axiom:"AxiomF",UnaryInf:["InferenceF",1],BinaryInf:["InferenceF",2],TrinaryInf:["InferenceF",3],QuaternaryInf:["InferenceF",4],QuinaryInf:["InferenceF",5]},n.default),new a.EnvironmentMap("Bussproofs-environments",o.default.environment,{prooftree:["Prooftree",null,!1]},n.default)},3583:function(t,e,r){var n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},o=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r0);var c=t.create("node","mtr",s,{}),f=t.create("node","mtable",[c],{framespacing:"0 0"}),d=u(t,t.GetArgument(e)),h=n.getProperty("currentLine");h!==n.getProperty("line")&&n.setProperty("currentLine",n.getProperty("line"));var m=p(t,f,[d],n.getProperty("left"),n.getProperty("right"),h,o);n.setProperty("left",null),n.setProperty("right",null),l.setProperty(m,"inference",i),t.configuration.addNode("inference",m),n.Push(m)},c.Label=function(t,e,r){var n=t.stack.Top();if("proofTree"!==n.kind)throw new a.default("IllegalProofCommand","Proof commands only allowed in prooftree environment.");var o=s.default.internalMath(t,t.GetArgument(e),0),i=o.length>1?t.create("node","mrow",o,{}):o[0];n.setProperty(r,i)},c.SetLine=function(t,e,r,n){var o=t.stack.Top();if("proofTree"!==o.kind)throw new a.default("IllegalProofCommand","Proof commands only allowed in prooftree environment.");o.setProperty("currentLine",r),n&&o.setProperty("line",r)},c.RootAtTop=function(t,e,r){var n=t.stack.Top();if("proofTree"!==n.kind)throw new a.default("IllegalProofCommand","Proof commands only allowed in prooftree environment.");n.setProperty("rootAtTop",r)},c.AxiomF=function(t,e){var r=t.stack.Top();if("proofTree"!==r.kind)throw new a.default("IllegalProofCommand","Proof commands only allowed in prooftree environment.");var n=f(t,e);l.setProperty(n,"axiom",!0),r.Push(n)},c.FCenter=function(t,e){},c.InferenceF=function(t,e,r){var n=t.stack.Top();if("proofTree"!==n.kind)throw new a.default("IllegalProofCommand","Proof commands only allowed in prooftree environment.");if(n.Size()0);var c=t.create("node","mtr",s,{}),u=t.create("node","mtable",[c],{framespacing:"0 0"}),d=f(t,e),h=n.getProperty("currentLine");h!==n.getProperty("line")&&n.setProperty("currentLine",n.getProperty("line"));var m=p(t,u,[d],n.getProperty("left"),n.getProperty("right"),h,o);n.setProperty("left",null),n.setProperty("right",null),l.setProperty(m,"inference",i),t.configuration.addNode("inference",m),n.Push(m)},e.default=c},3118:function(t,e,r){var n,o=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},a=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.clearDocument=e.saveDocument=e.makeBsprAttributes=e.removeProperty=e.getProperty=e.setProperty=e.balanceRules=void 0;var i=r(8321),s=r(7702),l=null,c=null,u=function(t){return c.root=t,l.outputJax.getBBox(c,l).w},p=function(t){for(var e=0;t&&!i.default.isType(t,"mtable");){if(i.default.isType(t,"text"))return null;i.default.isType(t,"mrow")?(t=t.childNodes[0],e=0):(t=t.parent.childNodes[e],e++)}return t},f=function(t,e){return t.childNodes["up"===e?1:0].childNodes[0].childNodes[0].childNodes[0].childNodes[0]},d=function(t,e){return t.childNodes[e].childNodes[0].childNodes[0]},h=function(t){return d(t,0)},m=function(t){return d(t,t.childNodes.length-1)},g=function(t,e){return t.childNodes["up"===e?0:1].childNodes[0].childNodes[0].childNodes[0]},y=function(t){for(;t&&!i.default.isType(t,"mtd");)t=t.parent;return t},v=function(t){return t.parent.childNodes[t.parent.childNodes.indexOf(t)+1]},b=function(t){for(;t&&null==e.getProperty(t,"inference");)t=t.parent;return t},x=function(t,e,r){void 0===r&&(r=!1);var n=0;if(t===e)return n;if(t!==e.parent){var o=t.childNodes,a=r?o.length-1:0;i.default.isType(o[a],"mspace")&&(n+=u(o[a])),t=e.parent}if(t===e)return n;var s=t.childNodes,l=r?s.length-1:0;return s[l]!==e&&(n+=u(s[l])),n},_=function(t,r){void 0===r&&(r=!1);var n=p(t),o=g(n,e.getProperty(n,"inferenceRule"));return x(t,n,r)+(u(n)-u(o))/2},A=function(t,r,n,o){if(void 0===o&&(o=!1),e.getProperty(r,"inferenceRule")||e.getProperty(r,"labelledRule")){var a=t.nodeFactory.create("node","mrow");r.parent.replaceChild(a,r),a.setChildren([r]),M(r,a),r=a}var l=o?r.childNodes.length-1:0,c=r.childNodes[l];i.default.isType(c,"mspace")?i.default.setAttribute(c,"width",s.default.Em(s.default.dimen2em(i.default.getAttribute(c,"width"))+n)):(c=t.nodeFactory.create("node","mspace",[],{width:s.default.Em(n)}),o?r.appendChild(c):(c.parent=r,r.childNodes.unshift(c)))},M=function(t,r){["inference","proof","maxAdjust","labelledRule"].forEach((function(n){var o=e.getProperty(t,n);null!=o&&(e.setProperty(r,n,o),e.removeProperty(t,n))}))},C=function(t,r,n,o,a){var i=t.nodeFactory.create("node","mspace",[],{width:s.default.Em(a)});if("left"===o){var l=r.childNodes[n].childNodes[0];i.parent=l,l.childNodes.unshift(i)}else r.childNodes[n].appendChild(i);e.setProperty(r.parent,"sequentAdjust_"+o,a)},w=function(t,r){for(var n=r.pop();r.length;){var a=r.pop(),i=o(S(n,a),2),s=i[0],l=i[1];e.getProperty(n.parent,"axiom")&&(C(t,s<0?n:a,0,"left",Math.abs(s)),C(t,l<0?n:a,2,"right",Math.abs(l))),n=a}},S=function(t,e){var r=u(t.childNodes[2]),n=u(e.childNodes[2]);return[u(t.childNodes[0])-u(e.childNodes[0]),r-n]};e.balanceRules=function(t){var r,n;c=new t.document.options.MathItem("",null,t.math.display);var o=t.data;!function(t){var r=t.nodeLists.sequent;if(r)for(var n=r.length-1,o=void 0;o=r[n];n--)if(e.getProperty(o,"sequentProcessed"))e.removeProperty(o,"sequentProcessed");else{var a=[],i=b(o);if(1===e.getProperty(i,"inference")){for(a.push(o);1===e.getProperty(i,"inference");){i=p(i);var s=h(f(i,e.getProperty(i,"inferenceRule"))),l=e.getProperty(s,"inferenceRule")?g(s,e.getProperty(s,"inferenceRule")):s;e.getProperty(l,"sequent")&&(o=l.childNodes[0],a.push(o),e.setProperty(o,"sequentProcessed",!0)),i=s}w(t,a)}}}(o);var i=o.nodeLists.inference||[];try{for(var s=a(i),l=s.next();!l.done;l=s.next()){var u=l.value,d=e.getProperty(u,"proof"),M=p(u),C=f(M,e.getProperty(M,"inferenceRule")),S=h(C);if(e.getProperty(S,"inference")){var P=_(S);if(P){A(o,S,-P);var T=x(u,M,!1);A(o,u,P-T)}}var O=m(C);if(null!=e.getProperty(O,"inference")){var k=_(O,!0);A(o,O,-k,!0);var E=x(u,M,!0),I=e.getProperty(u,"maxAdjust");null!=I&&(k=Math.max(k,I));var F=void 0;if(!d&&(F=y(u))){var N=v(F);if(N){var L=o.nodeFactory.create("node","mspace",[],{width:k-E+"em"});N.appendChild(L),u.removeProperty("maxAdjust")}else{var q=b(F);q&&(k=e.getProperty(q,"maxAdjust")?Math.max(e.getProperty(q,"maxAdjust"),k):k,e.setProperty(q,"maxAdjust",k))}}else A(o,e.getProperty(u,"proof")?u:u.parent,k-E,!0)}}}catch(t){r={error:t}}finally{try{l&&!l.done&&(n=s.return)&&n.call(s)}finally{if(r)throw r.error}}};var P="bspr_",T=((n={}).bspr_maxAdjust=!0,n);e.setProperty=function(t,e,r){i.default.setProperty(t,P+e,r)};e.getProperty=function(t,e){return i.default.getProperty(t,P+e)};e.removeProperty=function(t,e){t.removeProperty(P+e)};e.makeBsprAttributes=function(t){t.data.root.walkTree((function(t,e){var r=[];t.getPropertyNames().forEach((function(e){!T[e]&&e.match(RegExp("^bspr_"))&&r.push(e+":"+t.getProperty(e))})),r.length&&i.default.setAttribute(t,"semantics",r.join(";"))}))};e.saveDocument=function(t){if(!("getBBox"in(l=t.document).outputJax))throw Error("The bussproofs extension requires an output jax with a getBBox() method")};e.clearDocument=function(t){l=null}},9489:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.CancelConfiguration=e.CancelMethods=void 0;var n=r(6552),o=r(7007),a=r(7628),i=r(7702),s=r(6755);e.CancelMethods={},e.CancelMethods.Cancel=function(t,e,r){var n=t.GetBrackets(e,""),o=t.ParseArg(e),a=i.default.keyvalOptions(n,s.ENCLOSE_OPTIONS);a.notation=r,t.Push(t.create("node","menclose",[o],a))},e.CancelMethods.CancelTo=function(t,e){var r=t.GetBrackets(e,""),n=t.ParseArg(e),a=t.ParseArg(e),l=i.default.keyvalOptions(r,s.ENCLOSE_OPTIONS);l.notation=[o.TexConstant.Notation.UPDIAGONALSTRIKE,o.TexConstant.Notation.UPDIAGONALARROW,o.TexConstant.Notation.NORTHEASTARROW].join(" "),n=t.create("node","mpadded",[n],{depth:"-.1em",height:"+.1em",voffset:".1em"}),t.Push(t.create("node","msup",[t.create("node","menclose",[a],l),n]))},new a.CommandMap("cancel",{cancel:["Cancel",o.TexConstant.Notation.UPDIAGONALSTRIKE],bcancel:["Cancel",o.TexConstant.Notation.DOWNDIAGONALSTRIKE],xcancel:["Cancel",o.TexConstant.Notation.UPDIAGONALSTRIKE+" "+o.TexConstant.Notation.DOWNDIAGONALSTRIKE],cancelto:"CancelTo"},e.CancelMethods),e.CancelConfiguration=n.Configuration.create("cancel",{handler:{macro:["cancel"]}})},4151:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.ColorConfiguration=void 0;var n=r(7628),o=r(6552),a=r(9574),i=r(3997);new n.CommandMap("color",{color:"Color",textcolor:"TextColor",definecolor:"DefineColor",colorbox:"ColorBox",fcolorbox:"FColorBox"},a.ColorMethods);e.ColorConfiguration=o.Configuration.create("color",{handler:{macro:["color"]},options:{color:{padding:"5px",borderWidth:"2px"}},config:function(t,e){e.parseOptions.packageData.set("color",{model:new i.ColorModel})}})},6961:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.COLORS=void 0,e.COLORS=new Map([["Apricot","#FBB982"],["Aquamarine","#00B5BE"],["Bittersweet","#C04F17"],["Black","#221E1F"],["Blue","#2D2F92"],["BlueGreen","#00B3B8"],["BlueViolet","#473992"],["BrickRed","#B6321C"],["Brown","#792500"],["BurntOrange","#F7921D"],["CadetBlue","#74729A"],["CarnationPink","#F282B4"],["Cerulean","#00A2E3"],["CornflowerBlue","#41B0E4"],["Cyan","#00AEEF"],["Dandelion","#FDBC42"],["DarkOrchid","#A4538A"],["Emerald","#00A99D"],["ForestGreen","#009B55"],["Fuchsia","#8C368C"],["Goldenrod","#FFDF42"],["Gray","#949698"],["Green","#00A64F"],["GreenYellow","#DFE674"],["JungleGreen","#00A99A"],["Lavender","#F49EC4"],["LimeGreen","#8DC73E"],["Magenta","#EC008C"],["Mahogany","#A9341F"],["Maroon","#AF3235"],["Melon","#F89E7B"],["MidnightBlue","#006795"],["Mulberry","#A93C93"],["NavyBlue","#006EB8"],["OliveGreen","#3C8031"],["Orange","#F58137"],["OrangeRed","#ED135A"],["Orchid","#AF72B0"],["Peach","#F7965A"],["Periwinkle","#7977B8"],["PineGreen","#008B72"],["Plum","#92268F"],["ProcessBlue","#00B0F0"],["Purple","#99479B"],["RawSienna","#974006"],["Red","#ED1B23"],["RedOrange","#F26035"],["RedViolet","#A1246B"],["Rhodamine","#EF559F"],["RoyalBlue","#0071BC"],["RoyalPurple","#613F99"],["RubineRed","#ED017D"],["Salmon","#F69289"],["SeaGreen","#3FBC9D"],["Sepia","#671800"],["SkyBlue","#46C5DD"],["SpringGreen","#C6DC67"],["Tan","#DA9D76"],["TealBlue","#00AEB3"],["Thistle","#D883B7"],["Turquoise","#00B4CE"],["Violet","#58429B"],["VioletRed","#EF58A0"],["White","#FFFFFF"],["WildStrawberry","#EE2967"],["Yellow","#FFF200"],["YellowGreen","#98CC70"],["YellowOrange","#FAA21A"]])},9574:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.ColorMethods=void 0;var n=r(8321),o=r(7702);function a(t){var e="+"+t,r=t.replace(/^.*?([a-z]*)$/,"$1");return{width:"+"+2*parseFloat(e)+r,height:e,depth:e,lspace:t}}e.ColorMethods={},e.ColorMethods.Color=function(t,e){var r=t.GetBrackets(e,""),n=t.GetArgument(e),o=t.configuration.packageData.get("color").model.getColor(r,n),a=t.itemFactory.create("style").setProperties({styles:{mathcolor:o}});t.stack.env.color=o,t.Push(a)},e.ColorMethods.TextColor=function(t,e){var r=t.GetBrackets(e,""),n=t.GetArgument(e),o=t.configuration.packageData.get("color").model.getColor(r,n),a=t.stack.env.color;t.stack.env.color=o;var i=t.ParseArg(e);a?t.stack.env.color=a:delete t.stack.env.color;var s=t.create("node","mstyle",[i],{mathcolor:o});t.Push(s)},e.ColorMethods.DefineColor=function(t,e){var r=t.GetArgument(e),n=t.GetArgument(e),o=t.GetArgument(e);t.configuration.packageData.get("color").model.defineColor(n,r,o)},e.ColorMethods.ColorBox=function(t,e){var r=t.GetArgument(e),i=o.default.internalMath(t,t.GetArgument(e)),s=t.configuration.packageData.get("color").model,l=t.create("node","mpadded",i,{mathbackground:s.getColor("named",r)});n.default.setProperties(l,a(t.options.color.padding)),t.Push(l)},e.ColorMethods.FColorBox=function(t,e){var r=t.GetArgument(e),i=t.GetArgument(e),s=o.default.internalMath(t,t.GetArgument(e)),l=t.options.color,c=t.configuration.packageData.get("color").model,u=t.create("node","mpadded",s,{mathbackground:c.getColor("named",i),style:"border: "+l.borderWidth+" solid "+c.getColor("named",r)});n.default.setProperties(u,a(l.padding)),t.Push(u)}},3997:function(t,e,r){var n=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.ColorModel=void 0;var o=r(3466),a=r(6961),i=new Map,s=function(){function t(){this.userColors=new Map}return t.prototype.normalizeColor=function(t,e){if(!t||"named"===t)return e;if(i.has(t))return i.get(t)(e);throw new o.default("UndefinedColorModel","Color model '%1' not defined",t)},t.prototype.getColor=function(t,e){return t&&"named"!==t?this.normalizeColor(t,e):this.getColorByName(e)},t.prototype.getColorByName=function(t){return this.userColors.has(t)?this.userColors.get(t):a.COLORS.has(t)?a.COLORS.get(t):t},t.prototype.defineColor=function(t,e,r){var n=this.normalizeColor(t,r);this.userColors.set(e,n)},t}();e.ColorModel=s,i.set("rgb",(function(t){var e,r,a=t.trim().split(/\s*,\s*/),i="#";if(3!==a.length)throw new o.default("ModelArg1","Color values for the %1 model require 3 numbers","rgb");try{for(var s=n(a),l=s.next();!l.done;l=s.next()){var c=l.value;if(!c.match(/^(\d+(\.\d*)?|\.\d+)$/))throw new o.default("InvalidDecimalNumber","Invalid decimal number");var u=parseFloat(c);if(u<0||u>1)throw new o.default("ModelArg2","Color values for the %1 model must be between %2 and %3","rgb","0","1");var p=Math.floor(255*u).toString(16);p.length<2&&(p="0"+p),i+=p}}catch(t){e={error:t}}finally{try{l&&!l.done&&(r=s.return)&&r.call(s)}finally{if(e)throw e.error}}return i})),i.set("RGB",(function(t){var e,r,a=t.trim().split(/\s*,\s*/),i="#";if(3!==a.length)throw new o.default("ModelArg1","Color values for the %1 model require 3 numbers","RGB");try{for(var s=n(a),l=s.next();!l.done;l=s.next()){var c=l.value;if(!c.match(/^\d+$/))throw new o.default("InvalidNumber","Invalid number");var u=parseInt(c);if(u>255)throw new o.default("ModelArg2","Color values for the %1 model must be between %2 and %3","RGB","0","255");var p=u.toString(16);p.length<2&&(p="0"+p),i+=p}}catch(t){e={error:t}}finally{try{l&&!l.done&&(r=s.return)&&r.call(s)}finally{if(e)throw e.error}}return i})),i.set("gray",(function(t){if(!t.match(/^\s*(\d+(\.\d*)?|\.\d+)\s*$/))throw new o.default("InvalidDecimalNumber","Invalid decimal number");var e=parseFloat(t);if(e<0||e>1)throw new o.default("ModelArg2","Color values for the %1 model must be between %2 and %3","gray","0","1");var r=Math.floor(255*e).toString(16);return r.length<2&&(r="0"+r),"#"+r+r+r}))},2298:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.ColorConfiguration=e.ColorV2Methods=void 0;var n=r(7628),o=r(6552);e.ColorV2Methods={Color:function(t,e){var r=t.GetArgument(e),n=t.stack.env.color;t.stack.env.color=r;var o=t.ParseArg(e);n?t.stack.env.color=n:delete t.stack.env.color;var a=t.create("node","mstyle",[o],{mathcolor:r});t.Push(a)}},new n.CommandMap("colorv2",{color:"Color"},e.ColorV2Methods),e.ColorConfiguration=o.Configuration.create("colorv2",{handler:{macro:["colorv2"]}})},3274:function(t,e,r){var n,o=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],n=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.ConfigMacrosConfiguration=void 0;var a=r(6552),i=r(9077),s=r(7628),l=r(4708),c=r(4237),u=r(8562),p=r(6706),f="configmacros-map",d="configmacros-env-map";e.ConfigMacrosConfiguration=a.Configuration.create("configmacros",{init:function(t){new s.CommandMap(f,{},{}),new s.EnvironmentMap(d,l.default.environment,{},{}),t.append(a.Configuration.local({handler:{macro:[f],environment:[d]},priority:3}))},config:function(t,e){!function(t){var e,r,n=t.parseOptions.handlers.retrieve(f),a=t.parseOptions.options.macros;try{for(var i=o(Object.keys(a)),s=i.next();!s.done;s=i.next()){var l=s.value,p="string"==typeof a[l]?[a[l]]:a[l],d=Array.isArray(p[2])?new c.Macro(l,u.default.MacroWithTemplate,p.slice(0,2).concat(p[2])):new c.Macro(l,u.default.Macro,p);n.add(l,d)}}catch(t){e={error:t}}finally{try{s&&!s.done&&(r=i.return)&&r.call(i)}finally{if(e)throw e.error}}}(e),function(t){var e,r,n=t.parseOptions.handlers.retrieve(d),a=t.parseOptions.options.environments;try{for(var i=o(Object.keys(a)),s=i.next();!s.done;s=i.next()){var l=s.value;n.add(l,new c.Macro(l,u.default.BeginEnv,[!0].concat(a[l])))}}catch(t){e={error:t}}finally{try{s&&!s.done&&(r=i.return)&&r.call(i)}finally{if(e)throw e.error}}}(e)},items:(n={},n[p.BeginEnvItem.prototype.kind]=p.BeginEnvItem,n),options:{macros:i.expandable({}),environments:i.expandable({})}})},6755:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.EncloseConfiguration=e.EncloseMethods=e.ENCLOSE_OPTIONS=void 0;var n=r(6552),o=r(7628),a=r(7702);e.ENCLOSE_OPTIONS={"data-arrowhead":1,color:1,mathcolor:1,background:1,mathbackground:1,"data-padding":1,"data-thickness":1},e.EncloseMethods={},e.EncloseMethods.Enclose=function(t,r){var n=t.GetArgument(r).replace(/,/g," "),o=t.GetBrackets(r,""),i=t.ParseArg(r),s=a.default.keyvalOptions(o,e.ENCLOSE_OPTIONS);s.notation=n,t.Push(t.create("node","menclose",[i],s))},new o.CommandMap("enclose",{enclose:"Enclose"},e.EncloseMethods),e.EncloseConfiguration=n.Configuration.create("enclose",{handler:{macro:["enclose"]}})},5246:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.ExtpfeilConfiguration=e.ExtpfeilMethods=void 0;var n=r(6552),o=r(7628),a=r(2684),i=r(5282),s=r(2200),l=r(3466);e.ExtpfeilMethods={},e.ExtpfeilMethods.xArrow=a.AmsMethods.xArrow,e.ExtpfeilMethods.NewExtArrow=function(t,r){var n=t.GetArgument(r),o=t.GetArgument(r),a=t.GetArgument(r);if(!n.match(/^\\([a-z]+|.)$/i))throw new l.default("NewextarrowArg1","First argument to %1 must be a control sequence name",r);if(!o.match(/^(\d+),(\d+)$/))throw new l.default("NewextarrowArg2","Second argument to %1 must be two integers separated by a comma",r);if(!a.match(/^(\d+|0x[0-9A-F]+)$/i))throw new l.default("NewextarrowArg3","Third argument to %1 must be a unicode character number",r);n=n.substr(1);var s=o.split(",");i.default.addMacro(t,n,e.ExtpfeilMethods.xArrow,[parseInt(a),parseInt(s[0]),parseInt(s[1])])},new o.CommandMap("extpfeil",{xtwoheadrightarrow:["xArrow",8608,12,16],xtwoheadleftarrow:["xArrow",8606,17,13],xmapsto:["xArrow",8614,6,7],xlongequal:["xArrow",61,7,7],xtofrom:["xArrow",8644,12,12],Newextarrow:"NewExtArrow"},e.ExtpfeilMethods);e.ExtpfeilConfiguration=n.Configuration.create("extpfeil",{handler:{macro:["extpfeil"]},init:function(t){s.NewcommandConfiguration.init(t)}})},153:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.HtmlConfiguration=void 0;var n=r(6552),o=r(7628),a=r(2565);new o.CommandMap("html_macros",{href:"Href",class:"Class",style:"Style",cssId:"Id"},a.default),e.HtmlConfiguration=n.Configuration.create("html",{handler:{macro:["html_macros"]}})},2565:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0});var n=r(8321),o={Href:function(t,e){var r=t.GetArgument(e),o=a(t,e);n.default.setAttribute(o,"href",r),t.Push(o)},Class:function(t,e){var r=t.GetArgument(e),o=a(t,e),i=n.default.getAttribute(o,"class");i&&(r=i+" "+r),n.default.setAttribute(o,"class",r),t.Push(o)},Style:function(t,e){var r=t.GetArgument(e),o=a(t,e),i=n.default.getAttribute(o,"style");i&&(";"!==r.charAt(r.length-1)&&(r+=";"),r=i+" "+r),n.default.setAttribute(o,"style",r),t.Push(o)},Id:function(t,e){var r=t.GetArgument(e),o=a(t,e);n.default.setAttribute(o,"id",r),t.Push(o)}},a=function(t,e){var r=t.ParseArg(e);if(!n.default.isInferred(r))return r;var o=n.default.getChildren(r);if(1===o.length)return o[0];var a=t.create("node","mrow");return n.default.copyChildren(r,a),n.default.copyAttributes(r,a),a};e.default=o},1323:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.MhchemConfiguration=void 0;var n=r(6552),o=r(7628),a=r(3466),i=r(724),s=r(2684),l=r(7552),c={};c.Macro=i.default.Macro,c.xArrow=s.AmsMethods.xArrow,c.Machine=function(t,e,r){try{var n=t.GetArgument(e),o=l.mhchemParser.toTex(n,r);t.string=o+t.string.substr(t.i),t.i=0}catch(t){throw new a.default(t[0],t[1],t.slice(2))}},new o.CommandMap("mhchem",{ce:["Machine","ce"],pu:["Machine","pu"],longrightleftharpoons:["Macro","\\stackrel{\\textstyle{-}\\!\\!{\\rightharpoonup}}{\\smash{{\\leftharpoondown}\\!\\!{-}}}"],longRightleftharpoons:["Macro","\\stackrel{\\textstyle{-}\\!\\!{\\rightharpoonup}}{\\smash{\\leftharpoondown}}"],longLeftrightharpoons:["Macro","\\stackrel{\\textstyle\\vphantom{{-}}{\\rightharpoonup}}{\\smash{{\\leftharpoondown}\\!\\!{-}}}"],longleftrightarrows:["Macro","\\stackrel{\\longrightarrow}{\\smash{\\longleftarrow}\\Rule{0px}{.25em}{0px}}"],tripledash:["Macro","\\vphantom{-}\\raise2mu{\\kern2mu\\tiny\\text{-}\\kern1mu\\text{-}\\kern1mu\\text{-}\\kern2mu}"],xrightarrow:["xArrow",8594,5,6],xleftarrow:["xArrow",8592,7,3],xleftrightarrow:["xArrow",8596,6,6],xrightleftharpoons:["xArrow",8652,5,7],xRightleftharpoons:["xArrow",8652,5,7],xLeftrightharpoons:["xArrow",8652,5,7]},c),e.MhchemConfiguration=n.Configuration.create("mhchem",{handler:{macro:["mhchem"]}})},7552:function(t,e){ +/*! + ************************************************************************* + * + * mhchemParser.ts + * 4.0.0 + * + * Parser for the \ce command and \pu command for MathJax and Co. + * + * mhchem's \ce is a tool for writing beautiful chemical equations easily. + * mhchem's \pu is a tool for writing physical units easily. + * + * ---------------------------------------------------------------------- + * + * Copyright (c) 2015-2021 Martin Hensel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ---------------------------------------------------------------------- + * + * https://github.com/mhchem/mhchemParser + * + */ +Object.defineProperty(e,"__esModule",{value:!0}),e.mhchemParser=void 0;var r=function(){function t(){}return t.toTex=function(t,e){return a.go(o.go(t,e),"tex"!==e)},t}();function n(t){var e,r,n={};for(e in t)for(r in t[e]){var o=r.split("|");t[e][r].stateArray=o;for(var a=0;a0))return s;if(f.revisit||(t=p.remainder),!f.toContinue)break t}}if(i<=0)throw["MhchemBugU","mhchem bug U. Please report."]}},concatArray:function(t,e){if(e)if(Array.isArray(e))for(var r=0;r":/^[=<>]/,"#":/^[#\u2261]/,"+":/^\+/,"-$":/^-(?=[\s_},;\]/]|$|\([a-z]+\))/,"-9":/^-(?=[0-9])/,"- orbital overlap":/^-(?=(?:[spd]|sp)(?:$|[\s,;\)\]\}]))/,"-":/^-/,"pm-operator":/^(?:\\pm|\$\\pm\$|\+-|\+\/-)/,operator:/^(?:\+|(?:[\-=<>]|<<|>>|\\approx|\$\\approx\$)(?=\s|$|-?[0-9]))/,arrowUpDown:/^(?:v|\(v\)|\^|\(\^\))(?=$|[\s,;\)\]\}])/,"\\bond{(...)}":function(t){return o.patterns.findObserveGroups(t,"\\bond{","","","}")},"->":/^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u2192\u27F6\u21CC])/,CMT:/^[CMT](?=\[)/,"[(...)]":function(t){return o.patterns.findObserveGroups(t,"[","","","]")},"1st-level escape":/^(&|\\\\|\\hline)\s*/,"\\,":/^(?:\\[,\ ;:])/,"\\x{}{}":function(t){return o.patterns.findObserveGroups(t,"",/^\\[a-zA-Z]+\{/,"}","","","{","}","",!0)},"\\x{}":function(t){return o.patterns.findObserveGroups(t,"",/^\\[a-zA-Z]+\{/,"}","")},"\\ca":/^\\ca(?:\s+|(?![a-zA-Z]))/,"\\x":/^(?:\\[a-zA-Z]+\s*|\\[_&{}%])/,orbital:/^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])/,others:/^[\/~|]/,"\\frac{(...)}":function(t){return o.patterns.findObserveGroups(t,"\\frac{","","","}","{","","","}")},"\\overset{(...)}":function(t){return o.patterns.findObserveGroups(t,"\\overset{","","","}","{","","","}")},"\\underset{(...)}":function(t){return o.patterns.findObserveGroups(t,"\\underset{","","","}","{","","","}")},"\\underbrace{(...)}":function(t){return o.patterns.findObserveGroups(t,"\\underbrace{","","","}_","{","","","}")},"\\color{(...)}":function(t){return o.patterns.findObserveGroups(t,"\\color{","","","}")},"\\color{(...)}{(...)}":function(t){return o.patterns.findObserveGroups(t,"\\color{","","","}","{","","","}")||o.patterns.findObserveGroups(t,"\\color","\\","",/^(?=\{)/,"{","","","}")},"\\ce{(...)}":function(t){return o.patterns.findObserveGroups(t,"\\ce{","","","}")},"\\pu{(...)}":function(t){return o.patterns.findObserveGroups(t,"\\pu{","","","}")},oxidation$:/^(?:[+-][IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,"d-oxidation$":/^(?:[+-]?\s?[IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,"roman numeral":/^[IVX]+/,"1/2$":/^[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+(?:\$[a-z]\$|[a-z])?$/,amount:function(t){var e;if(e=t.match(/^(?:(?:(?:\([+\-]?[0-9]+\/[0-9]+\)|[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+|[+\-]?[0-9]+[.,][0-9]+|[+\-]?\.[0-9]+|[+\-]?[0-9]+)(?:[a-z](?=\s*[A-Z]))?)|[+\-]?[a-z](?=\s*[A-Z])|\+(?!\s))/))return{match_:e[0],remainder:t.substr(e[0].length)};var r=o.patterns.findObserveGroups(t,"","$","$","");return r&&(e=r.match_.match(/^\$(?:\(?[+\-]?(?:[0-9]*[a-z]?[+\-])?[0-9]*[a-z](?:[+\-][0-9]*[a-z]?)?\)?|\+|-)\$$/))?{match_:e[0],remainder:t.substr(e[0].length)}:null},amount2:function(t){return this.amount(t)},"(KV letters),":/^(?:[A-Z][a-z]{0,2}|i)(?=,)/,formula$:function(t){if(t.match(/^\([a-z]+\)$/))return null;var e=t.match(/^(?:[a-z]|(?:[0-9\ \+\-\,\.\(\)]+[a-z])+[0-9\ \+\-\,\.\(\)]*|(?:[a-z][0-9\ \+\-\,\.\(\)]+)+[a-z]?)$/);return e?{match_:e[0],remainder:t.substr(e[0].length)}:null},uprightEntities:/^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])/,"/":/^\s*(\/)\s*/,"//":/^\s*(\/\/)\s*/,"*":/^\s*[*.]\s*/},findObserveGroups:function(t,e,r,n,o,a,i,s,l,c){var u=function(t,e){if("string"==typeof e)return 0!==t.indexOf(e)?null:e;var r=t.match(e);return r?r[0]:null},p=u(t,e);if(null===p)return null;if(t=t.substr(p.length),null===(p=u(t,r)))return null;var f=function(t,e,r){for(var n=0;e2?{match_:n.slice(1),remainder:e.substr(n[0].length)}:{match_:n[1]||n[0],remainder:e.substr(n[0].length)}:null}},actions:{"a=":function(t,e){t.a=(t.a||"")+e},"b=":function(t,e){t.b=(t.b||"")+e},"p=":function(t,e){t.p=(t.p||"")+e},"o=":function(t,e){t.o=(t.o||"")+e},"q=":function(t,e){t.q=(t.q||"")+e},"d=":function(t,e){t.d=(t.d||"")+e},"rm=":function(t,e){t.rm=(t.rm||"")+e},"text=":function(t,e){t.text_=(t.text_||"")+e},insert:function(t,e,r){return{type_:r}},"insert+p1":function(t,e,r){return{type_:r,p1:e}},"insert+p1+p2":function(t,e,r){return{type_:r,p1:e[0],p2:e[1]}},copy:function(t,e){return e},write:function(t,e,r){return r},rm:function(t,e){return{type_:"rm",p1:e}},text:function(t,e){return o.go(e,"text")},"tex-math":function(t,e){return o.go(e,"tex-math")},"tex-math tight":function(t,e){return o.go(e,"tex-math tight")},bond:function(t,e,r){return{type_:"bond",kind_:r||e}},"color0-output":function(t,e){return{type_:"color0",color:e}},ce:function(t,e){return o.go(e,"ce")},pu:function(t,e){return o.go(e,"pu")},"1/2":function(t,e){var r=[];e.match(/^[+\-]/)&&(r.push(e.substr(0,1)),e=e.substr(1));var n=e.match(/^([0-9]+|\$[a-z]\$|[a-z])\/([0-9]+)(\$[a-z]\$|[a-z])?$/);return n[1]=n[1].replace(/\$/g,""),r.push({type_:"frac",p1:n[1],p2:n[2]}),n[3]&&(n[3]=n[3].replace(/\$/g,""),r.push({type_:"tex-math",p1:n[3]})),r},"9,9":function(t,e){return o.go(e,"9,9")}},stateMachines:{tex:{transitions:n({empty:{0:{action_:"copy"}},"\\ce{(...)}":{0:{action_:[{type_:"write",option:"{"},"ce",{type_:"write",option:"}"}]}},"\\pu{(...)}":{0:{action_:[{type_:"write",option:"{"},"pu",{type_:"write",option:"}"}]}},else:{0:{action_:"copy"}}}),actions:{}},ce:{transitions:n({empty:{"*":{action_:"output"}},else:{"0|1|2":{action_:"beginsWithBond=false",revisit:!0,toContinue:!0}},oxidation$:{0:{action_:"oxidation-output"}},CMT:{r:{action_:"rdt=",nextState:"rt"},rd:{action_:"rqt=",nextState:"rdt"}},arrowUpDown:{"0|1|2|as":{action_:["sb=false","output","operator"],nextState:"1"}},uprightEntities:{"0|1|2":{action_:["o=","output"],nextState:"1"}},orbital:{"0|1|2|3":{action_:"o=",nextState:"o"}},"->":{"0|1|2|3":{action_:"r=",nextState:"r"},"a|as":{action_:["output","r="],nextState:"r"},"*":{action_:["output","r="],nextState:"r"}},"+":{o:{action_:"d= kv",nextState:"d"},"d|D":{action_:"d=",nextState:"d"},q:{action_:"d=",nextState:"qd"},"qd|qD":{action_:"d=",nextState:"qd"},dq:{action_:["output","d="],nextState:"d"},3:{action_:["sb=false","output","operator"],nextState:"0"}},amount:{"0|2":{action_:"a=",nextState:"a"}},"pm-operator":{"0|1|2|a|as":{action_:["sb=false","output",{type_:"operator",option:"\\pm"}],nextState:"0"}},operator:{"0|1|2|a|as":{action_:["sb=false","output","operator"],nextState:"0"}},"-$":{"o|q":{action_:["charge or bond","output"],nextState:"qd"},d:{action_:"d=",nextState:"d"},D:{action_:["output",{type_:"bond",option:"-"}],nextState:"3"},q:{action_:"d=",nextState:"qd"},qd:{action_:"d=",nextState:"qd"},"qD|dq":{action_:["output",{type_:"bond",option:"-"}],nextState:"3"}},"-9":{"3|o":{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"3"}},"- orbital overlap":{o:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"},d:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"}},"-":{"0|1|2":{action_:[{type_:"output",option:1},"beginsWithBond=true",{type_:"bond",option:"-"}],nextState:"3"},3:{action_:{type_:"bond",option:"-"}},a:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"},as:{action_:[{type_:"output",option:2},{type_:"bond",option:"-"}],nextState:"3"},b:{action_:"b="},o:{action_:{type_:"- after o/d",option:!1},nextState:"2"},q:{action_:{type_:"- after o/d",option:!1},nextState:"2"},"d|qd|dq":{action_:{type_:"- after o/d",option:!0},nextState:"2"},"D|qD|p":{action_:["output",{type_:"bond",option:"-"}],nextState:"3"}},amount2:{"1|3":{action_:"a=",nextState:"a"}},letters:{"0|1|2|3|a|as|b|p|bp|o":{action_:"o=",nextState:"o"},"q|dq":{action_:["output","o="],nextState:"o"},"d|D|qd|qD":{action_:"o after d",nextState:"o"}},digits:{o:{action_:"q=",nextState:"q"},"d|D":{action_:"q=",nextState:"dq"},q:{action_:["output","o="],nextState:"o"},a:{action_:"o=",nextState:"o"}},"space A":{"b|p|bp":{action_:[]}},space:{a:{action_:[],nextState:"as"},0:{action_:"sb=false"},"1|2":{action_:"sb=true"},"r|rt|rd|rdt|rdq":{action_:"output",nextState:"0"},"*":{action_:["output","sb=true"],nextState:"1"}},"1st-level escape":{"1|2":{action_:["output",{type_:"insert+p1",option:"1st-level escape"}]},"*":{action_:["output",{type_:"insert+p1",option:"1st-level escape"}],nextState:"0"}},"[(...)]":{"r|rt":{action_:"rd=",nextState:"rd"},"rd|rdt":{action_:"rq=",nextState:"rdq"}},"...":{"o|d|D|dq|qd|qD":{action_:["output",{type_:"bond",option:"..."}],nextState:"3"},"*":{action_:[{type_:"output",option:1},{type_:"insert",option:"ellipsis"}],nextState:"1"}},". __* ":{"*":{action_:["output",{type_:"insert",option:"addition compound"}],nextState:"1"}},"state of aggregation $":{"*":{action_:["output","state of aggregation"],nextState:"1"}},"{[(":{"a|as|o":{action_:["o=","output","parenthesisLevel++"],nextState:"2"},"0|1|2|3":{action_:["o=","output","parenthesisLevel++"],nextState:"2"},"*":{action_:["output","o=","output","parenthesisLevel++"],nextState:"2"}},")]}":{"0|1|2|3|b|p|bp|o":{action_:["o=","parenthesisLevel--"],nextState:"o"},"a|as|d|D|q|qd|qD|dq":{action_:["output","o=","parenthesisLevel--"],nextState:"o"}},", ":{"*":{action_:["output","comma"],nextState:"0"}},"^_":{"*":{action_:[]}},"^{(...)}|^($...$)":{"0|1|2|as":{action_:"b=",nextState:"b"},p:{action_:"b=",nextState:"bp"},"3|o":{action_:"d= kv",nextState:"D"},q:{action_:"d=",nextState:"qD"},"d|D|qd|qD|dq":{action_:["output","d="],nextState:"D"}},"^a|^\\x{}{}|^\\x{}|^\\x|'":{"0|1|2|as":{action_:"b=",nextState:"b"},p:{action_:"b=",nextState:"bp"},"3|o":{action_:"d= kv",nextState:"d"},q:{action_:"d=",nextState:"qd"},"d|qd|D|qD":{action_:"d="},dq:{action_:["output","d="],nextState:"d"}},"_{(state of aggregation)}$":{"d|D|q|qd|qD|dq":{action_:["output","q="],nextState:"q"}},"_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x":{"0|1|2|as":{action_:"p=",nextState:"p"},b:{action_:"p=",nextState:"bp"},"3|o":{action_:"q=",nextState:"q"},"d|D":{action_:"q=",nextState:"dq"},"q|qd|qD|dq":{action_:["output","q="],nextState:"q"}},"=<>":{"0|1|2|3|a|as|o|q|d|D|qd|qD|dq":{action_:[{type_:"output",option:2},"bond"],nextState:"3"}},"#":{"0|1|2|3|a|as|o":{action_:[{type_:"output",option:2},{type_:"bond",option:"#"}],nextState:"3"}},"{}":{"*":{action_:{type_:"output",option:1},nextState:"1"}},"{...}":{"0|1|2|3|a|as|b|p|bp":{action_:"o=",nextState:"o"},"o|d|D|q|qd|qD|dq":{action_:["output","o="],nextState:"o"}},"$...$":{a:{action_:"a="},"0|1|2|3|as|b|p|bp|o":{action_:"o=",nextState:"o"},"as|o":{action_:"o="},"q|d|D|qd|qD|dq":{action_:["output","o="],nextState:"o"}},"\\bond{(...)}":{"*":{action_:[{type_:"output",option:2},"bond"],nextState:"3"}},"\\frac{(...)}":{"*":{action_:[{type_:"output",option:1},"frac-output"],nextState:"3"}},"\\overset{(...)}":{"*":{action_:[{type_:"output",option:2},"overset-output"],nextState:"3"}},"\\underset{(...)}":{"*":{action_:[{type_:"output",option:2},"underset-output"],nextState:"3"}},"\\underbrace{(...)}":{"*":{action_:[{type_:"output",option:2},"underbrace-output"],nextState:"3"}},"\\color{(...)}{(...)}":{"*":{action_:[{type_:"output",option:2},"color-output"],nextState:"3"}},"\\color{(...)}":{"*":{action_:[{type_:"output",option:2},"color0-output"]}},"\\ce{(...)}":{"*":{action_:[{type_:"output",option:2},"ce"],nextState:"3"}},"\\,":{"*":{action_:[{type_:"output",option:1},"copy"],nextState:"1"}},"\\pu{(...)}":{"*":{action_:["output",{type_:"write",option:"{"},"pu",{type_:"write",option:"}"}],nextState:"3"}},"\\x{}{}|\\x{}|\\x":{"0|1|2|3|a|as|b|p|bp|o|c0":{action_:["o=","output"],nextState:"3"},"*":{action_:["output","o=","output"],nextState:"3"}},others:{"*":{action_:[{type_:"output",option:1},"copy"],nextState:"3"}},else2:{a:{action_:"a to o",nextState:"o",revisit:!0},as:{action_:["output","sb=true"],nextState:"1",revisit:!0},"r|rt|rd|rdt|rdq":{action_:["output"],nextState:"0",revisit:!0},"*":{action_:["output","copy"],nextState:"3"}}}),actions:{"o after d":function(t,e){var r;if((t.d||"").match(/^[0-9]+$/)){var n=t.d;t.d=void 0,r=this.output(t),t.b=n}else r=this.output(t);return o.actions["o="](t,e),r},"d= kv":function(t,e){t.d=e,t.dType="kv"},"charge or bond":function(t,e){if(t.beginsWithBond){var r=[];return o.concatArray(r,this.output(t)),o.concatArray(r,o.actions.bond(t,e,"-")),r}t.d=e},"- after o/d":function(t,e,r){var n=o.patterns.match_("orbital",t.o||""),a=o.patterns.match_("one lowercase greek letter $",t.o||""),i=o.patterns.match_("one lowercase latin letter $",t.o||""),s=o.patterns.match_("$one lowercase latin letter$ $",t.o||""),l="-"===e&&(n&&""===n.remainder||a||i||s);!l||t.a||t.b||t.p||t.d||t.q||n||!i||(t.o="$"+t.o+"$");var c=[];return l?(o.concatArray(c,this.output(t)),c.push({type_:"hyphen"})):(n=o.patterns.match_("digits",t.d||""),r&&n&&""===n.remainder?(o.concatArray(c,o.actions["d="](t,e)),o.concatArray(c,this.output(t))):(o.concatArray(c,this.output(t)),o.concatArray(c,o.actions.bond(t,e,"-")))),c},"a to o":function(t){t.o=t.a,t.a=void 0},"sb=true":function(t){t.sb=!0},"sb=false":function(t){t.sb=!1},"beginsWithBond=true":function(t){t.beginsWithBond=!0},"beginsWithBond=false":function(t){t.beginsWithBond=!1},"parenthesisLevel++":function(t){t.parenthesisLevel++},"parenthesisLevel--":function(t){t.parenthesisLevel--},"state of aggregation":function(t,e){return{type_:"state of aggregation",p1:o.go(e,"o")}},comma:function(t,e){var r=e.replace(/\s*$/,"");return r!==e&&0===t.parenthesisLevel?{type_:"comma enumeration L",p1:r}:{type_:"comma enumeration M",p1:r}},output:function(t,e,r){var n;if(t.r){var a=void 0;a="M"===t.rdt?o.go(t.rd,"tex-math"):"T"===t.rdt?[{type_:"text",p1:t.rd||""}]:o.go(t.rd,"ce");var i=void 0;i="M"===t.rqt?o.go(t.rq,"tex-math"):"T"===t.rqt?[{type_:"text",p1:t.rq||""}]:o.go(t.rq,"ce"),n={type_:"arrow",r:t.r,rd:a,rq:i}}else n=[],(t.a||t.b||t.p||t.o||t.q||t.d||r)&&(t.sb&&n.push({type_:"entitySkip"}),t.o||t.q||t.d||t.b||t.p||2===r?t.o||t.q||t.d||!t.b&&!t.p?t.o&&"kv"===t.dType&&o.patterns.match_("d-oxidation$",t.d||"")?t.dType="oxidation":t.o&&"kv"===t.dType&&!t.q&&(t.dType=void 0):(t.o=t.a,t.d=t.b,t.q=t.p,t.a=t.b=t.p=void 0):(t.o=t.a,t.a=void 0),n.push({type_:"chemfive",a:o.go(t.a,"a"),b:o.go(t.b,"bd"),p:o.go(t.p,"pq"),o:o.go(t.o,"o"),q:o.go(t.q,"pq"),d:o.go(t.d,"oxidation"===t.dType?"oxidation":"bd"),dType:t.dType}));for(var s in t)"parenthesisLevel"!==s&&"beginsWithBond"!==s&&delete t[s];return n},"oxidation-output":function(t,e){var r=["{"];return o.concatArray(r,o.go(e,"oxidation")),r.push("}"),r},"frac-output":function(t,e){return{type_:"frac-ce",p1:o.go(e[0],"ce"),p2:o.go(e[1],"ce")}},"overset-output":function(t,e){return{type_:"overset",p1:o.go(e[0],"ce"),p2:o.go(e[1],"ce")}},"underset-output":function(t,e){return{type_:"underset",p1:o.go(e[0],"ce"),p2:o.go(e[1],"ce")}},"underbrace-output":function(t,e){return{type_:"underbrace",p1:o.go(e[0],"ce"),p2:o.go(e[1],"ce")}},"color-output":function(t,e){return{type_:"color",color1:e[0],color2:o.go(e[1],"ce")}},"r=":function(t,e){t.r=e},"rdt=":function(t,e){t.rdt=e},"rd=":function(t,e){t.rd=e},"rqt=":function(t,e){t.rqt=e},"rq=":function(t,e){t.rq=e},operator:function(t,e,r){return{type_:"operator",kind_:r||e}}}},a:{transitions:n({empty:{"*":{action_:[]}},"1/2$":{0:{action_:"1/2"}},else:{0:{action_:[],nextState:"1",revisit:!0}},"${(...)}$__$(...)$":{"*":{action_:"tex-math tight",nextState:"1"}},",":{"*":{action_:{type_:"insert",option:"commaDecimal"}}},else2:{"*":{action_:"copy"}}}),actions:{}},o:{transitions:n({empty:{"*":{action_:[]}},"1/2$":{0:{action_:"1/2"}},else:{0:{action_:[],nextState:"1",revisit:!0}},letters:{"*":{action_:"rm"}},"\\ca":{"*":{action_:{type_:"insert",option:"circa"}}},"\\pu{(...)}":{"*":{action_:[{type_:"write",option:"{"},"pu",{type_:"write",option:"}"}]}},"\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},"${(...)}$__$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:[{type_:"write",option:"{"},"text",{type_:"write",option:"}"}]}},else2:{"*":{action_:"copy"}}}),actions:{}},text:{transitions:n({empty:{"*":{action_:"output"}},"{...}":{"*":{action_:"text="}},"${(...)}$__$(...)$":{"*":{action_:"tex-math"}},"\\greek":{"*":{action_:["output","rm"]}},"\\pu{(...)}":{"*":{action_:["output",{type_:"write",option:"{"},"pu",{type_:"write",option:"}"}]}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:["output","copy"]}},else:{"*":{action_:"text="}}}),actions:{output:function(t){if(t.text_){var e={type_:"text",p1:t.text_};for(var r in t)delete t[r];return e}}}},pq:{transitions:n({empty:{"*":{action_:[]}},"state of aggregation $":{"*":{action_:"state of aggregation"}},i$:{0:{action_:[],nextState:"!f",revisit:!0}},"(KV letters),":{0:{action_:"rm",nextState:"0"}},formula$:{0:{action_:[],nextState:"f",revisit:!0}},"1/2$":{0:{action_:"1/2"}},else:{0:{action_:[],nextState:"!f",revisit:!0}},"${(...)}$__$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"text"}},"a-z":{f:{action_:"tex-math"}},letters:{"*":{action_:"rm"}},"-9.,9":{"*":{action_:"9,9"}},",":{"*":{action_:{type_:"insert+p1",option:"comma enumeration S"}}},"\\color{(...)}{(...)}":{"*":{action_:"color-output"}},"\\color{(...)}":{"*":{action_:"color0-output"}},"\\ce{(...)}":{"*":{action_:"ce"}},"\\pu{(...)}":{"*":{action_:[{type_:"write",option:"{"},"pu",{type_:"write",option:"}"}]}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},else2:{"*":{action_:"copy"}}}),actions:{"state of aggregation":function(t,e){return{type_:"state of aggregation subscript",p1:o.go(e,"o")}},"color-output":function(t,e){return{type_:"color",color1:e[0],color2:o.go(e[1],"pq")}}}},bd:{transitions:n({empty:{"*":{action_:[]}},x$:{0:{action_:[],nextState:"!f",revisit:!0}},formula$:{0:{action_:[],nextState:"f",revisit:!0}},else:{0:{action_:[],nextState:"!f",revisit:!0}},"-9.,9 no missing 0":{"*":{action_:"9,9"}},".":{"*":{action_:{type_:"insert",option:"electron dot"}}},"a-z":{f:{action_:"tex-math"}},x:{"*":{action_:{type_:"insert",option:"KV x"}}},letters:{"*":{action_:"rm"}},"'":{"*":{action_:{type_:"insert",option:"prime"}}},"${(...)}$__$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"text"}},"\\color{(...)}{(...)}":{"*":{action_:"color-output"}},"\\color{(...)}":{"*":{action_:"color0-output"}},"\\ce{(...)}":{"*":{action_:"ce"}},"\\pu{(...)}":{"*":{action_:[{type_:"write",option:"{"},"pu",{type_:"write",option:"}"}]}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},else2:{"*":{action_:"copy"}}}),actions:{"color-output":function(t,e){return{type_:"color",color1:e[0],color2:o.go(e[1],"bd")}}}},oxidation:{transitions:n({empty:{"*":{action_:[]}},"roman numeral":{"*":{action_:"roman-numeral"}},"${(...)}$__$(...)$":{"*":{action_:"tex-math"}},else:{"*":{action_:"copy"}}}),actions:{"roman-numeral":function(t,e){return{type_:"roman numeral",p1:e}}}},"tex-math":{transitions:n({empty:{"*":{action_:"output"}},"\\ce{(...)}":{"*":{action_:["output","ce"]}},"\\pu{(...)}":{"*":{action_:["output",{type_:"write",option:"{"},"pu",{type_:"write",option:"}"}]}},"{...}|\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"o="}},else:{"*":{action_:"o="}}}),actions:{output:function(t){if(t.o){var e={type_:"tex-math",p1:t.o};for(var r in t)delete t[r];return e}}}},"tex-math tight":{transitions:n({empty:{"*":{action_:"output"}},"\\ce{(...)}":{"*":{action_:["output","ce"]}},"\\pu{(...)}":{"*":{action_:["output",{type_:"write",option:"{"},"pu",{type_:"write",option:"}"}]}},"{...}|\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"o="}},"-|+":{"*":{action_:"tight operator"}},else:{"*":{action_:"o="}}}),actions:{"tight operator":function(t,e){t.o=(t.o||"")+"{"+e+"}"},output:function(t){if(t.o){var e={type_:"tex-math",p1:t.o};for(var r in t)delete t[r];return e}}}},"9,9":{transitions:n({empty:{"*":{action_:[]}},",":{"*":{action_:"comma"}},else:{"*":{action_:"copy"}}}),actions:{comma:function(){return{type_:"commaDecimal"}}}},pu:{transitions:n({empty:{"*":{action_:"output"}},space$:{"*":{action_:["output","space"]}},"{[(|)]}":{"0|a":{action_:"copy"}},"(-)(9)^(-9)":{0:{action_:"number^",nextState:"a"}},"(-)(9.,9)(e)(99)":{0:{action_:"enumber",nextState:"a"}},space:{"0|a":{action_:[]}},"pm-operator":{"0|a":{action_:{type_:"operator",option:"\\pm"},nextState:"0"}},operator:{"0|a":{action_:"copy",nextState:"0"}},"//":{d:{action_:"o=",nextState:"/"}},"/":{d:{action_:"o=",nextState:"/"}},"{...}|else":{"0|d":{action_:"d=",nextState:"d"},a:{action_:["space","d="],nextState:"d"},"/|q":{action_:"q=",nextState:"q"}}}),actions:{enumber:function(t,e){var r=[];return"+-"===e[0]||"+/-"===e[0]?r.push("\\pm "):e[0]&&r.push(e[0]),e[1]&&(o.concatArray(r,o.go(e[1],"pu-9,9")),e[2]&&(e[2].match(/[,.]/)?o.concatArray(r,o.go(e[2],"pu-9,9")):r.push(e[2])),(e[3]||e[4])&&("e"===e[3]||"*"===e[4]?r.push({type_:"cdot"}):r.push({type_:"times"}))),e[5]&&r.push("10^{"+e[5]+"}"),r},"number^":function(t,e){var r=[];return"+-"===e[0]||"+/-"===e[0]?r.push("\\pm "):e[0]&&r.push(e[0]),o.concatArray(r,o.go(e[1],"pu-9,9")),r.push("^{"+e[2]+"}"),r},operator:function(t,e,r){return{type_:"operator",kind_:r||e}},space:function(){return{type_:"pu-space-1"}},output:function(t){var e,r=o.patterns.match_("{(...)}",t.d||"");r&&""===r.remainder&&(t.d=r.match_);var n=o.patterns.match_("{(...)}",t.q||"");if(n&&""===n.remainder&&(t.q=n.match_),t.d&&(t.d=t.d.replace(/\u00B0C|\^oC|\^{o}C/g,"{}^{\\circ}C"),t.d=t.d.replace(/\u00B0F|\^oF|\^{o}F/g,"{}^{\\circ}F")),t.q){t.q=t.q.replace(/\u00B0C|\^oC|\^{o}C/g,"{}^{\\circ}C"),t.q=t.q.replace(/\u00B0F|\^oF|\^{o}F/g,"{}^{\\circ}F");var a={d:o.go(t.d,"pu"),q:o.go(t.q,"pu")};"//"===t.o?e={type_:"pu-frac",p1:a.d,p2:a.q}:(e=a.d,a.d.length>1||a.q.length>1?e.push({type_:" / "}):e.push({type_:"/"}),o.concatArray(e,a.q))}else e=o.go(t.d,"pu-2");for(var i in t)delete t[i];return e}}},"pu-2":{transitions:n({empty:{"*":{action_:"output"}},"*":{"*":{action_:["output","cdot"],nextState:"0"}},"\\x":{"*":{action_:"rm="}},space:{"*":{action_:["output","space"],nextState:"0"}},"^{(...)}|^(-1)":{1:{action_:"^(-1)"}},"-9.,9":{0:{action_:"rm=",nextState:"0"},1:{action_:"^(-1)",nextState:"0"}},"{...}|else":{"*":{action_:"rm=",nextState:"1"}}}),actions:{cdot:function(){return{type_:"tight cdot"}},"^(-1)":function(t,e){t.rm+="^{"+e+"}"},space:function(){return{type_:"pu-space-2"}},output:function(t){var e=[];if(t.rm){var r=o.patterns.match_("{(...)}",t.rm||"");e=r&&""===r.remainder?o.go(r.match_,"pu"):{type_:"rm",p1:t.rm}}for(var n in t)delete t[n];return e}}},"pu-9,9":{transitions:n({empty:{0:{action_:"output-0"},o:{action_:"output-o"}},",":{0:{action_:["output-0","comma"],nextState:"o"}},".":{0:{action_:["output-0","copy"],nextState:"o"}},else:{"*":{action_:"text="}}}),actions:{comma:function(){return{type_:"commaDecimal"}},"output-0":function(t){var e=[];if(t.text_=t.text_||"",t.text_.length>4){var r=t.text_.length%3;0===r&&(r=3);for(var n=t.text_.length-3;n>0;n-=3)e.push(t.text_.substr(n,3)),e.push({type_:"1000 separator"});e.push(t.text_.substr(0,r)),e.reverse()}else e.push(t.text_);for(var o in t)delete t[o];return e},"output-o":function(t){var e=[];if(t.text_=t.text_||"",t.text_.length>4){var r=t.text_.length-3,n=void 0;for(n=0;n"===t.r||"<=>>"===t.r||"<<=>"===t.r||"<--\x3e"===t.r?(s="\\long"+s,i.rd&&(s="\\overset{"+i.rd+"}{"+s+"}"),i.rq&&(s="<--\x3e"===t.r?"\\underset{\\lower2mu{"+i.rq+"}}{"+s+"}":"\\underset{\\lower6mu{"+i.rq+"}}{"+s+"}"),s=" {}\\mathrel{"+s+"}{} "):(i.rq&&(s+="[{"+i.rq+"}]"),s=" {}\\mathrel{\\x"+(s+="{"+i.rd+"}")+"}{} "):s=" {}\\mathrel{\\long"+s+"}{} ",e=s;break;case"operator":e=a._getOperator(t.kind_);break;case"1st-level escape":e=t.p1+" ";break;case"space":e=" ";break;case"entitySkip":case"pu-space-1":e="~";break;case"pu-space-2":e="\\mkern3mu ";break;case"1000 separator":e="\\mkern2mu ";break;case"commaDecimal":e="{,}";break;case"comma enumeration L":e="{"+t.p1+"}\\mkern6mu ";break;case"comma enumeration M":e="{"+t.p1+"}\\mkern3mu ";break;case"comma enumeration S":e="{"+t.p1+"}\\mkern1mu ";break;case"hyphen":e="\\text{-}";break;case"addition compound":e="\\,{\\cdot}\\,";break;case"electron dot":e="\\mkern1mu \\bullet\\mkern1mu ";break;case"KV x":e="{\\times}";break;case"prime":e="\\prime ";break;case"cdot":e="\\cdot ";break;case"tight cdot":e="\\mkern1mu{\\cdot}\\mkern1mu ";break;case"times":e="\\times ";break;case"circa":e="{\\sim}";break;case"^":e="uparrow";break;case"v":e="downarrow";break;case"ellipsis":e="\\ldots ";break;case"/":e="/";break;case" / ":e="\\,/\\,";break;default:throw["MhchemBugT","mhchem bug T. Please report."]}return e},_getArrow:function(t){switch(t){case"->":case"\u2192":case"\u27f6":return"rightarrow";case"<-":return"leftarrow";case"<->":return"leftrightarrow";case"<--\x3e":return"leftrightarrows";case"<=>":case"\u21cc":return"rightleftharpoons";case"<=>>":return"Rightleftharpoons";case"<<=>":return"Leftrightharpoons";default:throw["MhchemBugT","mhchem bug T. Please report."]}},_getBond:function(t){switch(t){case"-":case"1":return"{-}";case"=":case"2":return"{=}";case"#":case"3":return"{\\equiv}";case"~":return"{\\tripledash}";case"~-":return"{\\rlap{\\lower.1em{-}}\\raise.1em{\\tripledash}}";case"~=":case"~--":return"{\\rlap{\\lower.2em{-}}\\rlap{\\raise.2em{\\tripledash}}-}";case"-~-":return"{\\rlap{\\lower.2em{-}}\\rlap{\\raise.2em{-}}\\tripledash}";case"...":return"{{\\cdot}{\\cdot}{\\cdot}}";case"....":return"{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}";case"->":return"{\\rightarrow}";case"<-":return"{\\leftarrow}";case"<":return"{<}";case">":return"{>}";default:throw["MhchemBugT","mhchem bug T. Please report."]}},_getOperator:function(t){switch(t){case"+":return" {}+{} ";case"-":return" {}-{} ";case"=":return" {}={} ";case"<":return" {}<{} ";case">":return" {}>{} ";case"<<":return" {}\\ll{} ";case">>":return" {}\\gg{} ";case"\\pm":return" {}\\pm{} ";case"\\approx":case"$\\approx$":return" {}\\approx{} ";case"v":case"(v)":return" \\downarrow{} ";case"^":case"(^)":return" \\uparrow{} ";default:throw["MhchemBugT","mhchem bug T. Please report."]}}}},2200:function(t,e,r){var n;Object.defineProperty(e,"__esModule",{value:!0}),e.NewcommandConfiguration=void 0;var o=r(6552),a=r(6706),i=r(5282);r(6823);var s=r(4708),l=r(7628);e.NewcommandConfiguration=o.Configuration.create("newcommand",{handler:{macro:["Newcommand-macros"]},items:(n={},n[a.BeginEnvItem.prototype.kind]=a.BeginEnvItem,n),options:{maxMacros:1e3},init:function(t){new l.DelimiterMap(i.default.NEW_DELIMITER,s.default.delimiter,{}),new l.CommandMap(i.default.NEW_COMMAND,{},{}),new l.EnvironmentMap(i.default.NEW_ENVIRONMENT,s.default.environment,{},{}),t.append(o.Configuration.local({handler:{character:[],delimiter:[i.default.NEW_DELIMITER],macro:[i.default.NEW_DELIMITER,i.default.NEW_COMMAND],environment:[i.default.NEW_ENVIRONMENT]},priority:-1}))}})},6706:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.BeginEnvItem=void 0;var a=r(3466),i=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"beginEnv"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isOpen",{get:function(){return!0},enumerable:!1,configurable:!0}),e.prototype.checkItem=function(e){if(e.isKind("end")){if(e.getName()!==this.getName())throw new a.default("EnvBadEnd","\\begin{%1} ended with \\end{%2}",this.getName(),e.getName());return[[this.factory.create("mml",this.toMml())],!0]}if(e.isKind("stop"))throw new a.default("EnvMissingEnd","Missing \\end{%1}",this.getName());return t.prototype.checkItem.call(this,e)},e}(r(7044).BaseItem);e.BeginEnvItem=i},6823:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0});var n=r(8562);new(r(7628).CommandMap)("Newcommand-macros",{newcommand:"NewCommand",renewcommand:"NewCommand",newenvironment:"NewEnvironment",renewenvironment:"NewEnvironment",def:"MacroDef",let:"Let"},n.default)},8562:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0});var n=r(3466),o=r(7628),a=r(724),i=r(7702),s=r(5282),l={NewCommand:function(t,e){var r=i.default.trimSpaces(t.GetArgument(e)),o=t.GetBrackets(e),a=t.GetBrackets(e),c=t.GetArgument(e);if("\\"===r.charAt(0)&&(r=r.substr(1)),!r.match(/^(.|[a-z]+)$/i))throw new n.default("IllegalControlSequenceName","Illegal control sequence name for %1",e);if(o&&!(o=i.default.trimSpaces(o)).match(/^[0-9]+$/))throw new n.default("IllegalParamNumber","Illegal number of parameters specified in %1",e);s.default.addMacro(t,r,l.Macro,[c,o,a])},NewEnvironment:function(t,e){var r=i.default.trimSpaces(t.GetArgument(e)),o=t.GetBrackets(e),a=t.GetBrackets(e),c=t.GetArgument(e),u=t.GetArgument(e);if(o&&!(o=i.default.trimSpaces(o)).match(/^[0-9]+$/))throw new n.default("IllegalParamNumber","Illegal number of parameters specified in %1",e);s.default.addEnvironment(t,r,l.BeginEnv,[!0,c,u,o,a])},MacroDef:function(t,e){var r=s.default.GetCSname(t,e),n=s.default.GetTemplate(t,e,"\\"+r),o=t.GetArgument(e);n instanceof Array?s.default.addMacro(t,r,l.MacroWithTemplate,[o].concat(n)):s.default.addMacro(t,r,l.Macro,[o,n])},Let:function(t,e){var r=s.default.GetCSname(t,e),n=t.GetNext();"="===n&&(t.i++,n=t.GetNext());var a=t.configuration.handlers;if("\\"!==n){t.i++;var i=a.get("delimiter").lookup(n);i?s.default.addDelimiter(t,"\\"+r,i.char,i.attributes):s.default.addMacro(t,r,l.Macro,[n])}else{e=s.default.GetCSname(t,e);var c=a.get("delimiter").lookup("\\"+e);if(c)return void s.default.addDelimiter(t,"\\"+r,c.char,c.attributes);var u=a.get("macro").applicable(e);if(!u)return;if(u instanceof o.MacroMap){var p=u.lookup(e);return void s.default.addMacro(t,r,p.func,p.args,p.symbol)}c=u.lookup(e);var f=s.default.disassembleSymbol(r,c);s.default.addMacro(t,r,(function(t,e){for(var r=[],n=2;nt.configuration.options.maxMacros)throw new n.default("MaxMacroSub1","MathJax maximum macro substitution count exceeded; is here a recursive macro call?")},BeginEnv:function(t,e,r,n,o,a){if(e.getProperty("end")&&t.stack.env.closing===e.getName()){delete t.stack.env.closing;var s=t.string.slice(t.i);return t.string=n,t.i=0,t.Parse(),t.string=s,t.i=0,t.itemFactory.create("end").setProperty("name",e.getName())}if(o){var l=[];if(null!=a){var c=t.GetBrackets("\\begin{"+e.getName()+"}");l.push(null==c?a:c)}for(var u=l.length;u0?[i.toString()].concat(o):i;t.i++}throw new a.default("MissingReplacementString","Missing replacement string for definition of %1",e)},t.GetParameter=function(t,r,n){if(null==n)return t.GetArgument(r);for(var o=t.i,i=0,s=0;t.i=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.NoUndefinedConfiguration=void 0;var o=r(6552);e.NoUndefinedConfiguration=o.Configuration.create("noundefined",{fallback:{macro:function(t,e){var r,o,a=t.create("text","\\"+e),i=t.options.noundefined||{},s={};try{for(var l=n(["color","background","size"]),c=l.next();!c.done;c=l.next()){var u=c.value;i[u]&&(s["math"+u]=i[u])}}catch(t){r={error:t}}finally{try{c&&!c.done&&(o=l.return)&&o.call(l)}finally{if(r)throw r.error}}t.Push(t.create("node","mtext",[],s,a))}},options:{noundefined:{color:"red",background:"",size:""}},priority:3})},9589:function(t,e,r){var n;Object.defineProperty(e,"__esModule",{value:!0}),e.PhysicsConfiguration=void 0;var o=r(6552),a=r(4996);r(8047),e.PhysicsConfiguration=o.Configuration.create("physics",{handler:{macro:["Physics-automatic-bracing-macros","Physics-vector-macros","Physics-vector-chars","Physics-derivative-macros","Physics-expressions-macros","Physics-quick-quad-macros","Physics-bra-ket-macros","Physics-matrix-macros"],character:["Physics-characters"],environment:["Physics-aux-envs"]},items:(n={},n[a.AutoOpen.prototype.kind]=a.AutoOpen,n)})},4996:function(t,e,r){var n,o=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.AutoOpen=void 0;var a=r(7044),i=r(7702),s=r(810),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"kind",{get:function(){return"auto open"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isOpen",{get:function(){return!0},enumerable:!1,configurable:!0}),e.prototype.toMml=function(){var e=this.factory.configuration.parser,r=this.getProperty("right");if(this.getProperty("smash")){var n=t.prototype.toMml.call(this),o=e.create("node","mpadded",[n],{height:0,depth:0});this.Clear(),this.Push(e.create("node","TeXAtom",[o]))}r&&this.Push(new s.default(r,e.stack.env,e.configuration).mml());var a=t.prototype.toMml.call(this);return i.default.fenced(this.factory.configuration,this.getProperty("open"),a,this.getProperty("close"),this.getProperty("big"))},e.prototype.checkItem=function(e){var r=e.getProperty("autoclose");return r&&r===this.getProperty("close")?this.getProperty("ignore")?(this.Clear(),[[],!0]):[[this.toMml()],!0]:t.prototype.checkItem.call(this,e)},e}(a.BaseItem);e.AutoOpen=l},8047:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0});var n=r(7628),o=r(1541),a=r(7007),i=r(4708),s=r(8921);new n.CommandMap("Physics-automatic-bracing-macros",{quantity:"Quantity",qty:"Quantity",pqty:["Quantity","(",")",!0],bqty:["Quantity","[","]",!0],vqty:["Quantity","|","|",!0],Bqty:["Quantity","{","}",!0],absolutevalue:["Quantity","|","|",!0],abs:["Quantity","|","|",!0],norm:["Quantity","\\|","\\|",!0],evaluated:"Eval",eval:"Eval",order:["Quantity","(",")",!0,"O",a.TexConstant.Variant.CALLIGRAPHIC],commutator:"Commutator",comm:"Commutator",anticommutator:["Commutator","\\{","\\}"],acomm:["Commutator","\\{","\\}"],poissonbracket:["Commutator","\\{","\\}"],pb:["Commutator","\\{","\\}"]},o.default),new n.CharacterMap("Physics-vector-chars",i.default.mathchar0mi,{dotproduct:["\u22c5",{mathvariant:a.TexConstant.Variant.BOLD}],vdot:["\u22c5",{mathvariant:a.TexConstant.Variant.BOLD}],crossproduct:"\xd7",cross:"\xd7",cp:"\xd7",gradientnabla:["\u2207",{mathvariant:a.TexConstant.Variant.BOLD}],real:["\u211c",{mathvariant:a.TexConstant.Variant.NORMAL}],imaginary:["\u2111",{mathvariant:a.TexConstant.Variant.NORMAL}]}),new n.CommandMap("Physics-vector-macros",{vectorbold:"VectorBold",vb:"VectorBold",vectorarrow:["StarMacro",1,"\\vec{\\vb","{#1}}"],va:["StarMacro",1,"\\vec{\\vb","{#1}}"],vectorunit:["StarMacro",1,"\\hat{\\vb","{#1}}"],vu:["StarMacro",1,"\\hat{\\vb","{#1}}"],gradient:["OperatorApplication","\\gradientnabla","(","["],grad:["OperatorApplication","\\gradientnabla","(","["],divergence:["VectorOperator","\\gradientnabla\\vdot","(","["],div:["VectorOperator","\\gradientnabla\\vdot","(","["],curl:["VectorOperator","\\gradientnabla\\crossproduct","(","["],laplacian:["OperatorApplication","\\nabla^2","(","["]},o.default),new n.CommandMap("Physics-expressions-macros",{sin:"Expression",sinh:"Expression",arcsin:"Expression",asin:"Expression",cos:"Expression",cosh:"Expression",arccos:"Expression",acos:"Expression",tan:"Expression",tanh:"Expression",arctan:"Expression",atan:"Expression",csc:"Expression",csch:"Expression",arccsc:"Expression",acsc:"Expression",sec:"Expression",sech:"Expression",arcsec:"Expression",asec:"Expression",cot:"Expression",coth:"Expression",arccot:"Expression",acot:"Expression",exp:["Expression",!1],log:"Expression",ln:"Expression",det:["Expression",!1],Pr:["Expression",!1],tr:["Expression",!1],trace:["Expression",!1,"tr"],Tr:["Expression",!1],Trace:["Expression",!1,"Tr"],rank:"NamedFn",erf:["Expression",!1],Res:["OperatorApplication","{\\rm Res}","(","[","{"],principalvalue:["OperatorApplication","{\\cal P}"],pv:["OperatorApplication","{\\cal P}"],PV:["OperatorApplication","{\\rm P.V.}"],Re:["OperatorApplication","{\\rm Re}","{"],Im:["OperatorApplication","{\\rm Im}","{"],sine:["NamedFn","sin"],hypsine:["NamedFn","sinh"],arcsine:["NamedFn","arcsin"],asine:["NamedFn","asin"],cosine:["NamedFn","cos"],hypcosine:["NamedFn","cosh"],arccosine:["NamedFn","arccos"],acosine:["NamedFn","acos"],tangent:["NamedFn","tan"],hyptangent:["NamedFn","tanh"],arctangent:["NamedFn","arctan"],atangent:["NamedFn","atan"],cosecant:["NamedFn","csc"],hypcosecant:["NamedFn","csch"],arccosecant:["NamedFn","arccsc"],acosecant:["NamedFn","acsc"],secant:["NamedFn","sec"],hypsecant:["NamedFn","sech"],arcsecant:["NamedFn","arcsec"],asecant:["NamedFn","asec"],cotangent:["NamedFn","cot"],hypcotangent:["NamedFn","coth"],arccotangent:["NamedFn","arccot"],acotangent:["NamedFn","acot"],exponential:["NamedFn","exp"],logarithm:["NamedFn","log"],naturallogarithm:["NamedFn","ln"],determinant:["NamedFn","det"],Probability:["NamedFn","Pr"]},o.default),new n.CommandMap("Physics-quick-quad-macros",{qqtext:"Qqtext",qq:"Qqtext",qcomma:["Macro","\\qqtext*{,}"],qc:["Macro","\\qqtext*{,}"],qcc:["Qqtext","c.c."],qif:["Qqtext","if"],qthen:["Qqtext","then"],qelse:["Qqtext","else"],qotherwise:["Qqtext","otherwise"],qunless:["Qqtext","unless"],qgiven:["Qqtext","given"],qusing:["Qqtext","using"],qassume:["Qqtext","assume"],"qsince,":["Qqtext","since,"],qlet:["Qqtext","let"],qfor:["Qqtext","for"],qall:["Qqtext","all"],qeven:["Qqtext","even"],qodd:["Qqtext","odd"],qinteger:["Qqtext","integer"],qand:["Qqtext","and"],qor:["Qqtext","or"],qas:["Qqtext","as"],qin:["Qqtext","in"]},o.default),new n.CommandMap("Physics-derivative-macros",{flatfrac:["Macro","\\left.#1\\middle/#2\\right.",2],differential:["Differential","{\\rm d}"],dd:["Differential","{\\rm d}"],variation:["Differential","\\delta"],var:["Differential","\\delta"],derivative:["Derivative",2,"{\\rm d}"],dv:["Derivative",2,"{\\rm d}"],partialderivative:["Derivative",3,"\\partial"],pderivative:["Derivative",3,"\\partial"],pdv:["Derivative",3,"\\partial"],functionalderivative:["Derivative",2,"\\delta"],fderivative:["Derivative",2,"\\delta"],fdv:["Derivative",2,"\\delta"]},o.default),new n.CommandMap("Physics-bra-ket-macros",{bra:"Bra",ket:"Ket",innerproduct:"BraKet",braket:"BraKet",outerproduct:"KetBra",dyad:"KetBra",ketbra:"KetBra",op:"KetBra",expectationvalue:"Expectation",expval:"Expectation",ev:"Expectation",matrixelement:"MatrixElement",matrixel:"MatrixElement",mel:"MatrixElement"},o.default),new n.CommandMap("Physics-matrix-macros",{matrixquantity:"MatrixQuantity",mqty:"MatrixQuantity",pmqty:["Macro","\\mqty(#1)",1],Pmqty:["Macro","\\mqty*(#1)",1],bmqty:["Macro","\\mqty[#1]",1],vmqty:["Macro","\\mqty|#1|",1],smallmatrixquantity:["MatrixQuantity",!0],smqty:["MatrixQuantity",!0],spmqty:["Macro","\\smqty(#1)",1],sPmqty:["Macro","\\smqty*(#1)",1],sbmqty:["Macro","\\smqty[#1]",1],svmqty:["Macro","\\smqty|#1|",1],matrixdeterminant:["Macro","\\vmqty{#1}",1],mdet:["Macro","\\vmqty{#1}",1],smdet:["Macro","\\svmqty{#1}",1],identitymatrix:"IdentityMatrix",imat:"IdentityMatrix",xmatrix:"XMatrix",xmat:"XMatrix",zeromatrix:["Macro","\\xmat{0}{#1}{#2}",2],zmat:["Macro","\\xmat{0}{#1}{#2}",2],paulimatrix:"PauliMatrix",pmat:"PauliMatrix",diagonalmatrix:"DiagonalMatrix",dmat:"DiagonalMatrix",antidiagonalmatrix:["DiagonalMatrix",!0],admat:["DiagonalMatrix",!0]},o.default),new n.EnvironmentMap("Physics-aux-envs",i.default.environment,{smallmatrix:["Array",null,null,null,"c","0.333em",".2em","S",1]},o.default),new n.MacroMap("Physics-characters",{"|":["AutoClose",s.TEXCLASS.ORD],")":"AutoClose","]":"AutoClose"},o.default)},1541:function(t,e,r){var n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i};Object.defineProperty(e,"__esModule",{value:!0});var o=r(724),a=r(810),i=r(3466),s=r(8921),l=r(7702),c=r(8321),u=r(8644),p={},f={"(":")","[":"]","{":"}","|":"|"},d=/^(b|B)i(g{1,2})$/;p.Quantity=function(t,e,r,n,o,u,p){void 0===r&&(r="("),void 0===n&&(n=")"),void 0===o&&(o=!1),void 0===u&&(u=""),void 0===p&&(p="");var h=!!o&&t.GetStar(),m=t.GetNext(),g=t.i,y=null;if("\\"===m){if(t.i++,!(y=t.GetCS()).match(d)){var v=t.create("node","mrow");return t.Push(l.default.fenced(t.configuration,r,v,n)),void(t.i=g)}m=t.GetNext()}var b=f[m];if(o&&"{"!==m)throw new i.default("MissingArgFor","Missing argument for %1",t.currentCS);if(!b){v=t.create("node","mrow");return t.Push(l.default.fenced(t.configuration,r,v,n)),void(t.i=g)}if(u){var x=t.create("token","mi",{texClass:s.TEXCLASS.OP},u);p&&c.default.setAttribute(x,"mathvariant",p),t.Push(t.itemFactory.create("fn",x))}if("{"===m){var _=t.GetArgument(e);return m=o?r:"\\{",b=o?n:"\\}",_=h?m+" "+_+" "+b:y?"\\"+y+"l"+m+" "+_+" \\"+y+"r"+b:"\\left"+m+" "+_+" \\right"+b,void t.Push(new a.default(_,t.stack.env,t.configuration).mml())}o&&(m=r,b=n),t.i++,t.Push(t.itemFactory.create("auto open").setProperties({open:m,close:b,big:y}))},p.Eval=function(t,e){var r=t.GetStar(),n=t.GetNext();if("{"!==n){if("("===n||"["===n)return t.i++,void t.Push(t.itemFactory.create("auto open").setProperties({open:n,close:"|",smash:r,right:"\\vphantom{\\int}"}));throw new i.default("MissingArgFor","Missing argument for %1",t.currentCS)}var o=t.GetArgument(e),a="\\left. "+(r?"\\smash{"+o+"}":o)+" \\vphantom{\\int}\\right|";t.string=t.string.slice(0,t.i)+a+t.string.slice(t.i)},p.Commutator=function(t,e,r,n){void 0===r&&(r="["),void 0===n&&(n="]");var o=t.GetStar(),s=t.GetNext(),l=null;if("\\"===s){if(t.i++,!(l=t.GetCS()).match(d))throw new i.default("MissingArgFor","Missing argument for %1",t.currentCS);s=t.GetNext()}if("{"!==s)throw new i.default("MissingArgFor","Missing argument for %1",t.currentCS);var c=t.GetArgument(e)+","+t.GetArgument(e);c=o?r+" "+c+" "+n:l?"\\"+l+"l"+r+" "+c+" \\"+l+"r"+n:"\\left"+r+" "+c+" \\right"+n,t.Push(new a.default(c,t.stack.env,t.configuration).mml())};var h=[65,90],m=[97,122],g=[913,937],y=[945,969],v=[48,57];function b(t,e){return t>=e[0]&&t<=e[1]}function x(t,e,r,n){var o=t.configuration.parser,a=u.NodeFactory.createToken(t,e,r,n),i=n.codePointAt(0);return 1===n.length&&!o.stack.env.font&&o.stack.env.vectorFont&&(b(i,h)||b(i,m)||b(i,g)||b(i,v)||b(i,y)&&o.stack.env.vectorStar||c.default.getAttribute(a,"accent"))&&c.default.setAttribute(a,"mathvariant",o.stack.env.vectorFont),a}p.VectorBold=function(t,e){var r=t.GetStar(),n=t.GetArgument(e),o=t.configuration.nodeFactory.get("token"),i=t.stack.env.font;delete t.stack.env.font,t.configuration.nodeFactory.set("token",x),t.stack.env.vectorFont=r?"bold-italic":"bold",t.stack.env.vectorStar=r;var s=new a.default(n,t.stack.env,t.configuration).mml();i&&(t.stack.env.font=i),delete t.stack.env.vectorFont,delete t.stack.env.vectorStar,t.configuration.nodeFactory.set("token",o),t.Push(s)},p.StarMacro=function(t,e,r){for(var n=[],o=3;ot.configuration.options.maxMacros)throw new i.default("MaxMacroSub1","MathJax maximum macro substitution count exceeded; is there a recursive macro call?")};var _=function(t,e,r,n,o){var i=new a.default(n,t.stack.env,t.configuration).mml();t.Push(t.itemFactory.create(e,i));var s=t.GetNext(),l=f[s];if(l){var c=-1!==o.indexOf(s);if("{"===s){var u=(c?"\\left\\{":"")+" "+t.GetArgument(r)+" "+(c?"\\right\\}":"");return t.string=u+t.string.slice(t.i),void(t.i=0)}c&&(t.i++,t.Push(t.itemFactory.create("auto open").setProperties({open:s,close:l})))}};function A(t,e,r){var o=n(t,3),a=o[0],i=o[1],s=o[2];return e&&r?"\\left\\langle{"+a+"}\\middle\\vert{"+i+"}\\middle\\vert{"+s+"}\\right\\rangle":e?"\\langle{"+a+"}\\vert{"+i+"}\\vert{"+s+"}\\rangle":"\\left\\langle{"+a+"}\\right\\vert{"+i+"}\\left\\vert{"+s+"}\\right\\rangle"}p.OperatorApplication=function(t,e,r){for(var n=[],o=3;o2&&l.length>2?(u="^{"+(l.length-1)+"}",c=!0):null!=i&&(r>2&&l.length>1&&(c=!0),p=u="^{"+i+"}");for(var f=o?"\\flatfrac":"\\frac",d=l.length>1?l[0]:"",h=l.length>1?l[1]:l[0],m="",g=2,y=void 0;y=l[g];g++)m+=n+" "+y;var v=f+"{"+n+u+d+"}{"+n+" "+h+p+" "+m+"}";t.Push(new a.default(v,t.stack.env,t.configuration).mml()),"("===t.GetNext()&&(t.i++,t.Push(t.itemFactory.create("auto open").setProperties({open:"(",close:")",ignore:c})))},p.Bra=function(t,e){var r=t.GetStar(),n=t.GetArgument(e),o="",i=!1,s=!1;if("\\"===t.GetNext()){var l=t.i;t.i++;var c=t.GetCS(),u=t.lookup("macro",c);u&&"ket"===u.symbol?(i=!0,l=t.i,s=t.GetStar(),"{"===t.GetNext()?o=t.GetArgument(c,!0):(t.i=l,s=!1)):t.i=l}var p="";p=i?r||s?"\\langle{"+n+"}\\vert{"+o+"}\\rangle":"\\left\\langle{"+n+"}\\middle\\vert{"+o+"}\\right\\rangle":r||s?"\\langle{"+n+"}\\vert":"\\left\\langle{"+n+"}\\right\\vert{"+o+"}",t.Push(new a.default(p,t.stack.env,t.configuration).mml())},p.Ket=function(t,e){var r=t.GetStar(),n=t.GetArgument(e),o=r?"\\vert{"+n+"}\\rangle":"\\left\\vert{"+n+"}\\right\\rangle";t.Push(new a.default(o,t.stack.env,t.configuration).mml())},p.BraKet=function(t,e){var r=t.GetStar(),n=t.GetArgument(e),o=null;"{"===t.GetNext()&&(o=t.GetArgument(e,!0));var i="";i=null==o?r?"\\langle{"+n+"}\\vert{"+n+"}\\rangle":"\\left\\langle{"+n+"}\\middle\\vert{"+n+"}\\right\\rangle":r?"\\langle{"+n+"}\\vert{"+o+"}\\rangle":"\\left\\langle{"+n+"}\\middle\\vert{"+o+"}\\right\\rangle",t.Push(new a.default(i,t.stack.env,t.configuration).mml())},p.KetBra=function(t,e){var r=t.GetStar(),n=t.GetArgument(e),o=null;"{"===t.GetNext()&&(o=t.GetArgument(e,!0));var i="";i=null==o?r?"\\vert{"+n+"}\\rangle\\!\\langle{"+n+"}\\vert":"\\left\\vert{"+n+"}\\middle\\rangle\\!\\middle\\langle{"+n+"}\\right\\vert":r?"\\vert{"+n+"}\\rangle\\!\\langle{"+o+"}\\vert":"\\left\\vert{"+n+"}\\middle\\rangle\\!\\middle\\langle{"+o+"}\\right\\vert",t.Push(new a.default(i,t.stack.env,t.configuration).mml())},p.Expectation=function(t,e){var r=t.GetStar(),n=r&&t.GetStar(),o=t.GetArgument(e),i=null;"{"===t.GetNext()&&(i=t.GetArgument(e,!0));var s=o&&i?A([i,o,i],r,n):r?"\\langle {"+o+"} \\rangle":"\\left\\langle {"+o+"} \\right\\rangle";t.Push(new a.default(s,t.stack.env,t.configuration).mml())},p.MatrixElement=function(t,e){var r=t.GetStar(),n=r&&t.GetStar(),o=A([t.GetArgument(e),t.GetArgument(e),t.GetArgument(e)],r,n);t.Push(new a.default(o,t.stack.env,t.configuration).mml())},p.MatrixQuantity=function(t,e,r){var n=t.GetStar(),o=r?"smallmatrix":"array",i="",s="",l="";switch(t.GetNext()){case"{":i=t.GetArgument(e);break;case"(":t.i++,s=n?"\\lgroup":"(",l=n?"\\rgroup":")",i=t.GetUpTo(e,")");break;case"[":t.i++,s="[",l="]",i=t.GetUpTo(e,"]");break;case"|":t.i++,s="|",l="|",i=t.GetUpTo(e,"|");break;default:s="(",l=")"}var c=(s?"\\left":"")+s+"\\begin{"+o+"}{} "+i+"\\end{"+o+"}"+(s?"\\right":"")+l;t.Push(new a.default(c,t.stack.env,t.configuration).mml())},p.IdentityMatrix=function(t,e){var r=t.GetArgument(e),n=parseInt(r,10);if(isNaN(n))throw new i.default("InvalidNumber","Invalid number");if(n<=1)return t.string="1"+t.string.slice(t.i),void(t.i=0);for(var o=Array(n).fill("0"),a=[],s=0;s=o){a.push(t.string.slice(s,o));break}s=t.i,a.push(i)}t.string=function(t,e){for(var r=t.length,n=[],o=0;o=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},o=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},a=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r":["Spacer",i.MATHSPACE.mediummathspace],";":["Spacer",i.MATHSPACE.thickmathspace],"!":["Spacer",i.MATHSPACE.negativethinmathspace],enspace:["Spacer",.5],quad:["Spacer",1],qquad:["Spacer",2],thinspace:["Spacer",i.MATHSPACE.thinmathspace],negthinspace:["Spacer",i.MATHSPACE.negativethinmathspace],hskip:"Hskip",hspace:"Hskip",kern:"Hskip",mskip:"Hskip",mspace:"Hskip",mkern:"Hskip",rule:"rule",Rule:["Rule"],Space:["Rule","blank"],color:"CheckAutoload",textcolor:"CheckAutoload",colorbox:"CheckAutoload",fcolorbox:"CheckAutoload",href:"CheckAutoload",style:"CheckAutoload",class:"CheckAutoload",cssId:"CheckAutoload",unicode:"CheckAutoload",ref:["HandleRef",!1],eqref:["HandleRef",!0]},a.TextMacrosMethods)},440:function(t,e,r){Object.defineProperty(e,"__esModule",{value:!0}),e.TextMacrosMethods=void 0;var n=r(810),o=r(239),a=r(724);e.TextMacrosMethods={Comment:function(t,e){for(;t.i=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var n,o,a=r.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(t){o={error:t}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},s=this&&this.__spreadArray||function(t,e){for(var r=0,n=e.length,o=t.length;r=0&&(l[c]=r),n&&s[e]&&((0,t.combineConfig)(s,(o={},a=r,i=s[e],a in o?Object.defineProperty(o,a,{value:i,enumerable:!0,configurable:!0,writable:!0}):o[a]=i,o)),delete s[e])}}function vt(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")};function o(e){return"object"==typeof e&&null!==e}function a(e,t){var r,i;try{for(var c=n(Object.keys(t)),s=c.next();!s.done;s=c.next()){var u=s.value;"__esModule"!==u&&(!o(e[u])||!o(t[u])||t[u]instanceof Promise?null!==t[u]&&void 0!==t[u]&&(e[u]=t[u]):a(e[u],t[u]))}}catch(e){r={error:e}}finally{try{s&&!s.done&&(i=c.return)&&i.call(c)}finally{if(r)throw r.error}}return e}Object.defineProperty(t,"__esModule",{value:!0}),t.MathJax=t.combineWithMathJax=t.combineDefaults=t.combineConfig=t.isObject=void 0,t.isObject=o,t.combineConfig=a,t.combineDefaults=function e(t,r,a){var i,c;t[r]||(t[r]={}),t=t[r];try{for(var s=n(Object.keys(a)),u=s.next();!u.done;u=s.next()){var l=u.value;o(t[l])&&o(a[l])?e(t,l,a[l]):null==t[l]&&null!=a[l]&&(t[l]=a[l])}}catch(e){i={error:e}}finally{try{u&&!u.done&&(c=s.return)&&c.call(s)}finally{if(i)throw i.error}}return t},t.combineWithMathJax=function(e){return a(t.MathJax,e)},void 0===r.g.MathJax&&(r.g.MathJax={}),r.g.MathJax.version||(r.g.MathJax={version:"3.1.4",_:{},config:r.g.MathJax}),t.MathJax=r.g.MathJax},235:function(e,t,r){var n=this&&this.__values||function(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(t,"__esModule",{value:!0}),t.CONFIG=t.MathJax=t.Loader=t.PathFilters=t.PackageError=t.Package=void 0;var o=r(515),a=r(265),i=r(265);Object.defineProperty(t,"Package",{enumerable:!0,get:function(){return i.Package}}),Object.defineProperty(t,"PackageError",{enumerable:!0,get:function(){return i.PackageError}});var c,s=r(525);t.PathFilters={source:function(e){return t.CONFIG.source.hasOwnProperty(e.name)&&(e.name=t.CONFIG.source[e.name]),!0},normalize:function(e){var t=e.name;return t.match(/^(?:[a-z]+:\/)?\/|[a-z]:\\|\[/i)||(e.name="[mathjax]/"+t.replace(/^\.\//,"")),e.addExtension&&!t.match(/\.[^\/]+$/)&&(e.name+=".js"),!0},prefix:function(e){for(var r;(r=e.name.match(/^\[([^\]]*)\]/))&&t.CONFIG.paths.hasOwnProperty(r[1]);)e.name=t.CONFIG.paths[r[1]]+e.name.substr(r[0].length);return!0}},function(e){e.ready=function(){for(var e,t,r=[],o=0;o=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")},i=this&&this.__read||function(e,t){var r="function"==typeof Symbol&&e[Symbol.iterator];if(!r)return e;var n,o,a=r.call(e),i=[];try{for(;(void 0===t||t-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(e){o={error:e}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},c=this&&this.__spreadArray||function(e,t){for(var r=0,n=t.length,o=e.length;r=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")},i=this&&this.__read||function(e,t){var r="function"==typeof Symbol&&e[Symbol.iterator];if(!r)return e;var n,o,a=r.call(e),i=[];try{for(;(void 0===t||t-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(e){o={error:e}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},c=this&&this.__spreadArray||function(e,t){for(var r=0,n=t.length,o=e.length;rt.length}}}},e.prototype.add=function(t,r){void 0===r&&(r=e.DEFAULTPRIORITY);var n=this.items.length;do{n--}while(n>=0&&r=0&&this.items[t].item!==e);t>=0&&this.items.splice(t,1)},e.prototype.toArray=function(){return Array.from(this)},e.DEFAULTPRIORITY=5,e}();t.PrioritizedList=r}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n].call(a.exports,a,a.exports,r),a.exports}r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),function(){var e=r(515),t=r(235),n=r(265);(0,e.combineWithMathJax)({_:{components:{loader:t,package:n}}});var o,a={tex:"[mathjax]/input/tex/extensions",sre:"[mathjax]/sre/"+("undefined"==typeof window?"sre-node":"sre_browser")},i=["[tex]/action","[tex]/ams","[tex]/amscd","[tex]/bbox","[tex]/boldsymbol","[tex]/braket","[tex]/bussproofs","[tex]/cancel","[tex]/color","[tex]/configmacros","[tex]/enclose","[tex]/extpfeil","[tex]/html","[tex]/mhchem","[tex]/newcommand","[tex]/noerrors","[tex]/noundefined","[tex]/physics","[tex]/require","[tex]/tagformat","[tex]/textmacros","[tex]/unicode","[tex]/verb"],c={startup:["loader"],"input/tex":["input/tex-base","[tex]/ams","[tex]/newcommand","[tex]/noundefined","[tex]/require","[tex]/autoload","[tex]/configmacros"],"input/tex-full":["input/tex-base","[tex]/all-packages"].concat(i),"[tex]/all-packages":i};function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTML=void 0;var s=r(716),l=r(4477),h=r(4142),c=r(6914),u=r(6720),p=function(t){function e(e){void 0===e&&(e=null);var r=t.call(this,e,l.CHTMLWrapperFactory,h.TeXFont)||this;return r.chtmlStyles=null,r.font.adaptiveCSS(r.options.adaptiveCSS),r}return n(e,t),e.prototype.escaped=function(t,e){return this.setDocument(e),this.html("span",{},[this.text(t.math)])},e.prototype.styleSheet=function(r){if(this.chtmlStyles&&!this.options.adaptiveCSS)return this.chtmlStyles;var o=this.chtmlStyles=t.prototype.styleSheet.call(this,r);return this.adaptor.setAttribute(o,"id",e.STYLESHEETID),o},e.prototype.addClassStyles=function(e){var r;this.options.adaptiveCSS&&!e.used||(e.autoStyle&&"unknown"!==e.kind&&this.cssStyles.addStyles(((r={})["mjx-"+e.kind]={display:"inline-block","text-align":"left"},r)),t.prototype.addClassStyles.call(this,e))},e.prototype.processMath=function(t,e){this.factory.wrap(t).toCHTML(e)},e.prototype.clearCache=function(){var t,e;this.cssStyles.clear(),this.font.clearCache();try{for(var r=a(this.factory.getKinds()),o=r.next();!o.done;o=r.next()){var n=o.value;this.factory.getNodeClass(n).used=!1}}catch(e){t={error:e}}finally{try{o&&!o.done&&(e=r.return)&&e.call(r)}finally{if(t)throw t.error}}},e.prototype.reset=function(){this.clearCache()},e.prototype.unknownText=function(t,e){var r={},o=100/this.math.metrics.scale;if(100!==o&&(r["font-size"]=this.fixed(o,1)+"%",r.padding=c.em(75/o)+" 0 "+c.em(20/o)+" 0"),"-explicitFont"!==e){var n=u.unicodeChars(t);(1!==n.length||n[0]<119808||n[0]>120831)&&this.cssFontStyles(this.font.getCssFont(e),r)}return this.html("mjx-utext",{variant:e,style:r},[this.text(t)])},e.prototype.measureTextNode=function(t){var e=this.adaptor;t=e.clone(t);var r=this.html("mjx-measure-text",{style:{position:"absolute","white-space":"nowrap"}},[t]);e.append(e.parent(this.math.start.node),this.container),e.append(this.container,r);var o=e.nodeSize(t,this.math.metrics.em)[0]/this.math.metrics.scale;return e.remove(this.container),e.remove(r),{w:o,h:.75,d:.2}},e.NAME="CHTML",e.OPTIONS=i(i({},s.CommonOutputJax.OPTIONS),{adaptiveCSS:!0}),e.commonStyles={'mjx-container[jax="CHTML"]':{"line-height":0},'mjx-container [space="1"]':{"margin-left":".111em"},'mjx-container [space="2"]':{"margin-left":".167em"},'mjx-container [space="3"]':{"margin-left":".222em"},'mjx-container [space="4"]':{"margin-left":".278em"},'mjx-container [space="5"]':{"margin-left":".333em"},'mjx-container [rspace="1"]':{"margin-right":".111em"},'mjx-container [rspace="2"]':{"margin-right":".167em"},'mjx-container [rspace="3"]':{"margin-right":".222em"},'mjx-container [rspace="4"]':{"margin-right":".278em"},'mjx-container [rspace="5"]':{"margin-right":".333em"},'mjx-container [size="s"]':{"font-size":"70.7%"},'mjx-container [size="ss"]':{"font-size":"50%"},'mjx-container [size="Tn"]':{"font-size":"60%"},'mjx-container [size="sm"]':{"font-size":"85%"},'mjx-container [size="lg"]':{"font-size":"120%"},'mjx-container [size="Lg"]':{"font-size":"144%"},'mjx-container [size="LG"]':{"font-size":"173%"},'mjx-container [size="hg"]':{"font-size":"207%"},'mjx-container [size="HG"]':{"font-size":"249%"},'mjx-container [width="full"]':{width:"100%"},"mjx-box":{display:"inline-block"},"mjx-block":{display:"block"},"mjx-itable":{display:"inline-table"},"mjx-row":{display:"table-row"},"mjx-row > *":{display:"table-cell"},"mjx-mtext":{display:"inline-block"},"mjx-mstyle":{display:"inline-block"},"mjx-merror":{display:"inline-block",color:"red","background-color":"yellow"},"mjx-mphantom":{visibility:"hidden"}},e.STYLESHEETID="MJX-CHTML-styles",e}(s.CommonOutputJax);e.CHTML=p},2098:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,o=arguments.length;r=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},h=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.AddCSS=e.CHTMLFontData=void 0;var c=r(9250),u=r(6914);s(r(9250),e);var p=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.charOptions=function(e,r){return t.charOptions.call(this,e,r)},e.prototype.adaptiveCSS=function(t){this.options.adaptiveCSS=t},e.prototype.clearCache=function(){var t,e,r,o,n,i;if(this.options.adaptiveCSS){try{for(var a=l(Object.keys(this.delimiters)),s=a.next();!s.done;s=a.next()){var h=s.value;this.delimiters[parseInt(h)].used=!1}}catch(e){t={error:e}}finally{try{s&&!s.done&&(e=a.return)&&e.call(a)}finally{if(t)throw t.error}}try{for(var c=l(Object.keys(this.variant)),u=c.next();!u.done;u=c.next()){var p=u.value,d=this.variant[p].chars;try{for(var f=(n=void 0,l(Object.keys(d))),m=f.next();!m.done;m=f.next()){h=m.value;var y=d[parseInt(h)][3];y&&(y.used=!1)}}catch(t){n={error:t}}finally{try{m&&!m.done&&(i=f.return)&&i.call(f)}finally{if(n)throw n.error}}}}catch(t){r={error:t}}finally{try{u&&!u.done&&(o=c.return)&&o.call(c)}finally{if(r)throw r.error}}}},e.prototype.createVariant=function(e,r,o){void 0===r&&(r=null),void 0===o&&(o=null),t.prototype.createVariant.call(this,e,r,o);var n=this.constructor;this.variant[e].classes=n.defaultVariantClasses[e],this.variant[e].letter=n.defaultVariantLetters[e]},e.prototype.defineChars=function(r,o){var n,i;t.prototype.defineChars.call(this,r,o);var a=this.variant[r].letter;try{for(var s=l(Object.keys(o)),h=s.next();!h.done;h=s.next()){var c=h.value,u=e.charOptions(o,parseInt(c));void 0===u.f&&(u.f=a)}}catch(t){n={error:t}}finally{try{h&&!h.done&&(i=s.return)&&i.call(s)}finally{if(n)throw n.error}}},Object.defineProperty(e.prototype,"styles",{get:function(){var t,e,r=this.constructor,o=i({},r.defaultStyles);this.addFontURLs(o,r.defaultFonts,this.options.fontURL);try{for(var n=l(Object.keys(this.delimiters)),a=n.next();!a.done;a=n.next()){var s=a.value,h=parseInt(s);this.addDelimiterStyles(o,h,this.delimiters[h])}}catch(e){t={error:e}}finally{try{a&&!a.done&&(e=n.return)&&e.call(n)}finally{if(t)throw t.error}}return this.addVariantChars(o),o},enumerable:!1,configurable:!0}),e.prototype.addVariantChars=function(t){var e,r,o,n,i=!this.options.adaptiveCSS;try{for(var a=l(Object.keys(this.variant)),s=a.next();!s.done;s=a.next()){var h=s.value,c=this.variant[h],u=c.letter;try{for(var p=(o=void 0,l(Object.keys(c.chars))),d=p.next();!d.done;d=p.next()){var f=d.value,m=parseInt(f),y=c.chars[m];(y[3]||{}).smp||(i&&y.length<4&&(y[3]={}),(4===y.length||i)&&this.addCharStyles(t,u,m,y))}}catch(t){o={error:t}}finally{try{d&&!d.done&&(n=p.return)&&n.call(p)}finally{if(o)throw o.error}}}}catch(t){e={error:t}}finally{try{s&&!s.done&&(r=a.return)&&r.call(a)}finally{if(e)throw e.error}}},e.prototype.addFontURLs=function(t,e,r){var o,n;try{for(var a=l(Object.keys(e)),s=a.next();!s.done;s=a.next()){var h=s.value,c=i({},e[h]);c.src=c.src.replace(/%%URL%%/,r),t[h]=c}}catch(t){o={error:t}}finally{try{s&&!s.done&&(n=a.return)&&n.call(a)}finally{if(o)throw o.error}}},e.prototype.addDelimiterStyles=function(t,e,r){if(!this.options.adaptiveCSS||r.used){var o=this.charSelector(e);r.c&&r.c!==e&&(t[".mjx-stretched mjx-c"+o+"::before"]={content:this.charContent(r.c)}),r.stretch&&(1===r.dir?this.addDelimiterVStyles(t,o,r):this.addDelimiterHStyles(t,o,r))}},e.prototype.addDelimiterVStyles=function(t,e,r){var o=r.HDW,n=h(r.stretch,4),i=n[0],a=n[1],s=n[2],l=n[3],c=this.addDelimiterVPart(t,e,"beg",i,o);this.addDelimiterVPart(t,e,"ext",a,o);var u=this.addDelimiterVPart(t,e,"end",s,o),p={};if(l){var d=this.addDelimiterVPart(t,e,"mid",l,o);p.height="50%",t["mjx-stretchy-v"+e+" > mjx-mid"]={"margin-top":this.em(-d/2),"margin-bottom":this.em(-d/2)}}c&&(p["border-top-width"]=this.em0(c-.03)),u&&(p["border-bottom-width"]=this.em0(u-.03),t["mjx-stretchy-v"+e+" > mjx-end"]={"margin-top":this.em(-u)}),Object.keys(p).length&&(t["mjx-stretchy-v"+e+" > mjx-ext"]=p)},e.prototype.addDelimiterVPart=function(t,e,r,o,n){if(!o)return 0;var i=this.getDelimiterData(o),a=(n[2]-i[2])/2,s={content:this.charContent(o)};return"ext"!==r?s.padding=this.padding(i,a):(s.width=this.em0(n[2]),a&&(s["padding-left"]=this.em0(a))),t["mjx-stretchy-v"+e+" mjx-"+r+" mjx-c::before"]=s,i[0]+i[1]},e.prototype.addDelimiterHStyles=function(t,e,r){var o=h(r.stretch,4),n=o[0],i=o[1],a=o[2],s=o[3],l=r.HDW;this.addDelimiterHPart(t,e,"beg",n,l),this.addDelimiterHPart(t,e,"ext",i,l),this.addDelimiterHPart(t,e,"end",a,l),s&&(this.addDelimiterHPart(t,e,"mid",s,l),t["mjx-stretchy-h"+e+" > mjx-ext"]={width:"50%"})},e.prototype.addDelimiterHPart=function(t,e,r,o,n){if(o){var i=this.getDelimiterData(o)[3],a={content:i&&i.c?'"'+i.c+'"':this.charContent(o)};a.padding=this.padding(n,0,-n[2]),t["mjx-stretchy-h"+e+" mjx-"+r+" mjx-c::before"]=a}},e.prototype.addCharStyles=function(t,e,r,o){var n=o[3];if(!this.options.adaptiveCSS||n.used){var i=void 0!==n.f?n.f:e;t["mjx-c"+this.charSelector(r)+(i?".TEX-"+i:"")+"::before"]={padding:this.padding(o,0,n.ic||0),content:null!=n.c?'"'+n.c+'"':this.charContent(r)}}},e.prototype.getDelimiterData=function(t){return this.getChar("-smallop",t)},e.prototype.em=function(t){return u.em(t)},e.prototype.em0=function(t){return u.em(Math.max(0,t))},e.prototype.padding=function(t,e,r){var o=h(t,3),n=o[0],i=o[1];return void 0===e&&(e=0),void 0===r&&(r=0),[n,o[2]+r,i,e].map(this.em0).join(" ")},e.prototype.charContent=function(t){return'"'+(t>=32&&t<=126&&34!==t&&39!==t&&92!==t?String.fromCharCode(t):"\\"+t.toString(16).toUpperCase())+'"'},e.prototype.charSelector=function(t){return".mjx-c"+t.toString(16).toUpperCase()},e.OPTIONS=i(i({},c.FontData.OPTIONS),{fontURL:"js/output/chtml/fonts/tex-woff-v2"}),e.defaultVariantClasses={},e.defaultVariantLetters={},e.defaultStyles={"mjx-c::before":{display:"block",width:0}},e.defaultFonts={"@font-face /* 0 */":{"font-family":"MJXZERO",src:'url("%%URL%%/MathJax_Zero.woff") format("woff")'}},e}(c.FontData);e.CHTMLFontData=p,e.AddCSS=function(t,e){var r,o;try{for(var n=l(Object.keys(e)),i=n.next();!i.done;i=n.next()){var a=i.value,s=parseInt(a);Object.assign(c.FontData.charOptions(t,s),e[s])}}catch(t){r={error:t}}finally{try{i&&!i.done&&(o=n.return)&&o.call(n)}finally{if(r)throw r.error}}return t}},4458:function(t,e,r){var o=this&&this.__createBinding||(Object.create?function(t,e,r,o){void 0===o&&(o=r),Object.defineProperty(t,o,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,o){void 0===o&&(o=r),t[o]=e[r]}),n=this&&this.__exportStar||function(t,e){for(var r in t)"default"===r||Object.prototype.hasOwnProperty.call(e,r)||o(e,t,r)},i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.Arrow=e.DiagonalArrow=e.DiagonalStrike=e.Border2=e.Border=e.RenderElement=void 0;var a=r(5373);n(r(5373),e);e.RenderElement=function(t,e){return void 0===e&&(e=""),function(r,o){var n=r.adjustBorder(r.html("mjx-"+t));if(e){var i=r.getOffset(e);if(r.thickness!==a.THICKNESS||i){var s="translate"+e+"("+r.em(r.thickness/2-i)+")";r.adaptor.setStyle(n,"transform",s)}}r.adaptor.append(r.chtml,n)}};e.Border=function(t){return a.CommonBorder((function(e,r){e.adaptor.setStyle(r,"border-"+t,e.em(e.thickness)+" solid")}))(t)};e.Border2=function(t,e,r){return a.CommonBorder2((function(t,o){var n=t.em(t.thickness)+" solid";t.adaptor.setStyle(o,"border-"+e,n),t.adaptor.setStyle(o,"border-"+r,n)}))(t,e,r)};e.DiagonalStrike=function(t,e){return a.CommonDiagonalStrike((function(t){return function(r,o){var n=r.getBBox(),a=n.w,s=n.h,l=n.d,h=i(r.getArgMod(a,s+l),2),c=h[0],u=h[1],p=e*r.thickness/2,d=r.adjustBorder(r.html(t,{style:{width:r.em(u),transform:"rotate("+r.fixed(-e*c)+"rad) translateY("+p+"em)"}}));r.adaptor.append(r.chtml,d)}}))(t)};e.DiagonalArrow=function(t){return a.CommonDiagonalArrow((function(t,e){t.adaptor.append(t.chtml,e)}))(t)};e.Arrow=function(t){return a.CommonArrow((function(t,e){t.adaptor.append(t.chtml,e)}))(t)}},6617:function(t,e,r){var o,n,i=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),a=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},s=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLWrapper=e.SPACE=e.FONTSIZE=void 0;var l=r(6914),h=r(1541),c=r(3717);e.FONTSIZE={"70.7%":"s","70%":"s","50%":"ss","60%":"Tn","85%":"sm","120%":"lg","144%":"Lg","173%":"LG","207%":"hg","249%":"HG"},e.SPACE=((n={})[l.em(2/18)]="1",n[l.em(3/18)]="2",n[l.em(4/18)]="3",n[l.em(5/18)]="4",n[l.em(6/18)]="5",n);var u=function(t){function r(){var e=null!==t&&t.apply(this,arguments)||this;return e.chtml=null,e}return i(r,t),r.prototype.toCHTML=function(t){var e,r,o=this.standardCHTMLnode(t);try{for(var n=a(this.childNodes),i=n.next();!i.done;i=n.next()){i.value.toCHTML(o)}}catch(t){e={error:t}}finally{try{i&&!i.done&&(r=n.return)&&r.call(n)}finally{if(e)throw e.error}}},r.prototype.standardCHTMLnode=function(t){this.markUsed();var e=this.createCHTMLnode(t);return this.handleStyles(),this.handleVariant(),this.handleScale(),this.handleColor(),this.handleSpace(),this.handleAttributes(),this.handlePWidth(),e},r.prototype.markUsed=function(){this.constructor.used=!0},r.prototype.createCHTMLnode=function(t){var e=this.node.attributes.get("href");return e&&(t=this.adaptor.append(t,this.html("a",{href:e}))),this.chtml=this.adaptor.append(t,this.html("mjx-"+this.node.kind)),this.chtml},r.prototype.handleStyles=function(){if(this.styles){var t=this.styles.cssText;if(t){this.adaptor.setAttribute(this.chtml,"style",t);var e=this.styles.get("font-family");e&&this.adaptor.setStyle(this.chtml,"font-family","MJXZERO, "+e)}}},r.prototype.handleVariant=function(){this.node.isToken&&"-explicitFont"!==this.variant&&this.adaptor.setAttribute(this.chtml,"class",(this.font.getVariant(this.variant)||this.font.getVariant("normal")).classes)},r.prototype.handleScale=function(){this.setScale(this.chtml,this.bbox.rscale)},r.prototype.setScale=function(t,r){var o=Math.abs(r-1)<.001?1:r;if(t&&1!==o){var n=this.percent(o);e.FONTSIZE[n]?this.adaptor.setAttribute(t,"size",e.FONTSIZE[n]):this.adaptor.setStyle(t,"fontSize",n)}return t},r.prototype.handleSpace=function(){var t,r;try{for(var o=a([[this.bbox.L,"space","marginLeft"],[this.bbox.R,"rspace","marginRight"]]),n=o.next();!n.done;n=o.next()){var i=n.value,l=s(i,3),h=l[0],c=l[1],u=l[2];if(h){var p=this.em(h);e.SPACE[p]?this.adaptor.setAttribute(this.chtml,c,e.SPACE[p]):this.adaptor.setStyle(this.chtml,u,p)}}}catch(e){t={error:e}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(t)throw t.error}}},r.prototype.handleColor=function(){var t=this.node.attributes,e=t.getExplicit("mathcolor"),r=t.getExplicit("color"),o=t.getExplicit("mathbackground"),n=t.getExplicit("background");(e||r)&&this.adaptor.setStyle(this.chtml,"color",e||r),(o||n)&&this.adaptor.setStyle(this.chtml,"backgroundColor",o||n)},r.prototype.handleAttributes=function(){var t,e,o,n,i=this.node.attributes,s=i.getAllDefaults(),l=r.skipAttributes;try{for(var h=a(i.getExplicitNames()),c=h.next();!c.done;c=h.next()){var u=c.value;!1!==l[u]&&(u in s||l[u]||this.adaptor.hasAttribute(this.chtml,u))||this.adaptor.setAttribute(this.chtml,u,i.getExplicit(u))}}catch(e){t={error:e}}finally{try{c&&!c.done&&(e=h.return)&&e.call(h)}finally{if(t)throw t.error}}if(i.get("class")){var p=i.get("class").trim().split(/ +/);try{for(var d=a(p),f=d.next();!f.done;f=d.next()){var m=f.value;this.adaptor.addClass(this.chtml,m)}}catch(t){o={error:t}}finally{try{f&&!f.done&&(n=d.return)&&n.call(d)}finally{if(o)throw o.error}}}},r.prototype.handlePWidth=function(){this.bbox.pwidth&&(this.bbox.pwidth===c.BBox.fullWidth?this.adaptor.setAttribute(this.chtml,"width","full"):this.adaptor.setStyle(this.chtml,"width",this.bbox.pwidth))},r.prototype.setIndent=function(t,e,r){var o=this.adaptor;if("center"===e||"left"===e){var n=this.getBBox().L;o.setStyle(t,"margin-left",this.em(r+n))}if("center"===e||"right"===e){var i=this.getBBox().R;o.setStyle(t,"margin-right",this.em(-r+i))}},r.prototype.drawBBox=function(){var t=this.getBBox(),e=t.w,r=t.h,o=t.d,n=t.R,i=this.html("mjx-box",{style:{opacity:.25,"margin-left":this.em(-e-n)}},[this.html("mjx-box",{style:{height:this.em(r),width:this.em(e),"background-color":"red"}}),this.html("mjx-box",{style:{height:this.em(o),width:this.em(e),"margin-left":this.em(-e),"vertical-align":this.em(-o),"background-color":"green"}})]),a=this.chtml||this.parent.chtml,s=this.adaptor.getAttribute(a,"size");s&&this.adaptor.setAttribute(i,"size",s);var l=this.adaptor.getStyle(a,"fontSize");l&&this.adaptor.setStyle(i,"fontSize",l),this.adaptor.append(this.adaptor.parent(a),i),this.adaptor.setStyle(a,"backgroundColor","#FFEE00")},r.prototype.html=function(t,e,r){return void 0===e&&(e={}),void 0===r&&(r=[]),this.jax.html(t,e,r)},r.prototype.text=function(t){return this.jax.text(t)},r.prototype.char=function(t){return this.font.charSelector(t).substr(1)},r.kind="unknown",r.autoStyle=!0,r.used=!1,r}(h.CommonWrapper);e.CHTMLWrapper=u},4477:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLWrapperFactory=void 0;var i=r(1475),a=r(8369),s=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.defaultNodes=a.CHTMLWrappers,e}(i.CommonWrapperFactory);e.CHTMLWrapperFactory=s},8369:function(t,e,r){var o;Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLWrappers=void 0;var n=r(6617),i=r(4155),a=r(3271),s=r(3292),l=r(7013),h=r(9821),c=r(6359),u=r(6024),p=r(7215),d=r(3215),f=r(3126),m=r(7047),y=r(7837),v=r(5437),b=r(7111),x=r(513),g=r(6577),M=r(1096),_=r(6918),w=r(7500),C=r(8709),j=r(7918),S=r(1315),O=r(7795),T=r(518),L=r(1114);e.CHTMLWrappers=((o={})[i.CHTMLmath.kind]=i.CHTMLmath,o[f.CHTMLmrow.kind]=f.CHTMLmrow,o[f.CHTMLinferredMrow.kind]=f.CHTMLinferredMrow,o[a.CHTMLmi.kind]=a.CHTMLmi,o[s.CHTMLmo.kind]=s.CHTMLmo,o[l.CHTMLmn.kind]=l.CHTMLmn,o[h.CHTMLms.kind]=h.CHTMLms,o[c.CHTMLmtext.kind]=c.CHTMLmtext,o[u.CHTMLmspace.kind]=u.CHTMLmspace,o[p.CHTMLmpadded.kind]=p.CHTMLmpadded,o[d.CHTMLmenclose.kind]=d.CHTMLmenclose,o[y.CHTMLmfrac.kind]=y.CHTMLmfrac,o[v.CHTMLmsqrt.kind]=v.CHTMLmsqrt,o[b.CHTMLmroot.kind]=b.CHTMLmroot,o[x.CHTMLmsub.kind]=x.CHTMLmsub,o[x.CHTMLmsup.kind]=x.CHTMLmsup,o[x.CHTMLmsubsup.kind]=x.CHTMLmsubsup,o[g.CHTMLmunder.kind]=g.CHTMLmunder,o[g.CHTMLmover.kind]=g.CHTMLmover,o[g.CHTMLmunderover.kind]=g.CHTMLmunderover,o[M.CHTMLmmultiscripts.kind]=M.CHTMLmmultiscripts,o[m.CHTMLmfenced.kind]=m.CHTMLmfenced,o[_.CHTMLmtable.kind]=_.CHTMLmtable,o[w.CHTMLmtr.kind]=w.CHTMLmtr,o[w.CHTMLmlabeledtr.kind]=w.CHTMLmlabeledtr,o[C.CHTMLmtd.kind]=C.CHTMLmtd,o[j.CHTMLmaction.kind]=j.CHTMLmaction,o[S.CHTMLmglyph.kind]=S.CHTMLmglyph,o[O.CHTMLsemantics.kind]=O.CHTMLsemantics,o[O.CHTMLannotation.kind]=O.CHTMLannotation,o[O.CHTMLannotationXML.kind]=O.CHTMLannotationXML,o[O.CHTMLxml.kind]=O.CHTMLxml,o[T.CHTMLTeXAtom.kind]=T.CHTMLTeXAtom,o[L.CHTMLTextNode.kind]=L.CHTMLTextNode,o[n.CHTMLWrapper.kind]=n.CHTMLWrapper,o)},518:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLTeXAtom=void 0;var i=r(6617),a=r(3438),s=r(4282),l=r(8921),h=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(e){if(t.prototype.toCHTML.call(this,e),this.adaptor.setAttribute(this.chtml,"texclass",l.TEXCLASSNAMES[this.node.texClass]),this.node.texClass===l.TEXCLASS.VCENTER){var r=this.childNodes[0].getBBox(),o=r.h,n=(o+r.d)/2+this.font.params.axis_height-o;this.adaptor.setStyle(this.chtml,"verticalAlign",this.em(n))}},e.kind=s.TeXAtom.prototype.kind,e}(a.CommonTeXAtomMixin(i.CHTMLWrapper));e.CHTMLTeXAtom=h},1114:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLTextNode=void 0;var a=r(8921),s=r(6617),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e,r;this.markUsed();var o=this.adaptor,n=this.parent.variant,a=this.node.getText();if("-explicitFont"===n){var s=this.jax.getFontData(this.parent.styles);o.append(t,this.jax.unknownText(a,n,s))}else{var l=this.remappedText(a,n);try{for(var h=i(l),c=h.next();!c.done;c=h.next()){var u=c.value,p=this.getVariantChar(n,u)[3],d=(s=p.f?" TEX-"+p.f:"",p.unknown?this.jax.unknownText(String.fromCodePoint(u),n):this.html("mjx-c",{class:this.char(u)+s}));o.append(t,d),p.used=!0}}catch(t){e={error:t}}finally{try{c&&!c.done&&(r=h.return)&&r.call(h)}finally{if(e)throw e.error}}}},e.kind=a.TextNode.prototype.kind,e.autoStyle=!1,e.styles={"mjx-c":{display:"inline-block"},"mjx-utext":{display:"inline-block",padding:".75em 0 .2em 0"}},e}(r(555).CommonTextNodeMixin(s.CHTMLWrapper));e.CHTMLTextNode=l},7918:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmaction=void 0;var i=r(6617),a=r(3345),s=r(3345),l=r(3969),h=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e=this.standardCHTMLnode(t);this.selected.toCHTML(e),this.action(this,this.data)},e.prototype.setEventHandler=function(t,e){this.chtml.addEventListener(t,e)},e.kind=l.MmlMaction.prototype.kind,e.styles={"mjx-maction":{position:"relative"},"mjx-maction > mjx-tool":{display:"none",position:"absolute",bottom:0,right:0,width:0,height:0,"z-index":500},"mjx-tool > mjx-tip":{display:"inline-block",padding:".2em",border:"1px solid #888","font-size":"70%","background-color":"#F8F8F8",color:"black","box-shadow":"2px 2px 5px #AAAAAA"},"mjx-maction[toggle]":{cursor:"pointer"},"mjx-status":{display:"block",position:"fixed",left:"1em",bottom:"1em","min-width":"25%",padding:".2em .4em",border:"1px solid #888","font-size":"90%","background-color":"#F8F8F8",color:"black"}},e.actions=new Map([["toggle",[function(t,e){t.adaptor.setAttribute(t.chtml,"toggle",t.node.attributes.get("selection"));var r=t.factory.jax.math,o=t.factory.jax.document,n=t.node;t.setEventHandler("click",(function(t){r.end.node||(r.start.node=r.end.node=r.typesetRoot,r.start.n=r.end.n=0),n.nextToggleSelection(),r.rerender(o),t.stopPropagation()}))},{}]],["tooltip",[function(t,e){var r=t.childNodes[1];if(r)if(r.node.isKind("mtext")){var o=r.node.getText();t.adaptor.setAttribute(t.chtml,"title",o)}else{var n=t.adaptor,i=n.append(t.chtml,t.html("mjx-tool",{style:{bottom:t.em(-t.dy),right:t.em(-t.dx)}},[t.html("mjx-tip")]));r.toCHTML(n.firstChild(i)),t.setEventHandler("mouseover",(function(r){e.stopTimers(t,e);var o=setTimeout((function(){return n.setStyle(i,"display","block")}),e.postDelay);e.hoverTimer.set(t,o),r.stopPropagation()})),t.setEventHandler("mouseout",(function(r){e.stopTimers(t,e);var o=setTimeout((function(){return n.setStyle(i,"display","")}),e.clearDelay);e.clearTimer.set(t,o),r.stopPropagation()}))}},s.TooltipData]],["statusline",[function(t,e){var r=t.childNodes[1];if(r&&r.node.isKind("mtext")){var o=t.adaptor,n=r.node.getText();o.setAttribute(t.chtml,"statusline",n),t.setEventHandler("mouseover",(function(r){if(null===e.status){var i=o.body(o.document);e.status=o.append(i,t.html("mjx-status",{},[t.text(n)]))}r.stopPropagation()})),t.setEventHandler("mouseout",(function(t){e.status&&(o.remove(e.status),e.status=null),t.stopPropagation()}))}},{status:null}]]]),e}(a.CommonMactionMixin(i.CHTMLWrapper));e.CHTMLmaction=h},4155:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmath=void 0;var a=r(6617),s=r(2057),l=r(304),h=r(3717),c=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(e){t.prototype.toCHTML.call(this,e);var r=this.chtml,o=this.adaptor;"block"===this.node.attributes.get("display")?(o.setAttribute(r,"display","true"),o.setAttribute(e,"display","true"),this.handleDisplay(e)):this.handleInline(e),o.addClass(r,"MJX-TEX")},e.prototype.handleDisplay=function(t){var e=this.adaptor,r=i(this.getAlignShift(),2),o=r[0],n=r[1];if("center"!==o&&e.setAttribute(t,"justify",o),this.bbox.pwidth===h.BBox.fullWidth){if(e.setAttribute(t,"width","full"),this.jax.table){var a=this.jax.table.getBBox(),s=a.L,l=a.w,c=a.R;"right"===o?c=Math.max(c||-n,-n):"left"===o?s=Math.max(s||n,n):"center"===o&&(l+=2*Math.abs(n));var u=this.em(Math.max(0,s+l+c));e.setStyle(t,"min-width",u),e.setStyle(this.jax.table.chtml,"min-width",u)}}else this.setIndent(this.chtml,o,n)},e.prototype.handleInline=function(t){var e=this.adaptor,r=e.getStyle(this.chtml,"margin-right");r&&(e.setStyle(this.chtml,"margin-right",""),e.setStyle(t,"margin-right",r),e.setStyle(t,"width","0"))},e.prototype.setChildPWidths=function(e,r,o){return void 0===r&&(r=null),void 0===o&&(o=!0),!!this.parent&&t.prototype.setChildPWidths.call(this,e,r,o)},e.kind=l.MmlMath.prototype.kind,e.styles={"mjx-math":{"line-height":0,"text-align":"left","text-indent":0,"font-style":"normal","font-weight":"normal","font-size":"100%","font-size-adjust":"none","letter-spacing":"normal","word-wrap":"normal","word-spacing":"normal","white-space":"nowrap",direction:"ltr",padding:"1px 0"},'mjx-container[jax="CHTML"][display="true"]':{display:"block","text-align":"center",margin:"1em 0"},'mjx-container[jax="CHTML"][display="true"][width="full"]':{display:"flex"},'mjx-container[jax="CHTML"][display="true"] mjx-math':{padding:0},'mjx-container[jax="CHTML"][justify="left"]':{"text-align":"left"},'mjx-container[jax="CHTML"][justify="right"]':{"text-align":"right"}},e}(s.CommonMathMixin(a.CHTMLWrapper));e.CHTMLmath=c},3215:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},a=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmenclose=void 0;var s=r(6617),l=r(6200),h=r(4458),c=r(4374),u=r(6914);function p(t,e){return Math.atan2(t,e).toFixed(3).replace(/\.?0+$/,"")}var d=p(h.ARROWDX,h.ARROWY),f=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e,r,o,n,a=this.adaptor,s=this.standardCHTMLnode(t),l=a.append(s,this.html("mjx-box"));this.renderChild?this.renderChild(this,l):this.childNodes[0].toCHTML(l);try{for(var c=i(Object.keys(this.notations)),u=c.next();!u.done;u=c.next()){var p=u.value,d=this.notations[p];!d.renderChild&&d.renderer(this,l)}}catch(t){e={error:t}}finally{try{u&&!u.done&&(r=c.return)&&r.call(c)}finally{if(e)throw e.error}}var f=this.getPadding();try{for(var m=i(h.sideNames),y=m.next();!y.done;y=m.next()){var v=y.value,b=h.sideIndex[v];f[b]>0&&a.setStyle(l,"padding-"+v,this.em(f[b]))}}catch(t){o={error:t}}finally{try{y&&!y.done&&(n=m.return)&&n.call(m)}finally{if(o)throw o.error}}},e.prototype.arrow=function(t,e,r,o,n){void 0===o&&(o=""),void 0===n&&(n=0);var i=this.getBBox().w,a={width:this.em(t)};i!==t&&(a.left=this.em((i-t)/2)),e&&(a.transform="rotate("+this.fixed(e)+"rad)");var s=this.html("mjx-arrow",{style:a},[this.html("mjx-aline"),this.html("mjx-rthead"),this.html("mjx-rbhead")]);return r&&(this.adaptor.append(s,this.html("mjx-lthead")),this.adaptor.append(s,this.html("mjx-lbhead")),this.adaptor.setAttribute(s,"double","true")),this.adjustArrow(s,r),this.moveArrow(s,o,n),s},e.prototype.adjustArrow=function(t,e){var r=this,o=this.thickness,n=this.arrowhead;if(n.x!==h.ARROWX||n.y!==h.ARROWY||n.dx!==h.ARROWDX||o!==h.THICKNESS){var i=a([o*n.x,o*n.y].map((function(t){return r.em(t)})),2),s=i[0],l=i[1],c=p(n.dx,n.y),u=a(this.adaptor.childNodes(t),5),d=u[0],f=u[1],m=u[2],y=u[3],v=u[4];this.adjustHead(f,[l,"0","1px",s],c),this.adjustHead(m,["1px","0",l,s],"-"+c),this.adjustHead(y,[l,s,"1px","0"],"-"+c),this.adjustHead(v,["1px",s,l,"0"],c),this.adjustLine(d,o,n.x,e)}},e.prototype.adjustHead=function(t,e,r){t&&(this.adaptor.setStyle(t,"border-width",e.join(" ")),this.adaptor.setStyle(t,"transform","skewX("+r+"rad)"))},e.prototype.adjustLine=function(t,e,r,o){this.adaptor.setStyle(t,"borderTop",this.em(e)+" solid"),this.adaptor.setStyle(t,"top",this.em(-e/2)),this.adaptor.setStyle(t,"right",this.em(e*(r-1))),o&&this.adaptor.setStyle(t,"left",this.em(e*(r-1)))},e.prototype.moveArrow=function(t,e,r){if(r){var o=this.adaptor.getStyle(t,"transform");this.adaptor.setStyle(t,"transform","translate"+e+"("+this.em(-r)+")"+(o?" "+o:""))}},e.prototype.adjustBorder=function(t){return this.thickness!==h.THICKNESS&&this.adaptor.setStyle(t,"borderWidth",this.em(this.thickness)),t},e.prototype.adjustThickness=function(t){return this.thickness!==h.THICKNESS&&this.adaptor.setStyle(t,"strokeWidth",this.fixed(this.thickness)),t},e.prototype.fixed=function(t,e){return void 0===e&&(e=3),Math.abs(t)<6e-4?"0":t.toFixed(e).replace(/\.?0+$/,"")},e.prototype.em=function(e){return t.prototype.em.call(this,e)},e.kind=c.MmlMenclose.prototype.kind,e.styles={"mjx-menclose":{position:"relative"},"mjx-menclose > mjx-dstrike":{display:"inline-block",left:0,top:0,position:"absolute","border-top":h.SOLID,"transform-origin":"top left"},"mjx-menclose > mjx-ustrike":{display:"inline-block",left:0,bottom:0,position:"absolute","border-top":h.SOLID,"transform-origin":"bottom left"},"mjx-menclose > mjx-hstrike":{"border-top":h.SOLID,position:"absolute",left:0,right:0,bottom:"50%",transform:"translateY("+u.em(h.THICKNESS/2)+")"},"mjx-menclose > mjx-vstrike":{"border-left":h.SOLID,position:"absolute",top:0,bottom:0,right:"50%",transform:"translateX("+u.em(h.THICKNESS/2)+")"},"mjx-menclose > mjx-rbox":{position:"absolute",top:0,bottom:0,right:0,left:0,border:h.SOLID,"border-radius":u.em(h.THICKNESS+h.PADDING)},"mjx-menclose > mjx-cbox":{position:"absolute",top:0,bottom:0,right:0,left:0,border:h.SOLID,"border-radius":"50%"},"mjx-menclose > mjx-arrow":{position:"absolute",left:0,bottom:"50%",height:0,width:0},"mjx-menclose > mjx-arrow > *":{display:"block",position:"absolute","transform-origin":"bottom","border-left":u.em(h.THICKNESS*h.ARROWX)+" solid","border-right":0,"box-sizing":"border-box"},"mjx-menclose > mjx-arrow > mjx-aline":{left:0,top:u.em(-h.THICKNESS/2),right:u.em(h.THICKNESS*(h.ARROWX-1)),height:0,"border-top":u.em(h.THICKNESS)+" solid","border-left":0},"mjx-menclose > mjx-arrow[double] > mjx-aline":{left:u.em(h.THICKNESS*(h.ARROWX-1)),height:0},"mjx-menclose > mjx-arrow > mjx-rthead":{transform:"skewX("+d+"rad)",right:0,bottom:"-1px","border-bottom":"1px solid transparent","border-top":u.em(h.THICKNESS*h.ARROWY)+" solid transparent"},"mjx-menclose > mjx-arrow > mjx-rbhead":{transform:"skewX(-"+d+"rad)","transform-origin":"top",right:0,top:"-1px","border-top":"1px solid transparent","border-bottom":u.em(h.THICKNESS*h.ARROWY)+" solid transparent"},"mjx-menclose > mjx-arrow > mjx-lthead":{transform:"skewX(-"+d+"rad)",left:0,bottom:"-1px","border-left":0,"border-right":u.em(h.THICKNESS*h.ARROWX)+" solid","border-bottom":"1px solid transparent","border-top":u.em(h.THICKNESS*h.ARROWY)+" solid transparent"},"mjx-menclose > mjx-arrow > mjx-lbhead":{transform:"skewX("+d+"rad)","transform-origin":"top",left:0,top:"-1px","border-left":0,"border-right":u.em(h.THICKNESS*h.ARROWX)+" solid","border-top":"1px solid transparent","border-bottom":u.em(h.THICKNESS*h.ARROWY)+" solid transparent"},"mjx-menclose > dbox":{position:"absolute",top:0,bottom:0,left:u.em(-1.5*h.PADDING),width:u.em(3*h.PADDING),border:u.em(h.THICKNESS)+" solid","border-radius":"50%","clip-path":"inset(0 0 0 "+u.em(1.5*h.PADDING)+")","box-sizing":"border-box"}},e.notations=new Map([h.Border("top"),h.Border("right"),h.Border("bottom"),h.Border("left"),h.Border2("actuarial","top","right"),h.Border2("madruwb","bottom","right"),h.DiagonalStrike("up",1),h.DiagonalStrike("down",-1),["horizontalstrike",{renderer:h.RenderElement("hstrike","Y"),bbox:function(t){return[0,t.padding,0,t.padding]}}],["verticalstrike",{renderer:h.RenderElement("vstrike","X"),bbox:function(t){return[t.padding,0,t.padding,0]}}],["box",{renderer:function(t,e){t.adaptor.setStyle(e,"border",t.em(t.thickness)+" solid")},bbox:h.fullBBox,border:h.fullBorder,remove:"left right top bottom"}],["roundedbox",{renderer:h.RenderElement("rbox"),bbox:h.fullBBox}],["circle",{renderer:h.RenderElement("cbox"),bbox:h.fullBBox}],["phasorangle",{renderer:function(t,e){var r=t.getBBox(),o=r.h,n=r.d,i=a(t.getArgMod(1.75*t.padding,o+n),2),s=i[0],l=i[1],h=t.thickness*Math.sin(s)*.9;t.adaptor.setStyle(e,"border-bottom",t.em(t.thickness)+" solid");var c=t.adjustBorder(t.html("mjx-ustrike",{style:{width:t.em(l),transform:"translateX("+t.em(h)+") rotate("+t.fixed(-s)+"rad)"}}));t.adaptor.append(t.chtml,c)},bbox:function(t){var e=t.padding/2,r=t.thickness;return[2*e,e,e+r,3*e+r]},border:function(t){return[0,0,t.thickness,0]},remove:"bottom"}],h.Arrow("up"),h.Arrow("down"),h.Arrow("left"),h.Arrow("right"),h.Arrow("updown"),h.Arrow("leftright"),h.DiagonalArrow("updiagonal"),h.DiagonalArrow("northeast"),h.DiagonalArrow("southeast"),h.DiagonalArrow("northwest"),h.DiagonalArrow("southwest"),h.DiagonalArrow("northeastsouthwest"),h.DiagonalArrow("northwestsoutheast"),["longdiv",{renderer:function(t,e){var r=t.adaptor;r.setStyle(e,"border-top",t.em(t.thickness)+" solid");var o=r.append(t.chtml,t.html("dbox")),n=t.thickness,i=t.padding;n!==h.THICKNESS&&r.setStyle(o,"border-width",t.em(n)),i!==h.PADDING&&(r.setStyle(o,"left",t.em(-1.5*i)),r.setStyle(o,"width",t.em(3*i)),r.setStyle(o,"clip-path","inset(0 0 0 "+t.em(1.5*i)+")"))},bbox:function(t){var e=t.padding,r=t.thickness;return[e+r,e,e,2*e+r/2]}}],["radical",{renderer:function(t,e){t.msqrt.toCHTML(e);var r=t.sqrtTRBL();t.adaptor.setStyle(t.msqrt.chtml,"margin",r.map((function(e){return t.em(-e)})).join(" "))},init:function(t){t.msqrt=t.createMsqrt(t.childNodes[0])},bbox:function(t){return t.sqrtTRBL()},renderChild:!0}]]),e}(l.CommonMencloseMixin(s.CHTMLWrapper));e.CHTMLmenclose=f},7047:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmfenced=void 0;var i=r(6617),a=r(1346),s=r(7451),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e=this.standardCHTMLnode(t);this.mrow.toCHTML(e)},e.kind=s.MmlMfenced.prototype.kind,e}(a.CommonMfencedMixin(i.CHTMLWrapper));e.CHTMLmfenced=l},7837:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,o=arguments.length;r *":{"font-size":"2000%"},"mjx-dbox":{display:"block","font-size":"5%"},"mjx-num":{display:"block","text-align":"center"},"mjx-den":{display:"block","text-align":"center"},"mjx-mfrac[bevelled] > mjx-num":{display:"inline-block"},"mjx-mfrac[bevelled] > mjx-den":{display:"inline-block"},'mjx-den[align="right"], mjx-num[align="right"]':{"text-align":"right"},'mjx-den[align="left"], mjx-num[align="left"]':{"text-align":"left"},"mjx-nstrut":{display:"inline-block",height:".054em",width:0,"vertical-align":"-.054em"},'mjx-nstrut[type="d"]':{height:".217em","vertical-align":"-.217em"},"mjx-dstrut":{display:"inline-block",height:".505em",width:0},'mjx-dstrut[type="d"]':{height:".726em"},"mjx-line":{display:"block","box-sizing":"border-box","min-height":"1px",height:".06em","border-top":".06em solid",margin:".06em -.1em",overflow:"hidden"},'mjx-line[type="d"]':{margin:".18em -.1em"}},e}(s.CommonMfracMixin(a.CHTMLWrapper));e.CHTMLmfrac=h},1315:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmglyph=void 0;var i=r(6617),a=r(7969),s=r(910),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e=this.standardCHTMLnode(t),r=this.node.attributes.getList("src","alt"),o=r.src,n=r.alt,i={width:this.em(this.width),height:this.em(this.height)};this.valign&&(i.verticalAlign=this.em(this.valign));var a=this.html("img",{src:o,style:i,alt:n,title:n});this.adaptor.append(e,a)},e.kind=s.MmlMglyph.prototype.kind,e.styles={"mjx-mglyph > img":{display:"inline-block",border:0,padding:0}},e}(a.CommonMglyphMixin(i.CHTMLWrapper));e.CHTMLmglyph=l},3271:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmi=void 0;var i=r(6617),a=r(1419),s=r(7754),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.kind=s.MmlMi.prototype.kind,e}(a.CommonMiMixin(i.CHTMLWrapper));e.CHTMLmi=l},1096:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmmultiscripts=void 0;var a=r(513),s=r(9906),l=r(7764),h=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e=this.standardCHTMLnode(t),r=this.scriptData,o=this.combinePrePost(r.sub,r.psub),n=this.combinePrePost(r.sup,r.psup),a=i(this.getUVQ(o,n),2),s=a[0],l=a[1];r.numPrescripts&&this.addScripts(s,-l,!0,r.psub,r.psup,this.firstPrescript,r.numPrescripts),this.childNodes[0].toCHTML(e),r.numScripts&&this.addScripts(s,-l,!1,r.sub,r.sup,1,r.numScripts)},e.prototype.addScripts=function(t,e,r,o,n,i,a){var s=this.adaptor,l=t-n.d+(e-o.h),h=t<0&&0===e?o.h+t:t,c=l>0?{style:{height:this.em(l)}}:{},u=h?{style:{"vertical-align":this.em(h)}}:{},p=this.html("mjx-row"),d=this.html("mjx-row",c),f=this.html("mjx-row"),m="mjx-"+(r?"pre":"")+"scripts";s.append(this.chtml,this.html(m,u,[p,d,f]));for(var y=i+2*a;i mjx-row > mjx-cell":{"text-align":"right"}},e}(s.CommonMmultiscriptsMixin(a.CHTMLmsubsup));e.CHTMLmmultiscripts=h},7013:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmn=void 0;var i=r(6617),a=r(2304),s=r(3235),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.kind=s.MmlMn.prototype.kind,e}(a.CommonMnMixin(i.CHTMLWrapper));e.CHTMLmn=l},3292:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmo=void 0;var a=r(6617),s=r(437),l=r(9946),h=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e,r,o=this.node.attributes,n=o.get("symmetric")&&2!==this.stretch.dir,a=0!==this.stretch.dir;a&&null===this.size&&this.getStretchedVariant([]);var s=this.standardCHTMLnode(t);if(a&&this.size<0)this.stretchHTML(s);else{if(n||o.get("largeop")){var l=this.em(this.getCenterOffset());"0"!==l&&this.adaptor.setStyle(s,"verticalAlign",l)}this.node.getProperty("mathaccent")&&(this.adaptor.setStyle(s,"width","0"),this.adaptor.setStyle(s,"margin-left",this.em(this.getAccentOffset())));try{for(var h=i(this.childNodes),c=h.next();!c.done;c=h.next()){c.value.toCHTML(s)}}catch(t){e={error:t}}finally{try{c&&!c.done&&(r=h.return)&&r.call(h)}finally{if(e)throw e.error}}}},e.prototype.stretchHTML=function(t){var e=this.getText().codePointAt(0),r=this.stretch;r.used=!0;var o=r.stretch,n=[];o[0]&&n.push(this.html("mjx-beg",{},[this.html("mjx-c")])),n.push(this.html("mjx-ext",{},[this.html("mjx-c")])),4===o.length&&n.push(this.html("mjx-mid",{},[this.html("mjx-c")]),this.html("mjx-ext",{},[this.html("mjx-c")])),o[2]&&n.push(this.html("mjx-end",{},[this.html("mjx-c")]));var i={},a=this.bbox,l=a.h,h=a.d,c=a.w;1===r.dir?(n.push(this.html("mjx-mark")),i.height=this.em(l+h),i.verticalAlign=this.em(-h)):i.width=this.em(c);var u=s.DirectionVH[r.dir],p={class:this.char(r.c||e),style:i},d=this.html("mjx-stretchy-"+u,p,n);this.adaptor.append(t,d)},e.kind=l.MmlMo.prototype.kind,e.styles={"mjx-stretchy-h":{display:"inline-table",width:"100%"},"mjx-stretchy-h > *":{display:"table-cell",width:0},"mjx-stretchy-h > * > mjx-c":{display:"inline-block",transform:"scalex(1.0000001)"},"mjx-stretchy-h > * > mjx-c::before":{display:"inline-block",width:"initial"},"mjx-stretchy-h > mjx-ext":{overflow:"hidden",width:"100%"},"mjx-stretchy-h > mjx-ext > mjx-c::before":{transform:"scalex(500)"},"mjx-stretchy-h > mjx-ext > mjx-c":{width:0},"mjx-stretchy-h > mjx-beg > mjx-c":{"margin-right":"-.1em"},"mjx-stretchy-h > mjx-end > mjx-c":{"margin-left":"-.1em"},"mjx-stretchy-v":{display:"inline-block"},"mjx-stretchy-v > *":{display:"block"},"mjx-stretchy-v > mjx-beg":{height:0},"mjx-stretchy-v > mjx-end > mjx-c":{display:"block"},"mjx-stretchy-v > * > mjx-c":{transform:"scaley(1.0000001)","transform-origin":"left center",overflow:"hidden"},"mjx-stretchy-v > mjx-ext":{display:"block",height:"100%","box-sizing":"border-box",border:"0px solid transparent",overflow:"hidden"},"mjx-stretchy-v > mjx-ext > mjx-c::before":{width:"initial","box-sizing":"border-box"},"mjx-stretchy-v > mjx-ext > mjx-c":{transform:"scaleY(500) translateY(.075em)",overflow:"visible"},"mjx-mark":{display:"inline-block",height:"0px"}},e}(s.CommonMoMixin(a.CHTMLWrapper));e.CHTMLmo=h},7215:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},a=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmpadded=void 0;var s=r(6617),l=r(7481),h=r(189),c=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e,r,o=this.standardCHTMLnode(t),n=[],s={},l=i(this.getDimens(),9),h=l[2],c=l[3],u=l[4],p=l[5],d=l[6],f=l[7],m=l[8];if(p&&(s.width=this.em(h+p)),(c||u)&&(s.margin=this.em(c)+" 0 "+this.em(u)),d+m||f){s.position="relative";var y=this.html("mjx-rbox",{style:{left:this.em(d+m),top:this.em(-f)}});d+m&&this.childNodes[0].getBBox().pwidth&&(this.adaptor.setAttribute(y,"width","full"),this.adaptor.setStyle(y,"left",this.em(d))),n.push(y)}o=this.adaptor.append(o,this.html("mjx-block",{style:s},n));try{for(var v=a(this.childNodes),b=v.next();!b.done;b=v.next()){b.value.toCHTML(n[0]||o)}}catch(t){e={error:t}}finally{try{b&&!b.done&&(r=v.return)&&r.call(v)}finally{if(e)throw e.error}}},e.kind=h.MmlMpadded.prototype.kind,e.styles={"mjx-mpadded":{display:"inline-block"},"mjx-rbox":{display:"inline-block",position:"relative"}},e}(l.CommonMpaddedMixin(s.CHTMLWrapper));e.CHTMLmpadded=c},7111:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmroot=void 0;var a=r(5437),s=r(5997),l=r(4664),h=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.addRoot=function(t,e,r,o){e.toCHTML(t);var n=i(this.getRootDimens(r,o),3),a=n[0],s=n[1],l=n[2];this.adaptor.setStyle(t,"verticalAlign",this.em(s)),this.adaptor.setStyle(t,"width",this.em(a)),l&&this.adaptor.setStyle(this.adaptor.firstChild(t),"paddingLeft",this.em(l))},e.kind=l.MmlMroot.prototype.kind,e}(s.CommonMrootMixin(a.CHTMLmsqrt));e.CHTMLmroot=h},3126:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLinferredMrow=e.CHTMLmrow=void 0;var a=r(6617),s=r(9323),l=r(9323),h=r(1691),c=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e,r,o=this.node.isInferred?this.chtml=t:this.standardCHTMLnode(t),n=!1;try{for(var a=i(this.childNodes),s=a.next();!s.done;s=a.next()){var l=s.value;l.toCHTML(o),l.bbox.w<0&&(n=!0)}}catch(t){e={error:t}}finally{try{s&&!s.done&&(r=a.return)&&r.call(a)}finally{if(e)throw e.error}}if(n){var h=this.getBBox().w;h&&(this.adaptor.setStyle(o,"width",this.em(Math.max(0,h))),h<0&&this.adaptor.setStyle(o,"marginRight",this.em(h)))}},e.kind=h.MmlMrow.prototype.kind,e}(s.CommonMrowMixin(a.CHTMLWrapper));e.CHTMLmrow=c;var u=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.kind=h.MmlInferredMrow.prototype.kind,e}(l.CommonInferredMrowMixin(c));e.CHTMLinferredMrow=u},9821:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLms=void 0;var i=r(6617),a=r(6920),s=r(4042),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.kind=s.MmlMs.prototype.kind,e}(a.CommonMsMixin(i.CHTMLWrapper));e.CHTMLms=l},6024:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmspace=void 0;var i=r(6617),a=r(37),s=r(1465),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e=this.standardCHTMLnode(t),r=this.getBBox(),o=r.w,n=r.h,i=r.d;o<0&&(this.adaptor.setStyle(e,"marginRight",this.em(o)),o=0),o&&this.adaptor.setStyle(e,"width",this.em(o)),(n=Math.max(0,n+i))&&this.adaptor.setStyle(e,"height",this.em(Math.max(0,n))),i&&this.adaptor.setStyle(e,"verticalAlign",this.em(-i))},e.kind=s.MmlMspace.prototype.kind,e}(a.CommonMspaceMixin(i.CHTMLWrapper));e.CHTMLmspace=l},5437:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmsqrt=void 0;var a=r(6617),s=r(222),l=r(4655),h=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e,r,o,n,a=this.childNodes[this.surd],s=this.childNodes[this.base],l=a.getBBox(),h=s.getBBox(),c=i(this.getPQ(l),2)[1],u=this.font.params.rule_thickness,p=h.h+c+u,d=this.standardCHTMLnode(t);null!=this.root&&(o=this.adaptor.append(d,this.html("mjx-root")),n=this.childNodes[this.root]);var f=this.adaptor.append(d,this.html("mjx-sqrt",{},[e=this.html("mjx-surd"),r=this.html("mjx-box",{style:{paddingTop:this.em(c)}})]));this.addRoot(o,n,l,p),a.toCHTML(e),s.toCHTML(r),a.size<0&&this.adaptor.addClass(f,"mjx-tall")},e.prototype.addRoot=function(t,e,r,o){},e.kind=l.MmlMsqrt.prototype.kind,e.styles={"mjx-root":{display:"inline-block","white-space":"nowrap"},"mjx-surd":{display:"inline-block","vertical-align":"top"},"mjx-sqrt":{display:"inline-block","padding-top":".07em"},"mjx-sqrt > mjx-box":{"border-top":".07em solid"},"mjx-sqrt.mjx-tall > mjx-box":{"padding-left":".3em","margin-left":"-.3em"}},e}(s.CommonMsqrtMixin(a.CHTMLWrapper));e.CHTMLmsqrt=h},513:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmsubsup=e.CHTMLmsup=e.CHTMLmsub=void 0;var a=r(7322),s=r(3069),l=r(3069),h=r(3069),c=r(5857),u=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.kind=c.MmlMsub.prototype.kind,e}(s.CommonMsubMixin(a.CHTMLscriptbase));e.CHTMLmsub=u;var p=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.kind=c.MmlMsup.prototype.kind,e}(l.CommonMsupMixin(a.CHTMLscriptbase));e.CHTMLmsup=p;var d=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.markUsed=function(){t.prototype.markUsed.call(this),e.used=!0},e.prototype.toCHTML=function(t){var e=this.adaptor,r=this.standardCHTMLnode(t),o=i([this.baseChild,this.supChild,this.subChild],3),n=o[0],a=o[1],s=o[2],l=i(this.getUVQ(),3),h=l[1],c=l[2],u={"vertical-align":this.em(h)};n.toCHTML(r);var p=e.append(r,this.html("mjx-script",{style:u}));a.toCHTML(p),e.append(p,this.html("mjx-spacer",{style:{"margin-top":this.em(c)}})),s.toCHTML(p);var d=this.getAdjustedIc();d&&e.setStyle(a.chtml,"marginLeft",this.em(d/a.bbox.rscale)),this.baseRemoveIc&&e.setStyle(p,"marginLeft",this.em(-this.baseIc))},e.kind=c.MmlMsubsup.prototype.kind,e.styles={"mjx-script":{display:"inline-block","padding-right":".05em","padding-left":".033em"},"mjx-script > *":{display:"block"}},e}(h.CommonMsubsupMixin(a.CHTMLscriptbase));e.CHTMLmsubsup=d},6918:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},a=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmtable=void 0;var s=r(6617),l=r(8589),h=r(4859),c=r(6720),u=function(t){function e(e,r,o){void 0===o&&(o=null);var n=t.call(this,e,r,o)||this;return n.itable=n.html("mjx-itable"),n.labels=n.html("mjx-itable"),n}return n(e,t),e.prototype.getAlignShift=function(){var e=t.prototype.getAlignShift.call(this);return this.isTop||(e[1]=0),e},e.prototype.toCHTML=function(t){var e,r,o=this.standardCHTMLnode(t);this.adaptor.append(o,this.html("mjx-table",{},[this.itable]));try{for(var n=i(this.childNodes),a=n.next();!a.done;a=n.next()){a.value.toCHTML(this.itable)}}catch(t){e={error:t}}finally{try{a&&!a.done&&(r=n.return)&&r.call(n)}finally{if(e)throw e.error}}this.padRows(),this.handleColumnSpacing(),this.handleColumnLines(),this.handleColumnWidths(),this.handleRowSpacing(),this.handleRowLines(),this.handleRowHeights(),this.handleFrame(),this.handleWidth(),this.handleLabels(),this.handleAlign(),this.handleJustify(),this.shiftColor()},e.prototype.shiftColor=function(){var t=this.adaptor,e=t.getStyle(this.chtml,"backgroundColor");e&&(t.setStyle(this.chtml,"backgroundColor",""),t.setStyle(this.itable,"backgroundColor",e))},e.prototype.padRows=function(){var t,e,r=this.adaptor;try{for(var o=i(r.childNodes(this.itable)),n=o.next();!n.done;n=o.next())for(var a=n.value;r.childNodes(a).length1&&"0.4em"!==m||s&&1===u)&&this.adaptor.setStyle(v,"paddingLeft",m),(u1&&"0.215em"!==p||s&&1===l)&&this.adaptor.setStyle(y.chtml,"paddingTop",p),(l mjx-itable":{"vertical-align":"middle","text-align":"left","box-sizing":"border-box"},"mjx-labels > mjx-itable":{position:"absolute",top:0},'mjx-mtable[justify="left"]':{"text-align":"left"},'mjx-mtable[justify="right"]':{"text-align":"right"},'mjx-mtable[justify="left"][side="left"]':{"padding-right":"0 ! important"},'mjx-mtable[justify="left"][side="right"]':{"padding-left":"0 ! important"},'mjx-mtable[justify="right"][side="left"]':{"padding-right":"0 ! important"},'mjx-mtable[justify="right"][side="right"]':{"padding-left":"0 ! important"},"mjx-mtable[align]":{"vertical-align":"baseline"},'mjx-mtable[align="top"] > mjx-table':{"vertical-align":"top"},'mjx-mtable[align="bottom"] > mjx-table':{"vertical-align":"bottom"},'mjx-mtable[side="right"] mjx-labels':{"min-width":"100%"}},e}(l.CommonMtableMixin(s.CHTMLWrapper));e.CHTMLmtable=u},8709:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmtd=void 0;var i=r(6617),a=r(7805),s=r(2321),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(e){t.prototype.toCHTML.call(this,e);var r=this.node.attributes.get("rowalign"),o=this.node.attributes.get("columnalign");r!==this.parent.node.attributes.get("rowalign")&&this.adaptor.setAttribute(this.chtml,"rowalign",r),"center"===o||"mlabeledtr"===this.parent.kind&&this===this.parent.childNodes[0]&&o===this.parent.parent.node.attributes.get("side")||this.adaptor.setStyle(this.chtml,"textAlign",o),this.parent.parent.node.getProperty("useHeight")&&this.adaptor.append(this.chtml,this.html("mjx-tstrut"))},e.kind=s.MmlMtd.prototype.kind,e.styles={"mjx-mtd":{display:"table-cell","text-align":"center",padding:".215em .4em"},"mjx-mtd:first-child":{"padding-left":0},"mjx-mtd:last-child":{"padding-right":0},"mjx-mtable > * > mjx-itable > *:first-child > mjx-mtd":{"padding-top":0},"mjx-mtable > * > mjx-itable > *:last-child > mjx-mtd":{"padding-bottom":0},"mjx-tstrut":{display:"inline-block",height:"1em","vertical-align":"-.25em"},'mjx-labels[align="left"] > mjx-mtr > mjx-mtd':{"text-align":"left"},'mjx-labels[align="right"] > mjx-mtr > mjx-mtd':{"text-align":"right"},'mjx-mtr mjx-mtd[rowalign="top"], mjx-mlabeledtr mjx-mtd[rowalign="top"]':{"vertical-align":"top"},'mjx-mtr mjx-mtd[rowalign="center"], mjx-mlabeledtr mjx-mtd[rowalign="center"]':{"vertical-align":"middle"},'mjx-mtr mjx-mtd[rowalign="bottom"], mjx-mlabeledtr mjx-mtd[rowalign="bottom"]':{"vertical-align":"bottom"},'mjx-mtr mjx-mtd[rowalign="baseline"], mjx-mlabeledtr mjx-mtd[rowalign="baseline"]':{"vertical-align":"baseline"},'mjx-mtr mjx-mtd[rowalign="axis"], mjx-mlabeledtr mjx-mtd[rowalign="axis"]':{"vertical-align":".25em"}},e}(a.CommonMtdMixin(i.CHTMLWrapper));e.CHTMLmtd=l},6359:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmtext=void 0;var i=r(6617),a=r(8325),s=r(6277),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.kind=s.MmlMtext.prototype.kind,e}(a.CommonMtextMixin(i.CHTMLWrapper));e.CHTMLmtext=l},7500:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmlabeledtr=e.CHTMLmtr=void 0;var i=r(6617),a=r(4818),s=r(4818),l=r(4393),h=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(e){t.prototype.toCHTML.call(this,e);var r=this.node.attributes.get("rowalign");"baseline"!==r&&this.adaptor.setAttribute(this.chtml,"rowalign",r)},e.kind=l.MmlMtr.prototype.kind,e.styles={"mjx-mtr":{display:"table-row"},'mjx-mtr[rowalign="top"] > mjx-mtd':{"vertical-align":"top"},'mjx-mtr[rowalign="center"] > mjx-mtd':{"vertical-align":"middle"},'mjx-mtr[rowalign="bottom"] > mjx-mtd':{"vertical-align":"bottom"},'mjx-mtr[rowalign="baseline"] > mjx-mtd':{"vertical-align":"baseline"},'mjx-mtr[rowalign="axis"] > mjx-mtd':{"vertical-align":".25em"}},e}(a.CommonMtrMixin(i.CHTMLWrapper));e.CHTMLmtr=h;var c=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(e){t.prototype.toCHTML.call(this,e);var r=this.adaptor.firstChild(this.chtml);if(r){this.adaptor.remove(r);var o=this.node.attributes.get("rowalign"),n="baseline"!==o&&"axis"!==o?{rowalign:o}:{},i=this.html("mjx-mtr",n,[r]);h.used=!0,this.adaptor.append(this.parent.labels,i)}},e.kind=l.MmlMlabeledtr.prototype.kind,e.styles={"mjx-mlabeledtr":{display:"table-row"},'mjx-mlabeledtr[rowalign="top"] > mjx-mtd':{"vertical-align":"top"},'mjx-mlabeledtr[rowalign="center"] > mjx-mtd':{"vertical-align":"middle"},'mjx-mlabeledtr[rowalign="bottom"] > mjx-mtd':{"vertical-align":"bottom"},'mjx-mlabeledtr[rowalign="baseline"] > mjx-mtd':{"vertical-align":"baseline"},'mjx-mlabeledtr[rowalign="axis"] > mjx-mtd':{"vertical-align":".25em"}},e}(s.CommonMlabeledtrMixin(h));e.CHTMLmlabeledtr=c},6577:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLmunderover=e.CHTMLmover=e.CHTMLmunder=void 0;var i=r(513),a=r(9690),s=r(9690),l=r(9690),h=r(3102),c=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(e){if(this.hasMovableLimits())return t.prototype.toCHTML.call(this,e),void this.adaptor.setAttribute(this.chtml,"limits","false");this.chtml=this.standardCHTMLnode(e);var r=this.adaptor.append(this.adaptor.append(this.chtml,this.html("mjx-row")),this.html("mjx-base")),o=this.adaptor.append(this.adaptor.append(this.chtml,this.html("mjx-row")),this.html("mjx-under"));this.baseChild.toCHTML(r),this.scriptChild.toCHTML(o);var n=this.baseChild.getBBox(),i=this.scriptChild.getBBox(),a=this.getUnderKV(n,i)[0],s=this.isLineBelow?0:this.getDelta(!0);this.adaptor.setStyle(o,"paddingTop",this.em(a)),this.setDeltaW([r,o],this.getDeltaW([n,i],[0,-s])),this.adjustUnderDepth(o,i)},e.kind=h.MmlMunder.prototype.kind,e.styles={"mjx-over":{"text-align":"left"},'mjx-munder:not([limits="false"])':{display:"inline-table"},"mjx-munder > mjx-row":{"text-align":"left"},"mjx-under":{"padding-bottom":".1em"}},e}(a.CommonMunderMixin(i.CHTMLmsub));e.CHTMLmunder=c;var u=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(e){if(this.hasMovableLimits())return t.prototype.toCHTML.call(this,e),void this.adaptor.setAttribute(this.chtml,"limits","false");this.chtml=this.standardCHTMLnode(e);var r=this.adaptor.append(this.chtml,this.html("mjx-over")),o=this.adaptor.append(this.chtml,this.html("mjx-base"));this.scriptChild.toCHTML(r),this.baseChild.toCHTML(o);var n=this.scriptChild.getBBox(),i=this.baseChild.getBBox(),a=this.getOverKU(i,n)[0],s=this.isLineAbove?0:this.getDelta();this.adaptor.setStyle(r,"paddingBottom",this.em(a)),this.setDeltaW([o,r],this.getDeltaW([i,n],[0,s])),this.adjustOverDepth(r,n)},e.kind=h.MmlMover.prototype.kind,e.styles={'mjx-mover:not([limits="false"])':{"padding-top":".1em"},'mjx-mover:not([limits="false"]) > *':{display:"block","text-align":"left"}},e}(s.CommonMoverMixin(i.CHTMLmsup));e.CHTMLmover=u;var p=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(e){if(this.hasMovableLimits())return t.prototype.toCHTML.call(this,e),void this.adaptor.setAttribute(this.chtml,"limits","false");this.chtml=this.standardCHTMLnode(e);var r=this.adaptor.append(this.chtml,this.html("mjx-over")),o=this.adaptor.append(this.adaptor.append(this.chtml,this.html("mjx-box")),this.html("mjx-munder")),n=this.adaptor.append(this.adaptor.append(o,this.html("mjx-row")),this.html("mjx-base")),i=this.adaptor.append(this.adaptor.append(o,this.html("mjx-row")),this.html("mjx-under"));this.overChild.toCHTML(r),this.baseChild.toCHTML(n),this.underChild.toCHTML(i);var a=this.overChild.getBBox(),s=this.baseChild.getBBox(),l=this.underChild.getBBox(),h=this.getOverKU(s,a)[0],c=this.getUnderKV(s,l)[0],u=this.getDelta();this.adaptor.setStyle(r,"paddingBottom",this.em(h)),this.adaptor.setStyle(i,"paddingTop",this.em(c)),this.setDeltaW([n,i,r],this.getDeltaW([s,l,a],[0,this.isLineBelow?0:-u,this.isLineAbove?0:u])),this.adjustOverDepth(r,a),this.adjustUnderDepth(i,l)},e.kind=h.MmlMunderover.prototype.kind,e.styles={'mjx-munderover:not([limits="false"])':{"padding-top":".1em"},'mjx-munderover:not([limits="false"]) > *':{display:"block"}},e}(l.CommonMunderoverMixin(i.CHTMLmsubsup));e.CHTMLmunderover=p},7322:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},a=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLscriptbase=void 0;var s=r(6617),l=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){this.chtml=this.standardCHTMLnode(t);var e=i(this.getOffset(),2),r=e[0],o=e[1],n=r-(this.baseRemoveIc?this.baseIc:0),a={"vertical-align":this.em(o)};n&&(a["margin-left"]=this.em(n)),this.baseChild.toCHTML(this.chtml),this.scriptChild.toCHTML(this.adaptor.append(this.chtml,this.html("mjx-script",{style:a})))},e.prototype.setDeltaW=function(t,e){for(var r=0;r=0||this.adaptor.setStyle(t,"marginBottom",this.em(e.d*e.rscale))},e.prototype.adjustUnderDepth=function(t,e){var r,o;if(!(e.d>=0)){var n=this.adaptor,i=this.em(e.d),s=this.html("mjx-box",{style:{"margin-bottom":i,"vertical-align":i}});try{for(var l=a(n.childNodes(n.firstChild(t))),h=l.next();!h.done;h=l.next()){var c=h.value;n.append(s,c)}}catch(t){r={error:t}}finally{try{h&&!h.done&&(o=l.return)&&o.call(l)}finally{if(r)throw r.error}}n.append(n.firstChild(t),s)}},e.kind="scriptbase",e}(r(7091).CommonScriptbaseMixin(s.CHTMLWrapper));e.CHTMLscriptbase=l},7795:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CHTMLxml=e.CHTMLannotationXML=e.CHTMLannotation=e.CHTMLsemantics=void 0;var i=r(6617),a=r(3191),s=r(9167),l=r(8921),h=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){var e=this.standardCHTMLnode(t);this.childNodes.length&&this.childNodes[0].toCHTML(e)},e.kind=s.MmlSemantics.prototype.kind,e}(a.CommonSemanticsMixin(i.CHTMLWrapper));e.CHTMLsemantics=h;var c=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(e){t.prototype.toCHTML.call(this,e)},e.prototype.computeBBox=function(){return this.bbox},e.kind=s.MmlAnnotation.prototype.kind,e}(i.CHTMLWrapper);e.CHTMLannotation=c;var u=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.kind=s.MmlAnnotationXML.prototype.kind,e.styles={"mjx-annotation-xml":{"font-family":"initial","line-height":"normal"}},e}(i.CHTMLWrapper);e.CHTMLannotationXML=u;var p=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.toCHTML=function(t){this.chtml=this.adaptor.append(t,this.adaptor.clone(this.node.getXML()))},e.prototype.computeBBox=function(t,e){void 0===e&&(e=!1);var r=this.jax.measureXMLnode(this.node.getXML()),o=r.w,n=r.h,i=r.d;t.w=o,t.h=n,t.d=i},e.prototype.getStyles=function(){},e.prototype.getScale=function(){},e.prototype.getVariant=function(){},e.kind=l.XMLNode.prototype.kind,e.autoStyle=!1,e}(i.CHTMLWrapper);e.CHTMLxml=p},9250:function(t,e,r){var o=this&&this.__assign||function(){return(o=Object.assign||function(t){for(var e,r=1,o=arguments.length;r0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},i=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.FontData=e.NOSTRETCH=e.H=e.V=void 0;var s=r(9077);e.V=1,e.H=2,e.NOSTRETCH={dir:0};var l=function(){function t(t){var e,r,l,h;void 0===t&&(t=null),this.variant={},this.delimiters={},this.cssFontMap={},this.remapChars={},this.skewIcFactor=.75;var c=this.constructor;this.options=s.userOptions(s.defaultOptions({},c.OPTIONS),t),this.params=o({},c.defaultParams),this.sizeVariants=i([],n(c.defaultSizeVariants)),this.stretchVariants=i([],n(c.defaultStretchVariants)),this.cssFontMap=o({},c.defaultCssFonts);try{for(var u=a(Object.keys(this.cssFontMap)),p=u.next();!p.done;p=u.next()){var d=p.value;"unknown"===this.cssFontMap[d][0]&&(this.cssFontMap[d][0]=this.options.unknownFamily)}}catch(t){e={error:t}}finally{try{p&&!p.done&&(r=u.return)&&r.call(u)}finally{if(e)throw e.error}}this.cssFamilyPrefix=c.defaultCssFamilyPrefix,this.createVariants(c.defaultVariants),this.defineDelimiters(c.defaultDelimiters);try{for(var f=a(Object.keys(c.defaultChars)),m=f.next();!m.done;m=f.next()){var y=m.value;this.defineChars(y,c.defaultChars[y])}}catch(t){l={error:t}}finally{try{m&&!m.done&&(h=f.return)&&h.call(f)}finally{if(l)throw l.error}}this.defineRemap("accent",c.defaultAccentMap),this.defineRemap("mo",c.defaultMoMap),this.defineRemap("mn",c.defaultMnMap)}return t.charOptions=function(t,e){var r=t[e];return 3===r.length&&(r[3]={}),r[3]},Object.defineProperty(t.prototype,"styles",{get:function(){return this._styles},set:function(t){this._styles=t},enumerable:!1,configurable:!0}),t.prototype.createVariant=function(t,e,r){void 0===e&&(e=null),void 0===r&&(r=null);var o={linked:[],chars:e?Object.create(this.variant[e].chars):{}};r&&this.variant[r]&&(Object.assign(o.chars,this.variant[r].chars),this.variant[r].linked.push(o.chars),o.chars=Object.create(o.chars)),this.remapSmpChars(o.chars,t),this.variant[t]=o},t.prototype.remapSmpChars=function(t,e){var r,o,i,s,l=this.constructor;if(l.VariantSmp[e]){var h=l.SmpRemap,c=[null,null,l.SmpRemapGreekU,l.SmpRemapGreekL];try{for(var u=a(l.SmpRanges),p=u.next();!p.done;p=u.next()){var d=n(p.value,3),f=d[0],m=d[1],y=d[2],v=l.VariantSmp[e][f];if(v){for(var b=m;b<=y;b++)if(930!==b){var x=v+b-m;t[b]=this.smpChar(h[x]||x)}if(c[f])try{for(var g=(i=void 0,a(Object.keys(c[f]).map((function(t){return parseInt(t)})))),M=g.next();!M.done;M=g.next()){t[b=M.value]=this.smpChar(v+c[f][b])}}catch(t){i={error:t}}finally{try{M&&!M.done&&(s=g.return)&&s.call(g)}finally{if(i)throw i.error}}}}}catch(t){r={error:t}}finally{try{p&&!p.done&&(o=u.return)&&o.call(u)}finally{if(r)throw r.error}}}"bold"===e&&(t[988]=this.smpChar(120778),t[989]=this.smpChar(120779))},t.prototype.smpChar=function(t){return[,,,{smp:t}]},t.prototype.createVariants=function(t){var e,r;try{for(var o=a(t),n=o.next();!n.done;n=o.next()){var i=n.value;this.createVariant(i[0],i[1],i[2])}}catch(t){e={error:t}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(e)throw e.error}}},t.prototype.defineChars=function(t,e){var r,o,n=this.variant[t];Object.assign(n.chars,e);try{for(var i=a(n.linked),s=i.next();!s.done;s=i.next()){var l=s.value;Object.assign(l,e)}}catch(t){r={error:t}}finally{try{s&&!s.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}},t.prototype.defineDelimiters=function(t){Object.assign(this.delimiters,t)},t.prototype.defineRemap=function(t,e){this.remapChars.hasOwnProperty(t)||(this.remapChars[t]={}),Object.assign(this.remapChars[t],e)},t.prototype.getDelimiter=function(t){return this.delimiters[t]},t.prototype.getSizeVariant=function(t,e){return this.delimiters[t].variants&&(e=this.delimiters[t].variants[e]),this.sizeVariants[e]},t.prototype.getStretchVariant=function(t,e){return this.stretchVariants[this.delimiters[t].stretchv?this.delimiters[t].stretchv[e]:0]},t.prototype.getChar=function(t,e){return this.variant[t].chars[e]},t.prototype.getVariant=function(t){return this.variant[t]},t.prototype.getCssFont=function(t){return this.cssFontMap[t]||["serif",!1,!1]},t.prototype.getFamily=function(t){return this.cssFamilyPrefix?this.cssFamilyPrefix+", "+t:t},t.prototype.getRemappedChar=function(t,e){return(this.remapChars[t]||{})[e]},t.OPTIONS={unknownFamily:"serif"},t.defaultVariants=[["normal"],["bold","normal"],["italic","normal"],["bold-italic","italic","bold"],["double-struck","bold"],["fraktur","normal"],["bold-fraktur","bold","fraktur"],["script","italic"],["bold-script","bold-italic","script"],["sans-serif","normal"],["bold-sans-serif","bold","sans-serif"],["sans-serif-italic","italic","sans-serif"],["sans-serif-bold-italic","bold-italic","bold-sans-serif"],["monospace","normal"]],t.defaultCssFonts={normal:["unknown",!1,!1],bold:["unknown",!1,!0],italic:["unknown",!0,!1],"bold-italic":["unknown",!0,!0],"double-struck":["unknown",!1,!0],fraktur:["unknown",!1,!1],"bold-fraktur":["unknown",!1,!0],script:["cursive",!1,!1],"bold-script":["cursive",!1,!0],"sans-serif":["sans-serif",!1,!1],"bold-sans-serif":["sans-serif",!1,!0],"sans-serif-italic":["sans-serif",!0,!1],"sans-serif-bold-italic":["sans-serif",!0,!0],monospace:["monospace",!1,!1]},t.defaultCssFamilyPrefix="",t.VariantSmp={bold:[119808,119834,120488,120514,120782],italic:[119860,119886,120546,120572],"bold-italic":[119912,119938,120604,120630],script:[119964,119990],"bold-script":[120016,120042],fraktur:[120068,120094],"double-struck":[120120,120146,,,120792],"bold-fraktur":[120172,120198],"sans-serif":[120224,120250,,,120802],"bold-sans-serif":[120276,120302,120662,120688,120812],"sans-serif-italic":[120328,120354],"sans-serif-bold-italic":[120380,120406,120720,120746],monospace:[120432,120458,,,120822]},t.SmpRanges=[[0,65,90],[1,97,122],[2,913,937],[3,945,969],[4,48,57]],t.SmpRemap={119893:8462,119965:8492,119968:8496,119969:8497,119971:8459,119972:8464,119975:8466,119976:8499,119981:8475,119994:8495,119996:8458,120004:8500,120070:8493,120075:8460,120076:8465,120085:8476,120093:8488,120122:8450,120127:8461,120133:8469,120135:8473,120136:8474,120137:8477,120145:8484},t.SmpRemapGreekU={8711:25,1012:17},t.SmpRemapGreekL={977:27,981:29,982:31,1008:28,1009:30,1013:26,8706:25},t.defaultAccentMap={768:"\u02cb",769:"\u02ca",770:"\u02c6",771:"\u02dc",772:"\u02c9",774:"\u02d8",775:"\u02d9",776:"\xa8",778:"\u02da",780:"\u02c7",8594:"\u20d7",8242:"'",8243:"''",8244:"'''",8245:"`",8246:"``",8247:"```",8279:"''''",8400:"\u21bc",8401:"\u21c0",8406:"\u2190",8417:"\u2194",8432:"*",8411:"...",8412:"....",8428:"\u21c1",8429:"\u21bd",8430:"\u2190",8431:"\u2192"},t.defaultMoMap={45:"\u2212"},t.defaultMnMap={45:"\u2212"},t.defaultParams={x_height:.442,quad:1,num1:.676,num2:.394,num3:.444,denom1:.686,denom2:.345,sup1:.413,sup2:.363,sup3:.289,sub1:.15,sub2:.247,sup_drop:.386,sub_drop:.05,delim1:2.39,delim2:1,axis_height:.25,rule_thickness:.06,big_op_spacing1:.111,big_op_spacing2:.167,big_op_spacing3:.2,big_op_spacing4:.6,big_op_spacing5:.1,surd_height:.075,scriptspace:.05,nulldelimiterspace:.12,delimiterfactor:901,delimitershortfall:.3,min_rule_thickness:1.25,separation_factor:1.75,extra_ic:.033},t.defaultDelimiters={},t.defaultChars={},t.defaultSizeVariants=[],t.defaultStretchVariants=[],t}();e.FontData=l},5373:function(t,e){var r=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonArrow=e.CommonDiagonalArrow=e.CommonDiagonalStrike=e.CommonBorder2=e.CommonBorder=e.arrowBBox=e.diagonalArrowDef=e.arrowDef=e.arrowBBoxW=e.arrowBBoxHD=e.arrowHead=e.fullBorder=e.fullPadding=e.fullBBox=e.sideNames=e.sideIndex=e.SOLID=e.PADDING=e.THICKNESS=e.ARROWY=e.ARROWDX=e.ARROWX=void 0,e.ARROWX=4,e.ARROWDX=1,e.ARROWY=2,e.THICKNESS=.067,e.PADDING=.2,e.SOLID=e.THICKNESS+"em solid",e.sideIndex={top:0,right:1,bottom:2,left:3},e.sideNames=Object.keys(e.sideIndex),e.fullBBox=function(t){return new Array(4).fill(t.thickness+t.padding)},e.fullPadding=function(t){return new Array(4).fill(t.padding)},e.fullBorder=function(t){return new Array(4).fill(t.thickness)};e.arrowHead=function(t){return Math.max(t.padding,t.thickness*(t.arrowhead.x+t.arrowhead.dx+1))};e.arrowBBoxHD=function(t,e){if(t.childNodes[0]){var r=t.childNodes[0].getBBox(),o=r.h,n=r.d;e[0]=e[2]=Math.max(0,t.thickness*t.arrowhead.y-(o+n)/2)}return e};e.arrowBBoxW=function(t,e){if(t.childNodes[0]){var r=t.childNodes[0].getBBox().w;e[1]=e[3]=Math.max(0,t.thickness*t.arrowhead.y-r/2)}return e},e.arrowDef={up:[-Math.PI/2,!1,!0,"verticalstrike"],down:[Math.PI/2,!1,!0,"verticakstrike"],right:[0,!1,!1,"horizontalstrike"],left:[Math.PI,!1,!1,"horizontalstrike"],updown:[Math.PI/2,!0,!0,"verticalstrike uparrow downarrow"],leftright:[0,!0,!1,"horizontalstrike leftarrow rightarrow"]},e.diagonalArrowDef={updiagonal:[-1,0,!1,"updiagonalstrike northeastarrow"],northeast:[-1,0,!1,"updiagonalstrike updiagonalarrow"],southeast:[1,0,!1,"downdiagonalstrike"],northwest:[1,Math.PI,!1,"downdiagonalstrike"],southwest:[-1,Math.PI,!1,"updiagonalstrike"],northeastsouthwest:[-1,0,!0,"updiagonalstrike northeastarrow updiagonalarrow southwestarrow"],northwestsoutheast:[1,0,!0,"downdiagonalstrike northwestarrow southeastarrow"]},e.arrowBBox={up:function(t){return e.arrowBBoxW(t,[e.arrowHead(t),0,t.padding,0])},down:function(t){return e.arrowBBoxW(t,[t.padding,0,e.arrowHead(t),0])},right:function(t){return e.arrowBBoxHD(t,[0,e.arrowHead(t),0,t.padding])},left:function(t){return e.arrowBBoxHD(t,[0,t.padding,0,e.arrowHead(t)])},updown:function(t){return e.arrowBBoxW(t,[e.arrowHead(t),0,e.arrowHead(t),0])},leftright:function(t){return e.arrowBBoxHD(t,[0,e.arrowHead(t),0,e.arrowHead(t)])}};e.CommonBorder=function(t){return function(r){var o=e.sideIndex[r];return[r,{renderer:t,bbox:function(t){var e=[0,0,0,0];return e[o]=t.thickness+t.padding,e},border:function(t){var e=[0,0,0,0];return e[o]=t.thickness,e}}]}};e.CommonBorder2=function(t){return function(r,o,n){var i=e.sideIndex[o],a=e.sideIndex[n];return[r,{renderer:t,bbox:function(t){var e=t.thickness+t.padding,r=[0,0,0,0];return r[i]=r[a]=e,r},border:function(t){var e=[0,0,0,0];return e[i]=e[a]=t.thickness,e},remove:o+" "+n}]}};e.CommonDiagonalStrike=function(t){return function(r){var o="mjx-"+r.charAt(0)+"strike";return[r+"diagonalstrike",{renderer:t(o),bbox:e.fullBBox}]}};e.CommonDiagonalArrow=function(t){return function(o){var n=r(e.diagonalArrowDef[o],4),i=n[0],a=n[1],s=n[2];return[o+"arrow",{renderer:function(e,o){var n=r(e.arrowAW(),2),l=n[0],h=n[1],c=e.arrow(h,i*(l-a),s);t(e,c)},bbox:function(t){var e=t.arrowData(),o=e.a,n=e.x,i=e.y,a=r([t.arrowhead.x,t.arrowhead.y,t.arrowhead.dx],3),s=a[0],l=a[1],h=a[2],c=r(t.getArgMod(s+h,l),2),u=c[0],p=c[1],d=i+(u>o?t.thickness*p*Math.sin(u-o):0),f=n+(u>Math.PI/2-o?t.thickness*p*Math.sin(u+o-Math.PI/2):0);return[d,f,d,f]},remove:n[3]}]}};e.CommonArrow=function(t){return function(o){var n=r(e.arrowDef[o],4),i=n[0],a=n[1],s=n[2],l=n[3];return[o+"arrow",{renderer:function(e,o){var n=e.getBBox(),l=n.w,h=n.h,c=n.d,u=r(s?[h+c,"X"]:[l,"Y"],2),p=u[0],d=u[1],f=e.getOffset(d),m=e.arrow(p,i,a,d,f);t(e,m)},bbox:e.arrowBBox[o],remove:l}]}}},716:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__assign||function(){return(i=Object.assign||function(t){for(var e,r=1,o=arguments.length;r0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},s=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonOutputJax=void 0;var l=r(3985),h=r(4769),c=r(9077),u=r(6914),p=r(5878),d=r(5888),f=function(t){function e(e,r,o){void 0===e&&(e=null),void 0===r&&(r=null),void 0===o&&(o=null);var n=this,i=a(c.separateOptions(e,o.OPTIONS),2),s=i[0],l=i[1];return(n=t.call(this,s)||this).factory=n.options.wrapperFactory||new r,n.factory.jax=n,n.cssStyles=n.options.cssStyles||new d.CssStyles,n.font=n.options.font||new o(l),n.unknownCache=new Map,n}return n(e,t),e.prototype.typeset=function(t,e){this.setDocument(e);var r=this.createNode();return this.toDOM(t,r,e),r},e.prototype.createNode=function(){var t=this.constructor.NAME;return this.html("mjx-container",{class:"MathJax",jax:t})},e.prototype.setScale=function(t){var e=this.math.metrics.scale*this.options.scale;1!==e&&this.adaptor.setStyle(t,"fontSize",u.percent(e))},e.prototype.toDOM=function(t,e,r){void 0===r&&(r=null),this.setDocument(r),this.math=t,this.pxPerEm=t.metrics.ex/this.font.params.x_height,t.root.setTeXclass(null),this.setScale(e),this.nodeMap=new Map,this.container=e,this.processMath(t.root,e),this.nodeMap=null,this.executeFilters(this.postFilters,t,r,e)},e.prototype.getBBox=function(t,e){this.setDocument(e),this.math=t,t.root.setTeXclass(null),this.nodeMap=new Map;var r=this.factory.wrap(t.root).getBBox();return this.nodeMap=null,r},e.prototype.getMetrics=function(t){var e,r;this.setDocument(t);var o=this.adaptor,n=this.getMetricMaps(t);try{for(var i=s(t.math),a=i.next();!a.done;a=i.next()){var l=a.value,c=o.parent(l.start.node);if(l.state()=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},a=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},s=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r600?"bold":"normal"),o.family?r=this.explicitVariant(o.family,o.weight,o.style):(this.node.getProperty("variantForm")&&(r="-tex-variant"),r=(e.BOLDVARIANTS[o.weight]||{})[r]||r,r=(e.ITALICVARIANTS[o.style]||{})[r]||r)}this.variant=r}},e.prototype.explicitVariant=function(t,e,r){var o=this.styles;return o||(o=this.styles=new p.Styles),o.set("fontFamily",t),e&&o.set("fontWeight",e),r&&o.set("fontStyle",r),"-explicitFont"},e.prototype.getScale=function(){var t=1,e=this.parent,r=e?e.bbox.scale:1,o=this.node.attributes,n=Math.min(o.get("scriptlevel"),2),i=o.get("fontsize"),a=this.node.isToken||this.node.isKind("mstyle")?o.get("mathsize"):o.getInherited("mathsize");if(0!==n){t=Math.pow(o.get("scriptsizemultiplier"),n);var s=this.length2em(o.get("scriptminsize"),.8,1);t0;this.bbox.L=o.isSet("lspace")?Math.max(0,this.length2em(o.get("lspace"))):y(n,t.lspace),this.bbox.R=o.isSet("rspace")?Math.max(0,this.length2em(o.get("rspace"))):y(n,t.rspace);var i=r.childIndex(e);if(0!==i){var a=r.childNodes[i-1];if(a.isEmbellished){var s=this.jax.nodeMap.get(a).getBBox();s.R&&(this.bbox.L=Math.max(0,this.bbox.L-s.R))}}}},e.prototype.getTeXSpacing=function(t,e){if(!e){var r=this.node.texSpacing();r&&(this.bbox.L=this.length2em(r))}if(t||e){var o=this.node.coreMO().attributes;o.isSet("lspace")&&(this.bbox.L=Math.max(0,this.length2em(o.get("lspace")))),o.isSet("rspace")&&(this.bbox.R=Math.max(0,this.length2em(o.get("rspace"))))}},e.prototype.isTopEmbellished=function(){return this.node.isEmbellished&&!(this.node.parent&&this.node.parent.isEmbellished)},e.prototype.core=function(){return this.jax.nodeMap.get(this.node.core())},e.prototype.coreMO=function(){return this.jax.nodeMap.get(this.node.coreMO())},e.prototype.getText=function(){var t,e,r="";if(this.node.isToken)try{for(var o=i(this.node.childNodes),n=o.next();!n.done;n=o.next()){var a=n.value;a instanceof h.TextNode&&(r+=a.getText())}}catch(e){t={error:e}}finally{try{n&&!n.done&&(e=o.return)&&e.call(o)}finally{if(t)throw t.error}}return r},e.prototype.canStretch=function(t){if(this.stretch=f.NOSTRETCH,this.node.isEmbellished){var e=this.core();e&&e.node!==this.node&&e.canStretch(t)&&(this.stretch=e.stretch)}return 0!==this.stretch.dir},e.prototype.getAlignShift=function(){var t,e=(t=this.node.attributes).getList.apply(t,s([],a(h.indentAttributes))),r=e.indentalign,o=e.indentshift,n=e.indentalignfirst,i=e.indentshiftfirst;return"indentalign"!==n&&(r=n),"auto"===r&&(r=this.jax.options.displayAlign),"indentshift"!==i&&(o=i),"auto"===o&&(o=this.jax.options.displayIndent,"right"!==r||o.match(/^\s*0[a-z]*\s*$/)||(o=("-"+o.trim()).replace(/^--/,""))),[r,this.length2em(o,this.metrics.containerWidth)]},e.prototype.getAlignX=function(t,e,r){return"right"===r?t-(e.w+e.R)*e.rscale:"left"===r?e.L*e.rscale:(t-e.w*e.rscale)/2},e.prototype.getAlignY=function(t,e,r,o,n){return"top"===n?t-r:"bottom"===n?o-e:"center"===n?(t-r-(e-o))/2:0},e.prototype.getWrapWidth=function(t){return this.childNodes[t].getBBox().w},e.prototype.getChildAlign=function(t){return"left"},e.prototype.percent=function(t){return u.percent(t)},e.prototype.em=function(t){return u.em(t)},e.prototype.px=function(t,e){return void 0===e&&(e=-u.BIGDIMEN),u.px(t,e,this.metrics.em)},e.prototype.length2em=function(t,e,r){return void 0===e&&(e=1),void 0===r&&(r=null),null===r&&(r=this.bbox.scale),u.length2em(t,e,r,this.jax.pxPerEm)},e.prototype.unicodeChars=function(t,e){void 0===e&&(e=this.variant);var r=c.unicodeChars(t),o=this.font.getVariant(e);if(o&&o.chars){var n=o.chars;r=r.map((function(t){return((n[t]||[])[3]||{}).smp||t}))}return r},e.prototype.remapChars=function(t){return t},e.prototype.mmlText=function(t){return this.node.factory.create("text").setText(t)},e.prototype.mmlNode=function(t,e,r){return void 0===e&&(e={}),void 0===r&&(r=[]),this.node.factory.create(t,e,r)},e.prototype.createMo=function(t){var e=this.node.factory,r=e.create("text").setText(t),o=e.create("mo",{stretchy:!0},[r]);o.inheritAttributesFrom(this.node);var n=this.wrap(o);return n.parent=this,n},e.prototype.getVariantChar=function(t,e){var r=this.font.getChar(t,e)||[0,0,0,{unknown:!0}];return 3===r.length&&(r[3]={}),r},e.kind="unknown",e.styles={},e.removeStyles=["fontSize","fontFamily","fontWeight","fontStyle","fontVariant","font"],e.skipAttributes={fontfamily:!0,fontsize:!0,fontweight:!0,fontstyle:!0,color:!0,background:!0,class:!0,href:!0,style:!0,xmlns:!0},e.BOLDVARIANTS={bold:{normal:"bold",italic:"bold-italic",fraktur:"bold-fraktur",script:"bold-script","sans-serif":"bold-sans-serif","sans-serif-italic":"sans-serif-bold-italic"},normal:{bold:"normal","bold-italic":"italic","bold-fraktur":"fraktur","bold-script":"script","bold-sans-serif":"sans-serif","sans-serif-bold-italic":"sans-serif-italic"}},e.ITALICVARIANTS={italic:{normal:"italic",bold:"bold-italic","sans-serif":"sans-serif-italic","bold-sans-serif":"sans-serif-bold-italic"},normal:{italic:"normal","bold-italic":"bold","sans-serif-italic":"sans-serif","sans-serif-bold-italic":"bold-sans-serif"}},e}(l.AbstractWrapper);e.CommonWrapper=v},1475:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CommonWrapperFactory=void 0;var i=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.jax=null,e}return n(e,t),Object.defineProperty(e.prototype,"Wrappers",{get:function(){return this.node},enumerable:!1,configurable:!0}),e.defaultNodes={},e}(r(2506).AbstractWrapperFactory);e.CommonWrapperFactory=i},3438:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)});Object.defineProperty(e,"__esModule",{value:!0}),e.CommonTeXAtomMixin=void 0;var i=r(8921);e.CommonTeXAtomMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.computeBBox=function(e,r){if(void 0===r&&(r=!1),t.prototype.computeBBox.call(this,e,r),this.childNodes[0]&&this.childNodes[0].bbox.ic&&(e.ic=this.childNodes[0].bbox.ic),this.node.texClass===i.TEXCLASS.VCENTER){var o=e.h,n=(o+e.d)/2+this.font.params.axis_height-o;e.h+=n,e.d-=n}},e}(t)}},555:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),n=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonTextNodeMixin=void 0,e.CommonTextNodeMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.computeBBox=function(t,e){var r,o;void 0===e&&(e=!1);var a=this.parent.variant,s=this.node.getText();if("-explicitFont"===a){var l=this.jax.getFontData(this.parent.styles),h=this.jax.measureText(s,a,l),c=h.w,u=h.h,p=h.d;t.h=u,t.d=p,t.w=c}else{var d=this.remappedText(s,a);t.empty();try{for(var f=n(d),m=f.next();!m.done;m=f.next()){var y=m.value,v=i(this.getVariantChar(a,y),4),b=(u=v[0],p=v[1],c=v[2],v[3]);if(b.unknown){var x=this.jax.measureText(String.fromCodePoint(y),a);c=x.w,u=x.h,p=x.d}t.w+=c,u>t.h&&(t.h=u),p>t.d&&(t.d=p),t.ic=b.ic||0,t.sk=b.sk||0}}catch(t){r={error:t}}finally{try{m&&!m.done&&(o=f.return)&&o.call(f)}finally{if(r)throw r.error}}d.length>1&&(t.sk=0),t.clean()}},e.prototype.remappedText=function(t,e){var r=this.parent.stretch.c;return r?[r]:this.parent.remapChars(this.unicodeChars(t,e))},e.prototype.getStyles=function(){},e.prototype.getVariant=function(){},e.prototype.getScale=function(){},e.prototype.getSpace=function(){},e}(t)}},3345:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},a=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},a=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMencloseMixin=void 0;var l=r(5373),h=r(6720);e.CommonMencloseMixin=function(t){return function(t){function e(){for(var e=[],r=0;r.001?s:0},e.prototype.getArgMod=function(t,e){return[Math.atan2(e,t),Math.sqrt(t*t+e*e)]},e.prototype.arrow=function(t,e,r,o,n){return void 0===o&&(o=""),void 0===n&&(n=0),null},e.prototype.arrowData=function(){var t=i([this.padding,this.thickness],2),e=t[0],r=t[1]*(this.arrowhead.x+Math.max(1,this.arrowhead.dx)),o=this.childNodes[0].getBBox(),n=o.h,a=o.d,s=o.w,l=n+a,h=Math.sqrt(l*l+s*s),c=Math.max(e,r*s/h),u=Math.max(e,r*l/h),p=i(this.getArgMod(s+2*c,l+2*u),2);return{a:p[0],W:p[1],x:c,y:u}},e.prototype.arrowAW=function(){var t=this.childNodes[0].getBBox(),e=t.h,r=t.d,o=t.w,n=i(this.TRBL,4),a=n[0],s=n[1],l=n[2],h=n[3];return this.getArgMod(h+o+s,a+e+r+l)},e.prototype.createMsqrt=function(t){var e=this.node.factory.create("msqrt");e.inheritAttributesFrom(this.node),e.childNodes[0]=t.node;var r=this.wrap(e);return r.parent=this,r},e.prototype.sqrtTRBL=function(){var t=this.msqrt.getBBox(),e=this.msqrt.childNodes[0].getBBox();return[t.h-e.h,0,t.d-e.d,t.w-e.w]},e}(t)}},1346:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},i=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMfencedMixin=void 0,e.CommonMfencedMixin=function(t){return function(t){function e(){for(var e=[],r=0;r0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},i=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},i=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},a=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMmultiscriptsMixin=e.ScriptNames=e.NextScript=void 0;var l=r(3717);e.NextScript={base:"subList",subList:"supList",supList:"subList",psubList:"psupList",psupList:"psubList"},e.ScriptNames=["sup","sup","psup","psub"],e.CommonMmultiscriptsMixin=function(t){return function(t){function r(){for(var e=[],r=0;re.length&&e.push(l.BBox.empty())},r.prototype.combineBBoxLists=function(t,e,r,o){for(var n=0;nt.h&&(t.h=l),h>t.d&&(t.d=h),p>e.h&&(e.h=p),d>e.d&&(e.d=d)}},r.prototype.getScaledWHD=function(t){var e=t.w,r=t.h,o=t.d,n=t.rscale;return[e*n,r*n,o*n]},r.prototype.getUVQ=function(e,r){var o;if(!this.UVQ){var n=i([0,0,0],3),a=n[0],s=n[1],l=n[2];0===e.h&&0===e.d?a=this.getU():0===r.h&&0===r.d?a=-this.getV():(a=(o=i(t.prototype.getUVQ.call(this,e,r),3))[0],s=o[1],l=o[2]),this.UVQ=[a,s,l]}return this.UVQ},r}(t)}},2304:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)});Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMnMixin=void 0,e.CommonMnMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.remapChars=function(t){if(t.length){var e=this.font.getRemappedChar("mn",t[0]);if(e){var r=this.unicodeChars(e,this.variant);1===r.length?t[0]=r[0]:t=r.concat(t.slice(1))}}return t},e}(t)}},437:function(t,e,r){var o,n,i=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),a=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},s=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMoMixin=e.DirectionVH=void 0;var h=r(3717),c=r(6720),u=r(9250);e.DirectionVH=((n={})[1]="v",n[2]="h",n),e.CommonMoMixin=function(t){return function(t){function e(){for(var e=[],r=0;r=0)&&(t.w=0)},e.prototype.protoBBox=function(e){var r=0!==this.stretch.dir;r&&null===this.size&&this.getStretchedVariant([0]),r&&this.size<0||(t.prototype.computeBBox.call(this,e),this.copySkewIC(e))},e.prototype.getAccentOffset=function(){var t=h.BBox.empty();return this.protoBBox(t),-t.w/2},e.prototype.getCenterOffset=function(e){return void 0===e&&(e=null),e||(e=h.BBox.empty(),t.prototype.computeBBox.call(this,e)),(e.h+e.d)/2+this.font.params.axis_height-e.h},e.prototype.getVariant=function(){this.node.attributes.get("largeop")?this.variant=this.node.attributes.get("displaystyle")?"-largeop":"-smallop":this.node.attributes.getExplicit("mathvariant")||!1!==this.node.getProperty("pseudoscript")?t.prototype.getVariant.call(this):this.variant="-tex-variant"},e.prototype.canStretch=function(t){if(0!==this.stretch.dir)return this.stretch.dir===t;if(!this.node.attributes.get("stretchy"))return!1;var e=this.getText();if(1!==Array.from(e).length)return!1;var r=this.font.getDelimiter(e.codePointAt(0));return this.stretch=r&&r.dir===t?r:u.NOSTRETCH,0!==this.stretch.dir},e.prototype.getStretchedVariant=function(t,e){var r,o;if(void 0===e&&(e=!1),0!==this.stretch.dir){var n=this.getWH(t),i=this.getSize("minsize",0),a=this.getSize("maxsize",1/0),s=this.node.getProperty("mathaccent");n=Math.max(i,Math.min(a,n));var h=this.font.params.delimiterfactor/1e3,c=this.font.params.delimitershortfall,u=i||e?n:s?Math.min(n/h,n+c):Math.max(n*h,n-c),p=this.stretch,d=p.c||this.getText().codePointAt(0),f=0;if(p.sizes)try{for(var m=l(p.sizes),y=m.next();!y.done;y=m.next()){if(y.value>=u)return s&&f&&f--,this.variant=this.font.getSizeVariant(d,f),this.size=f,void(p.schar&&p.schar[f]&&(this.stretch.c=p.schar[f]));f++}}catch(t){r={error:t}}finally{try{y&&!y.done&&(o=m.return)&&o.call(m)}finally{if(r)throw r.error}}p.stretch?(this.size=-1,this.invalidateBBox(),this.getStretchBBox(t,this.checkExtendedHeight(n,p),p)):(this.variant=this.font.getSizeVariant(d,f-1),this.size=f-1)}},e.prototype.getSize=function(t,e){var r=this.node.attributes;return r.isSet(t)&&(e=this.length2em(r.get(t),1,1)),e},e.prototype.getWH=function(t){if(0===t.length)return 0;if(1===t.length)return t[0];var e=a(t,2),r=e[0],o=e[1],n=this.font.params.axis_height;return this.node.attributes.get("symmetric")?2*Math.max(r-n,o+n):r+o},e.prototype.getStretchBBox=function(t,e,r){var o;r.hasOwnProperty("min")&&r.min>e&&(e=r.min);var n=a(r.HDW,3),i=n[0],s=n[1],l=n[2];1===this.stretch.dir?(i=(o=a(this.getBaseline(t,e,r),2))[0],s=o[1]):l=e,this.bbox.h=i,this.bbox.d=s,this.bbox.w=l},e.prototype.getBaseline=function(t,e,r){var o=2===t.length&&t[0]+t[1]===e,n=this.node.attributes.get("symmetric"),i=a(o?t:[e,0],2),s=i[0],l=i[1],h=a([s+l,0],2),c=h[0],u=h[1];if(n){var p=this.font.params.axis_height;o&&(c=2*Math.max(s-p,l+p)),u=c/2-p}else if(o)u=l;else{var d=a(r.HDW||[.75,.25],2),f=d[0],m=d[1];u=m*(c/(f+m))}return[c-u,u]},e.prototype.checkExtendedHeight=function(t,e){if(e.fullExt){var r=a(e.fullExt,2),o=r[0],n=r[1];t=n+Math.ceil(Math.max(0,t-n)/o)*o}return t},e.prototype.remapChars=function(t){var e=this.node.getProperty("primes");if(e)return c.unicodeChars(e);if(1===t.length){var r=this.node.coreParent().parent,o=this.isAccent&&!r.isKind("mrow")?"accent":"mo",n=this.font.getRemappedChar(o,t[0]);n&&(t=this.unicodeChars(n,this.variant))}return t},e}(t)}},7481:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMpaddedMixin=void 0,e.CommonMpaddedMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.getDimens=function(){var t=this.node.attributes.getList("width","height","depth","lspace","voffset"),e=this.childNodes[0].getBBox(),r=e.w,o=e.h,n=e.d,i=r,a=o,s=n,l=0,h=0,c=0;""!==t.width&&(r=this.dimen(t.width,e,"w",0)),""!==t.height&&(o=this.dimen(t.height,e,"h",0)),""!==t.depth&&(n=this.dimen(t.depth,e,"d",0)),""!==t.voffset&&(h=this.dimen(t.voffset,e)),""!==t.lspace&&(l=this.dimen(t.lspace,e));var u=this.node.attributes.get("data-align");return u&&(c=this.getAlignX(r,e,u)),[a,s,i,o-a,n-s,r-i,l,h,c]},e.prototype.dimen=function(t,e,r,o){void 0===r&&(r=""),void 0===o&&(o=null);var n=(t=String(t)).match(/width|height|depth/),i=n?e[n[0].charAt(0)]:r?e[r]:0,a=this.length2em(t,i)||0;return t.match(/^[-+]/)&&r&&(a+=i),null!=o&&(a=Math.max(o,a)),a},e.prototype.computeBBox=function(t,e){void 0===e&&(e=!1);var r=n(this.getDimens(),6),o=r[0],i=r[1],a=r[2],s=r[3],l=r[4],h=r[5];t.w=a+h,t.h=o+s,t.d=i+l,this.setChildPWidths(e,t.w)},e.prototype.getWrapWidth=function(t){return this.getBBox().w},e.prototype.getChildAlign=function(t){return this.node.attributes.get("data-align")||"left"},e}(t)}},5997:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)});Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMrootMixin=void 0,e.CommonMrootMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"surd",{get:function(){return 2},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"root",{get:function(){return 1},enumerable:!1,configurable:!0}),e.prototype.combineRootBBox=function(t,e,r){var o=this.childNodes[this.root].getBBox(),n=this.getRootDimens(e,r)[1];t.combine(o,0,n)},e.prototype.getRootDimens=function(t,e){var r=this.childNodes[this.surd],o=this.childNodes[this.root].getBBox(),n=(r.size<0?.5:.6)*t.w,i=o.w,a=o.rscale,s=Math.max(i,n/a),l=Math.max(0,s-i);return[s*a-n,this.rootHeight(o,t,r.size,e),l]},e.prototype.rootHeight=function(t,e,r,o){var n=e.h+e.d;return(r<0?1.9:.55*n)-(n-o)+Math.max(0,t.d*t.rscale)},e}(t)}},9323:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},a=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonInferredMrowMixin=e.CommonMrowMixin=void 0;var l=r(3717);e.CommonMrowMixin=function(t){return function(t){function e(){for(var e,r,o=[],n=0;n1){var p=0,d=0,f=c>1&&c===u;try{for(var m=s(this.childNodes),y=m.next();!y.done;y=m.next()){var v=0===(C=y.value).stretch.dir;if(f||v){var b=C.getBBox(v),x=b.h,g=b.d,M=b.rscale;(x*=M)>p&&(p=x),(g*=M)>d&&(d=g)}}}catch(t){r={error:t}}finally{try{y&&!y.done&&(o=m.return)&&o.call(m)}finally{if(r)throw r.error}}try{for(var _=s(a),w=_.next();!w.done;w=_.next()){var C;(C=w.value).coreMO().getStretchedVariant([p,d])}}catch(t){n={error:t}}finally{try{w&&!w.done&&(i=_.return)&&i.call(_)}finally{if(n)throw n.error}}}},e}(t)},e.CommonInferredMrowMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getScale=function(){this.bbox.scale=this.parent.bbox.scale,this.bbox.rscale=1},e}(t)}},6920:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},i=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},a=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;rthis.surdH?(t.h+t.d-(this.surdH-2*e-r/2))/2:e+r/4]},e.prototype.getRootDimens=function(t,e){return[0,0,0,0]},e}(t)}},3069:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMsubsupMixin=e.CommonMsupMixin=e.CommonMsubMixin=void 0,e.CommonMsubMixin=function(t){var e;return(e=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"scriptChild",{get:function(){return this.childNodes[this.node.sub]},enumerable:!1,configurable:!0}),e.prototype.getOffset=function(){return[0,-this.getV()]},e}(t)).useIC=!1,e},e.CommonMsupMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"scriptChild",{get:function(){return this.childNodes[this.node.sup]},enumerable:!1,configurable:!0}),e.prototype.getOffset=function(){return[this.getAdjustedIc()-(this.baseRemoveIc?0:this.baseIc),this.getU()]},e}(t)},e.CommonMsubsupMixin=function(t){var e;return(e=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.UVQ=null,e}return o(e,t),Object.defineProperty(e.prototype,"subChild",{get:function(){return this.childNodes[this.node.sub]},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"supChild",{get:function(){return this.childNodes[this.node.sup]},enumerable:!1,configurable:!0}),e.prototype.computeBBox=function(t,e){void 0===e&&(e=!1);var r=this.baseChild.getBBox(),o=n([this.subChild.getBBox(),this.supChild.getBBox()],2),i=o[0],a=o[1];t.empty(),t.append(r);var s=this.getBaseWidth(),l=this.getAdjustedIc(),h=n(this.getUVQ(),2),c=h[0],u=h[1];t.combine(i,s,u),t.combine(a,s+l,c),t.w+=this.font.params.scriptspace,t.clean(),this.setChildPWidths(e)},e.prototype.getUVQ=function(t,e){void 0===t&&(t=this.subChild.getBBox()),void 0===e&&(e=this.supChild.getBBox());var r=this.baseCore.getBBox();if(this.UVQ)return this.UVQ;var o=this.font.params,i=3*o.rule_thickness,a=this.length2em(this.node.attributes.get("subscriptshift"),o.sub2),s=this.baseCharZero(r.d*this.baseScale+o.sub_drop*t.rscale),l=n([this.getU(),Math.max(s,a)],2),h=l[0],c=l[1],u=h-e.d*e.rscale-(t.h*t.rscale-c);if(u0&&(h+=p,c-=p)}return h=Math.max(this.length2em(this.node.attributes.get("superscriptshift"),h),h),c=Math.max(this.length2em(this.node.attributes.get("subscriptshift"),c),c),u=h-e.d*e.rscale-(t.h*t.rscale-c),this.UVQ=[h,-c,u],this.UVQ},e}(t)).useIC=!1,e}},8589:function(t,e,r){var o,n=this&&this.__extends||(o=function(t,e){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function r(){this.constructor=t}o(t,e),t.prototype=null===e?Object.create(e):(r.prototype=e.prototype,new r)}),i=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},a=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMtableMixin=void 0;var l=r(3717),h=r(6720),c=r(1490);e.CommonMtableMixin=function(t){return function(t){function e(){for(var e=[],r=0;r1){if(null===e){e=0;var f=p>1&&p===d;try{for(var m=s(this.tableRows),y=m.next();!y.done;y=m.next()){var v;if(v=y.value.getChild(t)){var b=0===(_=v.childNodes[0]).stretch.dir;if(f||b){var x=_.getBBox(b).w;x>e&&(e=x)}}}}catch(t){n={error:t}}finally{try{y&&!y.done&&(i=m.return)&&i.call(m)}finally{if(n)throw n.error}}}try{for(var g=s(h),M=g.next();!M.done;M=g.next()){var _;(_=M.value).coreMO().getStretchedVariant([e])}}catch(t){a={error:t}}finally{try{M&&!M.done&&(l=g.return)&&l.call(g)}finally{if(a)throw a.error}}}},e.prototype.getTableData=function(){if(this.data)return this.data;for(var t=new Array(this.numRows).fill(0),e=new Array(this.numRows).fill(0),r=new Array(this.numCols).fill(0),o=new Array(this.numRows),n=new Array(this.numRows),i=[0],a=this.tableRows,s=0;sn[r]&&(n[r]=h),c>i[r]&&(i[r]=c),d>s&&(s=d),a&&u>a[e]&&(a[e]=u),s},e.prototype.extendHD=function(t,e,r,o){var n=(o-(e[t]+r[t]))/2;n<1e-5||(e[t]+=n,r[t]+=n)},e.prototype.recordPWidthCell=function(t,e){t.childNodes[0]&&t.childNodes[0].getBBox().pwidth&&this.pwidthCells.push([t,e])},e.prototype.computeBBox=function(t,e){void 0===e&&(e=!1);var r,o,n=this.getTableData(),a=n.H,s=n.D;if(this.node.attributes.get("equalrows")){var l=this.getEqualRowHeight();r=c.sum([].concat(this.rLines,this.rSpace))+l*this.numRows}else r=c.sum(a.concat(s,this.rLines,this.rSpace));r+=2*(this.fLine+this.fSpace[1]);var u=this.getComputedWidths();o=c.sum(u.concat(this.cLines,this.cSpace))+2*(this.fLine+this.fSpace[0]);var p=this.node.attributes.get("width");"auto"!==p&&(o=Math.max(this.length2em(p,0)+2*this.fLine,o));var d=i(this.getBBoxHD(r),2),f=d[0],m=d[1];t.h=f,t.d=m,t.w=o;var y=i(this.getBBoxLR(),2),v=y[0],b=y[1];t.L=v,t.R=b,h.isPercent(p)||this.setColumnPWidths()},e.prototype.setChildPWidths=function(t,e,r){var o=this.node.attributes.get("width");if(!h.isPercent(o))return!1;this.hasLabels||(this.bbox.pwidth="",this.container.bbox.pwidth="");var n=this.bbox,i=n.w,a=n.L,s=n.R,l=Math.max(i,this.length2em(o,Math.max(e,a+i+s))),u=this.node.attributes.get("equalcolumns")?Array(this.numCols).fill(this.percent(1/Math.max(1,this.numCols))):this.getColumnAttributes("columnwidth",0);this.cWidths=this.getColumnWidthsFixed(u,l);var p=this.getComputedWidths();return this.pWidth=c.sum(p.concat(this.cLines,this.cSpace))+2*(this.fLine+this.fSpace[0]),this.isTop&&(this.bbox.w=this.pWidth),this.setColumnPWidths(),this.pWidth!==i&&this.parent.invalidateBBox(),this.pWidth!==i},e.prototype.setColumnPWidths=function(){var t,e,r=this.cWidths;try{for(var o=s(this.pwidthCells),n=o.next();!n.done;n=o.next()){var a=i(n.value,2),l=a[0],h=a[1];l.setChildPWidths(!1,r[h])&&(l.invalidateBBox(),l.getBBox())}}catch(e){t={error:e}}finally{try{n&&!n.done&&(e=o.return)&&e.call(o)}finally{if(t)throw t.error}}},e.prototype.getBBoxHD=function(t){var e=i(this.getAlignmentRow(),2),r=e[0],o=e[1];if(null===o){var n=this.font.params.axis_height,a=t/2;return{top:[0,t],center:[a,a],bottom:[t,0],baseline:[a,a],axis:[a+n,a-n]}[r]||[a,a]}var s=this.getVerticalPosition(o,r);return[s,t-s]},e.prototype.getBBoxLR=function(){if(this.hasLabels){var t=this.node.attributes.get("side"),e=i(this.getPadAlignShift(t),2),r=e[0];return"center"===e[1]?[r,r]:"left"===t?[r,0]:[0,r]}return[0,0]},e.prototype.getPadAlignShift=function(t){var e=this.getTableData().L+this.length2em(this.node.attributes.get("minlabelspacing")),r=i(null==this.styles?["",""]:[this.styles.get("padding-left"),this.styles.get("padding-right")],2),o=r[0],n=r[1];(o||n)&&(e=Math.max(e,this.length2em(o||"0"),this.length2em(n||"0")));var a=i(this.getAlignShift(),2),s=a[0],l=a[1];return s===t&&(l="left"===t?Math.max(e,l)-e:Math.min(-e,l)+e),[e,s,l]},e.prototype.getAlignShift=function(){return this.isTop?t.prototype.getAlignShift.call(this):[this.container.getChildAlign(this.containerI),0]},e.prototype.getWidth=function(){return this.pWidth||this.getBBox().w},e.prototype.getEqualRowHeight=function(){var t=this.getTableData(),e=t.H,r=t.D,o=Array.from(e.keys()).map((function(t){return e[t]+r[t]}));return Math.max.apply(Math,o)},e.prototype.getComputedWidths=function(){var t=this,e=this.getTableData().W,r=Array.from(e.keys()).map((function(r){return"number"==typeof t.cWidths[r]?t.cWidths[r]:e[r]}));return this.node.attributes.get("equalcolumns")&&(r=Array(r.length).fill(c.max(r))),r},e.prototype.getColumnWidths=function(){var t=this.node.attributes.get("width");if(this.node.attributes.get("equalcolumns"))return this.getEqualColumns(t);var e=this.getColumnAttributes("columnwidth",0);return"auto"===t?this.getColumnWidthsAuto(e):h.isPercent(t)?this.getColumnWidthsPercent(e):this.getColumnWidthsFixed(e,this.length2em(t))},e.prototype.getEqualColumns=function(t){var e,r=Math.max(1,this.numCols);if("auto"===t){var o=this.getTableData().W;e=c.max(o)}else if(h.isPercent(t))e=this.percent(1/r);else{var n=c.sum([].concat(this.cLines,this.cSpace))+2*this.fSpace[0];e=Math.max(0,this.length2em(t)-n)/r}return Array(this.numCols).fill(e)},e.prototype.getColumnWidthsAuto=function(t){var e=this;return t.map((function(t){return"auto"===t||"fit"===t?null:h.isPercent(t)?t:e.length2em(t)}))},e.prototype.getColumnWidthsPercent=function(t){var e=this,r=t.indexOf("fit")>=0,o=(r?this.getTableData():{W:null}).W;return Array.from(t.keys()).map((function(n){var i=t[n];return"fit"===i?null:"auto"===i?r?o[n]:null:h.isPercent(i)?i:e.length2em(i)}))},e.prototype.getColumnWidthsFixed=function(t,e){var r=this,o=Array.from(t.keys()),n=o.filter((function(e){return"fit"===t[e]})),i=o.filter((function(e){return"auto"===t[e]})),a=n.length||i.length,s=(a?this.getTableData():{W:null}).W,l=e-c.sum([].concat(this.cLines,this.cSpace))-2*this.fSpace[0],h=l;o.forEach((function(o){var n=t[o];h-="fit"===n||"auto"===n?s[o]:r.length2em(n,e)}));var u=a&&h>0?h/a:0;return o.map((function(e){var o=t[e];return"fit"===o?s[e]+u:"auto"===o?s[e]+(0===n.length?u:0):r.length2em(o,l)}))},e.prototype.getVerticalPosition=function(t,e){for(var r=this.node.attributes.get("equalrows"),o=this.getTableData(),n=o.H,a=o.D,s=r?this.getEqualRowHeight():0,l=this.getRowHalfSpacing(),h=this.fLine,c=0;cthis.numRows?null:o-1]},e.prototype.getColumnAttributes=function(t,e){void 0===e&&(e=1);var r=this.numCols-e,o=this.getAttributeArray(t);if(0===o.length)return null;for(;o.lengthr&&o.splice(r),o},e.prototype.getRowAttributes=function(t,e){void 0===e&&(e=1);var r=this.numRows-e,o=this.getAttributeArray(t);if(0===o.length)return null;for(;o.lengthr&&o.splice(r),o},e.prototype.getAttributeArray=function(t){var e=this.node.attributes.get(t);return e?h.split(e):[this.node.attributes.getDefault(t)]},e.prototype.addEm=function(t,e){var r=this;return void 0===e&&(e=1),t?t.map((function(t){return r.em(t/e)})):null},e.prototype.convertLengths=function(t){var e=this;return t?t.map((function(t){return e.length2em(t)})):null},e}(t)}},7805:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)});Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMtdMixin=void 0,e.CommonMtdMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"fixesPWidth",{get:function(){return!1},enumerable:!1,configurable:!0}),e.prototype.invalidateBBox=function(){this.bboxComputed=!1},e.prototype.getWrapWidth=function(t){var e=this.parent.parent,r=this.parent,o=this.node.childPosition()-(r.labeled?1:0);return"number"==typeof e.cWidths[o]?e.cWidths[o]:e.getTableData().W[o]},e.prototype.getChildAlign=function(t){return this.node.attributes.get("columnalign")},e}(t)}},8325:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)});Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMtextMixin=void 0,e.CommonMtextMixin=function(t){var e;return(e=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.getVariant=function(){var e=this.jax.options,r=this.jax.math.outputData,o=(!!r.merrorFamily||!!e.merrorFont)&&this.node.Parent.isKind("merror");if(r.mtextFamily||e.mtextFont||o){var n=this.node.attributes.get("mathvariant"),i=this.constructor.INHERITFONTS[n]||this.jax.font.getCssFont(n),a=i[0]||(o?r.merrorFamily||e.merrorFont:r.mtextFamily||e.mtextFont);this.variant=this.explicitVariant(a,i[2]?"bold":"",i[1]?"italic":"")}else t.prototype.getVariant.call(this)},e}(t)).INHERITFONTS={normal:["",!1,!1],bold:["",!1,!0],italic:["",!0,!1],"bold-italic":["",!0,!0]},e}},4818:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),n=this&&this.__values||function(t){var e="function"==typeof Symbol&&Symbol.iterator,r=e&&t[e],o=0;if(r)return r.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonMlabeledtrMixin=e.CommonMtrMixin=void 0,e.CommonMtrMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"fixesPWidth",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"numCells",{get:function(){return this.childNodes.length},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"labeled",{get:function(){return!1},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"tableCells",{get:function(){return this.childNodes},enumerable:!1,configurable:!0}),e.prototype.getChild=function(t){return this.childNodes[t]},e.prototype.getChildBBoxes=function(){return this.childNodes.map((function(t){return t.getBBox()}))},e.prototype.stretchChildren=function(t){var e,r,o,i,a,s;void 0===t&&(t=null);var l=[],h=this.labeled?this.childNodes.slice(1):this.childNodes;try{for(var c=n(h),u=c.next();!u.done;u=c.next()){(j=u.value.childNodes[0]).canStretch(1)&&l.push(j)}}catch(t){e={error:t}}finally{try{u&&!u.done&&(r=c.return)&&r.call(c)}finally{if(e)throw e.error}}var p=l.length,d=this.childNodes.length;if(p&&d>1){if(null===t){var f=0,m=0,y=p>1&&p===d;try{for(var v=n(h),b=v.next();!b.done;b=v.next()){var x=0===(j=b.value.childNodes[0]).stretch.dir;if(y||x){var g=j.getBBox(x),M=g.h,_=g.d;M>f&&(f=M),_>m&&(m=_)}}}catch(t){o={error:t}}finally{try{b&&!b.done&&(i=v.return)&&i.call(v)}finally{if(o)throw o.error}}t=[f,m]}try{for(var w=n(l),C=w.next();!C.done;C=w.next()){var j;(j=C.value).coreMO().getStretchedVariant(t)}}catch(t){a={error:t}}finally{try{C&&!C.done&&(s=w.return)&&s.call(w)}finally{if(a)throw a.error}}}},e}(t)},e.CommonMlabeledtrMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),Object.defineProperty(e.prototype,"numCells",{get:function(){return Math.max(0,this.childNodes.length-1)},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"labeled",{get:function(){return!0},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"tableCells",{get:function(){return this.childNodes.slice(1)},enumerable:!1,configurable:!0}),e.prototype.getChild=function(t){return this.childNodes[t+1]},e.prototype.getChildBBoxes=function(){return this.childNodes.slice(1).map((function(t){return t.getBBox()}))},e}(t)}},9690:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),n=this&&this.__read||function(t,e){var r="function"==typeof Symbol&&t[Symbol.iterator];if(!r)return t;var o,n,i=r.call(t),a=[];try{for(;(void 0===e||e-- >0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},i=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r0)&&!(o=i.next()).done;)a.push(o.value)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=i.return)&&r.call(i)}finally{if(n)throw n.error}}return a},i=this&&this.__spreadArray||function(t,e){for(var r=0,o=e.length,n=t.length;r=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(e,"__esModule",{value:!0}),e.CommonScriptbaseMixin=void 0,e.CommonScriptbaseMixin=function(t){var e;return(e=function(t){function e(){for(var e=[],r=0;r1){var p=0,d=c>1&&c===u;try{for(var f=a(this.childNodes),m=f.next();!m.done;m=f.next()){var y=0===(_=m.value).stretch.dir;if(d||y){var v=_.getBBox(y),b=v.w,x=v.rscale;b*x>p&&(p=b*x)}}}catch(t){r={error:t}}finally{try{m&&!m.done&&(o=f.return)&&o.call(f)}finally{if(r)throw r.error}}try{for(var g=a(s),M=g.next();!M.done;M=g.next()){var _;(_=M.value).coreMO().getStretchedVariant([p/_.bbox.rscale])}}catch(t){n={error:t}}finally{try{M&&!M.done&&(i=g.return)&&i.call(g)}finally{if(n)throw n.error}}}},e}(t)).useIC=!0,e}},3191:function(t,e){var r,o=this&&this.__extends||(r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,e)},function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function o(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)});Object.defineProperty(e,"__esModule",{value:!0}),e.CommonSemanticsMixin=void 0,e.CommonSemanticsMixin=function(t){return function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype.computeBBox=function(t,e){if(void 0===e&&(e=!1),this.childNodes.length){var r=this.childNodes[0].getBBox(),o=r.w,n=r.h,i=r.d;t.w=o,t.h=n,t.d=i}},e}(t)}},8723:function(t,e){MathJax._.components.global.isObject,MathJax._.components.global.combineConfig,e.PV=MathJax._.components.global.combineDefaults,e.r8=MathJax._.components.global.combineWithMathJax,MathJax._.components.global.MathJax},4769:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.protoItem=MathJax._.core.MathItem.protoItem,e.AbstractMathItem=MathJax._.core.MathItem.AbstractMathItem,e.STATE=MathJax._.core.MathItem.STATE,e.newState=MathJax._.core.MathItem.newState},8921:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.TEXCLASS=MathJax._.core.MmlTree.MmlNode.TEXCLASS,e.TEXCLASSNAMES=MathJax._.core.MmlTree.MmlNode.TEXCLASSNAMES,e.indentAttributes=MathJax._.core.MmlTree.MmlNode.indentAttributes,e.AbstractMmlNode=MathJax._.core.MmlTree.MmlNode.AbstractMmlNode,e.AbstractMmlTokenNode=MathJax._.core.MmlTree.MmlNode.AbstractMmlTokenNode,e.AbstractMmlLayoutNode=MathJax._.core.MmlTree.MmlNode.AbstractMmlLayoutNode,e.AbstractMmlBaseNode=MathJax._.core.MmlTree.MmlNode.AbstractMmlBaseNode,e.AbstractMmlEmptyNode=MathJax._.core.MmlTree.MmlNode.AbstractMmlEmptyNode,e.TextNode=MathJax._.core.MmlTree.MmlNode.TextNode,e.XMLNode=MathJax._.core.MmlTree.MmlNode.XMLNode},4282:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.TeXAtom=MathJax._.core.MmlTree.MmlNodes.TeXAtom.TeXAtom},3969:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMaction=MathJax._.core.MmlTree.MmlNodes.maction.MmlMaction},304:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMath=MathJax._.core.MmlTree.MmlNodes.math.MmlMath},4374:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMenclose=MathJax._.core.MmlTree.MmlNodes.menclose.MmlMenclose},7451:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMfenced=MathJax._.core.MmlTree.MmlNodes.mfenced.MmlMfenced},848:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMfrac=MathJax._.core.MmlTree.MmlNodes.mfrac.MmlMfrac},910:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMglyph=MathJax._.core.MmlTree.MmlNodes.mglyph.MmlMglyph},7754:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMi=MathJax._.core.MmlTree.MmlNodes.mi.MmlMi},7764:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMmultiscripts=MathJax._.core.MmlTree.MmlNodes.mmultiscripts.MmlMmultiscripts,e.MmlMprescripts=MathJax._.core.MmlTree.MmlNodes.mmultiscripts.MmlMprescripts,e.MmlNone=MathJax._.core.MmlTree.MmlNodes.mmultiscripts.MmlNone},3235:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMn=MathJax._.core.MmlTree.MmlNodes.mn.MmlMn},9946:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMo=MathJax._.core.MmlTree.MmlNodes.mo.MmlMo},189:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMpadded=MathJax._.core.MmlTree.MmlNodes.mpadded.MmlMpadded},4664:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMroot=MathJax._.core.MmlTree.MmlNodes.mroot.MmlMroot},1691:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMrow=MathJax._.core.MmlTree.MmlNodes.mrow.MmlMrow,e.MmlInferredMrow=MathJax._.core.MmlTree.MmlNodes.mrow.MmlInferredMrow},4042:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMs=MathJax._.core.MmlTree.MmlNodes.ms.MmlMs},1465:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMspace=MathJax._.core.MmlTree.MmlNodes.mspace.MmlMspace},4655:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMsqrt=MathJax._.core.MmlTree.MmlNodes.msqrt.MmlMsqrt},5857:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMsubsup=MathJax._.core.MmlTree.MmlNodes.msubsup.MmlMsubsup,e.MmlMsub=MathJax._.core.MmlTree.MmlNodes.msubsup.MmlMsub,e.MmlMsup=MathJax._.core.MmlTree.MmlNodes.msubsup.MmlMsup},4859:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMtable=MathJax._.core.MmlTree.MmlNodes.mtable.MmlMtable},2321:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMtd=MathJax._.core.MmlTree.MmlNodes.mtd.MmlMtd},6277:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMtext=MathJax._.core.MmlTree.MmlNodes.mtext.MmlMtext},4393:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMtr=MathJax._.core.MmlTree.MmlNodes.mtr.MmlMtr,e.MmlMlabeledtr=MathJax._.core.MmlTree.MmlNodes.mtr.MmlMlabeledtr},3102:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlMunderover=MathJax._.core.MmlTree.MmlNodes.munderover.MmlMunderover,e.MmlMunder=MathJax._.core.MmlTree.MmlNodes.munderover.MmlMunder,e.MmlMover=MathJax._.core.MmlTree.MmlNodes.munderover.MmlMover},9167:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.MmlSemantics=MathJax._.core.MmlTree.MmlNodes.semantics.MmlSemantics,e.MmlAnnotationXML=MathJax._.core.MmlTree.MmlNodes.semantics.MmlAnnotationXML,e.MmlAnnotation=MathJax._.core.MmlTree.MmlNodes.semantics.MmlAnnotation},3985:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractOutputJax=MathJax._.core.OutputJax.AbstractOutputJax},9879:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractWrapper=MathJax._.core.Tree.Wrapper.AbstractWrapper},2506:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractWrapperFactory=MathJax._.core.Tree.WrapperFactory.AbstractWrapperFactory},3717:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.BBoxStyleAdjust=MathJax._.util.BBox.BBoxStyleAdjust,e.BBox=MathJax._.util.BBox.BBox},9077:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.APPEND=MathJax._.util.Options.APPEND,e.REMOVE=MathJax._.util.Options.REMOVE,e.Expandable=MathJax._.util.Options.Expandable,e.expandable=MathJax._.util.Options.expandable,e.makeArray=MathJax._.util.Options.makeArray,e.keys=MathJax._.util.Options.keys,e.copy=MathJax._.util.Options.copy,e.insert=MathJax._.util.Options.insert,e.defaultOptions=MathJax._.util.Options.defaultOptions,e.userOptions=MathJax._.util.Options.userOptions,e.selectOptions=MathJax._.util.Options.selectOptions,e.selectOptionsFromKeys=MathJax._.util.Options.selectOptionsFromKeys,e.separateOptions=MathJax._.util.Options.separateOptions},5888:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.CssStyles=MathJax._.util.StyleList.CssStyles},5878:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.Styles=MathJax._.util.Styles.Styles},6914:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.BIGDIMEN=MathJax._.util.lengths.BIGDIMEN,e.UNITS=MathJax._.util.lengths.UNITS,e.RELUNITS=MathJax._.util.lengths.RELUNITS,e.MATHSPACE=MathJax._.util.lengths.MATHSPACE,e.length2em=MathJax._.util.lengths.length2em,e.percent=MathJax._.util.lengths.percent,e.em=MathJax._.util.lengths.em,e.emRounded=MathJax._.util.lengths.emRounded,e.px=MathJax._.util.lengths.px},1490:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.sum=MathJax._.util.numeric.sum,e.max=MathJax._.util.numeric.max},6720:function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.sortLength=MathJax._.util.string.sortLength,e.quotePattern=MathJax._.util.string.quotePattern,e.unicodeChars=MathJax._.util.string.unicodeChars,e.unicodeString=MathJax._.util.string.unicodeString,e.isPercent=MathJax._.util.string.isPercent,e.split=MathJax._.util.string.split},4142:function(t,e,r){r.r(e),r.d(e,{TeXFont:function(){return c}});var o=r(2098);function n(t){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function a(t,e){return(a=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function s(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}();return function(){var r,o=h(t);if(e){var n=h(this).constructor;r=Reflect.construct(o,arguments,n)}else r=o.apply(this,arguments);return l(this,r)}}function l(t,e){return!e||"object"!==n(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function h(t){return(h=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}var c=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&a(t,e)}(r,t);var e=s(r);function r(){return i(this,r),e.apply(this,arguments)}return r}(o.FontData);c.OPTIONS={fontURL:"."}}},ut={};function pt(t){var e=ut[t];if(void 0!==e)return e.exports;var r=ut[t]={exports:{}};return ct[t].call(r.exports,r,r.exports,pt),r.exports}pt.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return pt.d(e,{a:e}),e},pt.d=function(t,e){for(var r in e)pt.o(e,r)&&!pt.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},pt.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},pt.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},t=pt(8723),e=pt(7016),r=pt(2098),o=pt(4458),n=pt(6617),i=pt(4477),a=pt(8369),s=pt(518),l=pt(1114),h=pt(7918),c=pt(4155),u=pt(3215),p=pt(7047),d=pt(7837),f=pt(1315),m=pt(3271),y=pt(1096),v=pt(7013),b=pt(3292),x=pt(7215),g=pt(7111),M=pt(3126),_=pt(9821),w=pt(6024),C=pt(5437),j=pt(513),S=pt(6918),O=pt(8709),T=pt(6359),L=pt(7500),B=pt(6577),P=pt(7322),H=pt(7795),A=pt(9250),k=pt(5373),N=pt(716),E=pt(1541),D=pt(1475),W=pt(3438),R=pt(555),I=pt(3345),F=pt(2057),J=pt(6200),V=pt(1346),z=pt(5705),X=pt(7969),K=pt(1419),q=pt(9906),U=pt(2304),Q=pt(437),G=pt(7481),Y=pt(5997),Z=pt(9323),$=pt(6920),tt=pt(37),et=pt(222),rt=pt(3069),ot=pt(8589),nt=pt(7805),it=pt(8325),at=pt(4818),st=pt(9690),lt=pt(7091),ht=pt(3191),(0,t.r8)({_:{output:{chtml_ts:e,chtml:{FontData:r,Notation:o,Wrapper:n,WrapperFactory:i,Wrappers_ts:a,Wrappers:{TeXAtom:s,TextNode:l,maction:h,math:c,menclose:u,mfenced:p,mfrac:d,mglyph:f,mi:m,mmultiscripts:y,mn:v,mo:b,mpadded:x,mroot:g,mrow:M,ms:_,mspace:w,msqrt:C,msubsup:j,mtable:S,mtd:O,mtext:T,mtr:L,munderover:B,scriptbase:P,semantics:H}},common:{FontData:A,Notation:k,OutputJax:N,Wrapper:E,WrapperFactory:D,Wrappers:{TeXAtom:W,TextNode:R,maction:I,math:F,menclose:J,mfenced:V,mfrac:z,mglyph:X,mi:K,mmultiscripts:q,mn:U,mo:Q,mpadded:G,mroot:Y,mrow:Z,ms:$,mspace:tt,msqrt:et,msubsup:rt,mtable:ot,mtd:nt,mtext:it,mtr:at,munderover:st,scriptbase:lt,semantics:ht}}}}}),MathJax.loader&&(0,t.PV)(MathJax.config.loader,"output/chtml",{checkReady:function(){return MathJax.loader.load("output/chtml/fonts/tex")}}),MathJax.startup&&(MathJax.startup.registerConstructor("chtml",e.CHTML),MathJax.startup.useOutput("chtml"))}(); \ No newline at end of file diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/tex.js b/docs/assets/vendor/mathjax/output/chtml/fonts/tex.js new file mode 100644 index 0000000..0a7a1fb --- /dev/null +++ b/docs/assets/vendor/mathjax/output/chtml/fonts/tex.js @@ -0,0 +1 @@ +!function(){"use strict";var c={2308:function(c,f,i){var t,e=this&&this.__extends||(t=function(c,f){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(c,f){c.__proto__=f}||function(c,f){for(var i in f)Object.prototype.hasOwnProperty.call(f,i)&&(c[i]=f[i])})(c,f)},function(c,f){if("function"!=typeof f&&null!==f)throw new TypeError("Class extends value "+String(f)+" is not a constructor or null");function i(){this.constructor=c}t(c,f),c.prototype=null===f?Object.create(f):(i.prototype=f.prototype,new i)}),s=this&&this.__assign||function(){return(s=Object.assign||function(c){for(var f,i=1,t=arguments.length;i\\338"},8816:{c:"\\2264\\338"},8817:{c:"\\2265\\338"},8832:{c:"\\227A\\338"},8833:{c:"\\227B\\338"},8836:{c:"\\2282\\338"},8837:{c:"\\2283\\338"},8840:{c:"\\2286\\338"},8841:{c:"\\2287\\338"},8876:{c:"\\22A2\\338"},8877:{c:"\\22A8\\338"},8930:{c:"\\2291\\338"},8931:{c:"\\2292\\338"},9001:{c:"\\27E8"},9002:{c:"\\27E9"},9653:{c:"\\25B3"},9663:{c:"\\25BD"},10072:{c:"\\2223"},10744:{c:"/",f:"BI"},10799:{c:"\\D7"},12296:{c:"\\27E8"},12297:{c:"\\27E9"}})},6051:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.doubleStruck=void 0;var t=i(5674);Object.defineProperty(f,"doubleStruck",{enumerable:!0,get:function(){return t.doubleStruck}})},9236:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.frakturBold=void 0;var t=i(73),e=i(7002);f.frakturBold=t.AddCSS(e.frakturBold,{8260:{c:"/"}})},1937:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.fraktur=void 0;var t=i(73),e=i(9349);f.fraktur=t.AddCSS(e.fraktur,{8260:{c:"/"}})},4244:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.italic=void 0;var t=i(73),e=i(9741);f.italic=t.AddCSS(e.italic,{47:{f:"I"},989:{c:"\\E008",f:"A"},8213:{c:"\\2014"},8215:{c:"_"},8260:{c:"/",f:"I"},8710:{c:"\\394",f:"I"},10744:{c:"/",f:"I"}})},482:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.largeop=void 0;var t=i(73),e=i(2827);f.largeop=t.AddCSS(e.largeop,{8214:{f:"S1"},8260:{c:"/"},8593:{f:"S1"},8595:{f:"S1"},8657:{f:"S1"},8659:{f:"S1"},8739:{f:"S1"},8741:{f:"S1"},9001:{c:"\\27E8"},9002:{c:"\\27E9"},9168:{f:"S1"},10072:{c:"\\2223",f:"S1"},10764:{c:"\\222C\\222C"},12296:{c:"\\27E8"},12297:{c:"\\27E9"}})},196:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.monospace=void 0;var t=i(73),e=i(2970);f.monospace=t.AddCSS(e.monospace,{697:{c:"\\2032"},913:{c:"A"},914:{c:"B"},917:{c:"E"},918:{c:"Z"},919:{c:"H"},921:{c:"I"},922:{c:"K"},924:{c:"M"},925:{c:"N"},927:{c:"O"},929:{c:"P"},932:{c:"T"},935:{c:"X"},8215:{c:"_"},8243:{c:"\\2032\\2032"},8244:{c:"\\2032\\2032\\2032"},8260:{c:"/"},8279:{c:"\\2032\\2032\\2032\\2032"},8710:{c:"\\394"}})},527:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.normal=void 0;var t=i(73),e=i(1668);f.normal=t.AddCSS(e.normal,{163:{f:"MI"},165:{f:"A"},174:{f:"A"},183:{c:"\\22C5"},240:{f:"A"},697:{c:"\\2032"},913:{c:"A"},914:{c:"B"},917:{c:"E"},918:{c:"Z"},919:{c:"H"},921:{c:"I"},922:{c:"K"},924:{c:"M"},925:{c:"N"},927:{c:"O"},929:{c:"P"},932:{c:"T"},935:{c:"X"},8192:{c:""},8193:{c:""},8194:{c:""},8195:{c:""},8196:{c:""},8197:{c:""},8198:{c:""},8201:{c:""},8202:{c:""},8203:{c:""},8204:{c:""},8213:{c:"\\2014"},8214:{c:"\\2225"},8215:{c:"_"},8226:{c:"\\2219"},8243:{c:"\\2032\\2032"},8244:{c:"\\2032\\2032\\2032"},8245:{f:"A"},8246:{c:"\\2035\\2035",f:"A"},8247:{c:"\\2035\\2035\\2035",f:"A"},8254:{c:"\\2C9"},8260:{c:"/"},8279:{c:"\\2032\\2032\\2032\\2032"},8288:{c:""},8289:{c:""},8290:{c:""},8291:{c:""},8292:{c:""},8407:{c:"\\2192",f:"V"},8450:{c:"C",f:"A"},8459:{c:"H",f:"SC"},8460:{c:"H",f:"FR"},8461:{c:"H",f:"A"},8462:{c:"h",f:"I"},8463:{f:"A"},8464:{c:"I",f:"SC"},8465:{c:"I",f:"FR"},8466:{c:"L",f:"SC"},8469:{c:"N",f:"A"},8473:{c:"P",f:"A"},8474:{c:"Q",f:"A"},8475:{c:"R",f:"SC"},8476:{c:"R",f:"FR"},8477:{c:"R",f:"A"},8484:{c:"Z",f:"A"},8486:{c:"\\3A9"},8487:{f:"A"},8488:{c:"Z",f:"FR"},8492:{c:"B",f:"SC"},8493:{c:"C",f:"FR"},8496:{c:"E",f:"SC"},8497:{c:"F",f:"SC"},8498:{f:"A"},8499:{c:"M",f:"SC"},8502:{f:"A"},8503:{f:"A"},8504:{f:"A"},8513:{f:"A"},8602:{f:"A"},8603:{f:"A"},8606:{f:"A"},8608:{f:"A"},8610:{f:"A"},8611:{f:"A"},8619:{f:"A"},8620:{f:"A"},8621:{f:"A"},8622:{f:"A"},8624:{f:"A"},8625:{f:"A"},8630:{f:"A"},8631:{f:"A"},8634:{f:"A"},8635:{f:"A"},8638:{f:"A"},8639:{f:"A"},8642:{f:"A"},8643:{f:"A"},8644:{f:"A"},8646:{f:"A"},8647:{f:"A"},8648:{f:"A"},8649:{f:"A"},8650:{f:"A"},8651:{f:"A"},8653:{f:"A"},8654:{f:"A"},8655:{f:"A"},8666:{f:"A"},8667:{f:"A"},8669:{f:"A"},8672:{f:"A"},8674:{f:"A"},8705:{f:"A"},8708:{c:"\\2203\\338"},8710:{c:"\\394"},8716:{c:"\\220B\\338"},8717:{f:"A"},8719:{f:"S1"},8720:{f:"S1"},8721:{f:"S1"},8724:{f:"A"},8737:{f:"A"},8738:{f:"A"},8740:{f:"A"},8742:{f:"A"},8748:{f:"S1"},8749:{f:"S1"},8750:{f:"S1"},8756:{f:"A"},8757:{f:"A"},8765:{f:"A"},8769:{f:"A"},8770:{f:"A"},8772:{c:"\\2243\\338"},8775:{c:"\\2246",f:"A"},8777:{c:"\\2248\\338"},8778:{f:"A"},8782:{f:"A"},8783:{f:"A"},8785:{f:"A"},8786:{f:"A"},8787:{f:"A"},8790:{f:"A"},8791:{f:"A"},8796:{f:"A"},8802:{c:"\\2261\\338"},8806:{f:"A"},8807:{f:"A"},8808:{f:"A"},8809:{f:"A"},8812:{f:"A"},8813:{c:"\\224D\\338"},8814:{f:"A"},8815:{f:"A"},8816:{f:"A"},8817:{f:"A"},8818:{f:"A"},8819:{f:"A"},8820:{c:"\\2272\\338"},8821:{c:"\\2273\\338"},8822:{f:"A"},8823:{f:"A"},8824:{c:"\\2276\\338"},8825:{c:"\\2277\\338"},8828:{f:"A"},8829:{f:"A"},8830:{f:"A"},8831:{f:"A"},8832:{f:"A"},8833:{f:"A"},8836:{c:"\\2282\\338"},8837:{c:"\\2283\\338"},8840:{f:"A"},8841:{f:"A"},8842:{f:"A"},8843:{f:"A"},8847:{f:"A"},8848:{f:"A"},8858:{f:"A"},8859:{f:"A"},8861:{f:"A"},8862:{f:"A"},8863:{f:"A"},8864:{f:"A"},8865:{f:"A"},8873:{f:"A"},8874:{f:"A"},8876:{f:"A"},8877:{f:"A"},8878:{f:"A"},8879:{f:"A"},8882:{f:"A"},8883:{f:"A"},8884:{f:"A"},8885:{f:"A"},8888:{f:"A"},8890:{f:"A"},8891:{f:"A"},8892:{f:"A"},8896:{f:"S1"},8897:{f:"S1"},8898:{f:"S1"},8899:{f:"S1"},8903:{f:"A"},8905:{f:"A"},8906:{f:"A"},8907:{f:"A"},8908:{f:"A"},8909:{f:"A"},8910:{f:"A"},8911:{f:"A"},8912:{f:"A"},8913:{f:"A"},8914:{f:"A"},8915:{f:"A"},8916:{f:"A"},8918:{f:"A"},8919:{f:"A"},8920:{f:"A"},8921:{f:"A"},8922:{f:"A"},8923:{f:"A"},8926:{f:"A"},8927:{f:"A"},8928:{f:"A"},8929:{f:"A"},8930:{c:"\\2291\\338"},8931:{c:"\\2292\\338"},8934:{f:"A"},8935:{f:"A"},8936:{f:"A"},8937:{f:"A"},8938:{f:"A"},8939:{f:"A"},8940:{f:"A"},8941:{f:"A"},8965:{c:"\\22BC",f:"A"},8966:{c:"\\2A5E",f:"A"},8988:{c:"\\250C",f:"A"},8989:{c:"\\2510",f:"A"},8990:{c:"\\2514",f:"A"},8991:{c:"\\2518",f:"A"},9001:{c:"\\27E8"},9002:{c:"\\27E9"},9168:{f:"S1"},9416:{f:"A"},9484:{f:"A"},9488:{f:"A"},9492:{f:"A"},9496:{f:"A"},9585:{f:"A"},9586:{f:"A"},9632:{f:"A"},9633:{f:"A"},9642:{c:"\\25A0",f:"A"},9650:{f:"A"},9652:{c:"\\25B2",f:"A"},9653:{c:"\\25B3"},9654:{f:"A"},9656:{c:"\\25B6",f:"A"},9660:{f:"A"},9662:{c:"\\25BC",f:"A"},9663:{c:"\\25BD"},9664:{f:"A"},9666:{c:"\\25C0",f:"A"},9674:{f:"A"},9723:{c:"\\25A1",f:"A"},9724:{c:"\\25A0",f:"A"},9733:{f:"A"},10003:{f:"A"},10016:{f:"A"},10072:{c:"\\2223"},10731:{f:"A"},10744:{c:"/",f:"I"},10752:{f:"S1"},10753:{f:"S1"},10754:{f:"S1"},10756:{f:"S1"},10758:{f:"S1"},10764:{c:"\\222C\\222C",f:"S1"},10799:{c:"\\D7"},10846:{f:"A"},10877:{f:"A"},10878:{f:"A"},10885:{f:"A"},10886:{f:"A"},10887:{f:"A"},10888:{f:"A"},10889:{f:"A"},10890:{f:"A"},10891:{f:"A"},10892:{f:"A"},10901:{f:"A"},10902:{f:"A"},10933:{f:"A"},10934:{f:"A"},10935:{f:"A"},10936:{f:"A"},10937:{f:"A"},10938:{f:"A"},10949:{f:"A"},10950:{f:"A"},10955:{f:"A"},10956:{f:"A"},12296:{c:"\\27E8"},12297:{c:"\\27E9"},57350:{f:"A"},57351:{f:"A"},57352:{f:"A"},57353:{f:"A"},57356:{f:"A"},57357:{f:"A"},57358:{f:"A"},57359:{f:"A"},57360:{f:"A"},57361:{f:"A"},57366:{f:"A"},57367:{f:"A"},57368:{f:"A"},57369:{f:"A"},57370:{f:"A"},57371:{f:"A"},119808:{c:"A",f:"B"},119809:{c:"B",f:"B"},119810:{c:"C",f:"B"},119811:{c:"D",f:"B"},119812:{c:"E",f:"B"},119813:{c:"F",f:"B"},119814:{c:"G",f:"B"},119815:{c:"H",f:"B"},119816:{c:"I",f:"B"},119817:{c:"J",f:"B"},119818:{c:"K",f:"B"},119819:{c:"L",f:"B"},119820:{c:"M",f:"B"},119821:{c:"N",f:"B"},119822:{c:"O",f:"B"},119823:{c:"P",f:"B"},119824:{c:"Q",f:"B"},119825:{c:"R",f:"B"},119826:{c:"S",f:"B"},119827:{c:"T",f:"B"},119828:{c:"U",f:"B"},119829:{c:"V",f:"B"},119830:{c:"W",f:"B"},119831:{c:"X",f:"B"},119832:{c:"Y",f:"B"},119833:{c:"Z",f:"B"},119834:{c:"a",f:"B"},119835:{c:"b",f:"B"},119836:{c:"c",f:"B"},119837:{c:"d",f:"B"},119838:{c:"e",f:"B"},119839:{c:"f",f:"B"},119840:{c:"g",f:"B"},119841:{c:"h",f:"B"},119842:{c:"i",f:"B"},119843:{c:"j",f:"B"},119844:{c:"k",f:"B"},119845:{c:"l",f:"B"},119846:{c:"m",f:"B"},119847:{c:"n",f:"B"},119848:{c:"o",f:"B"},119849:{c:"p",f:"B"},119850:{c:"q",f:"B"},119851:{c:"r",f:"B"},119852:{c:"s",f:"B"},119853:{c:"t",f:"B"},119854:{c:"u",f:"B"},119855:{c:"v",f:"B"},119856:{c:"w",f:"B"},119857:{c:"x",f:"B"},119858:{c:"y",f:"B"},119859:{c:"z",f:"B"},119860:{c:"A",f:"I"},119861:{c:"B",f:"I"},119862:{c:"C",f:"I"},119863:{c:"D",f:"I"},119864:{c:"E",f:"I"},119865:{c:"F",f:"I"},119866:{c:"G",f:"I"},119867:{c:"H",f:"I"},119868:{c:"I",f:"I"},119869:{c:"J",f:"I"},119870:{c:"K",f:"I"},119871:{c:"L",f:"I"},119872:{c:"M",f:"I"},119873:{c:"N",f:"I"},119874:{c:"O",f:"I"},119875:{c:"P",f:"I"},119876:{c:"Q",f:"I"},119877:{c:"R",f:"I"},119878:{c:"S",f:"I"},119879:{c:"T",f:"I"},119880:{c:"U",f:"I"},119881:{c:"V",f:"I"},119882:{c:"W",f:"I"},119883:{c:"X",f:"I"},119884:{c:"Y",f:"I"},119885:{c:"Z",f:"I"},119886:{c:"a",f:"I"},119887:{c:"b",f:"I"},119888:{c:"c",f:"I"},119889:{c:"d",f:"I"},119890:{c:"e",f:"I"},119891:{c:"f",f:"I"},119892:{c:"g",f:"I"},119894:{c:"i",f:"I"},119895:{c:"j",f:"I"},119896:{c:"k",f:"I"},119897:{c:"l",f:"I"},119898:{c:"m",f:"I"},119899:{c:"n",f:"I"},119900:{c:"o",f:"I"},119901:{c:"p",f:"I"},119902:{c:"q",f:"I"},119903:{c:"r",f:"I"},119904:{c:"s",f:"I"},119905:{c:"t",f:"I"},119906:{c:"u",f:"I"},119907:{c:"v",f:"I"},119908:{c:"w",f:"I"},119909:{c:"x",f:"I"},119910:{c:"y",f:"I"},119911:{c:"z",f:"I"},119912:{c:"A",f:"BI"},119913:{c:"B",f:"BI"},119914:{c:"C",f:"BI"},119915:{c:"D",f:"BI"},119916:{c:"E",f:"BI"},119917:{c:"F",f:"BI"},119918:{c:"G",f:"BI"},119919:{c:"H",f:"BI"},119920:{c:"I",f:"BI"},119921:{c:"J",f:"BI"},119922:{c:"K",f:"BI"},119923:{c:"L",f:"BI"},119924:{c:"M",f:"BI"},119925:{c:"N",f:"BI"},119926:{c:"O",f:"BI"},119927:{c:"P",f:"BI"},119928:{c:"Q",f:"BI"},119929:{c:"R",f:"BI"},119930:{c:"S",f:"BI"},119931:{c:"T",f:"BI"},119932:{c:"U",f:"BI"},119933:{c:"V",f:"BI"},119934:{c:"W",f:"BI"},119935:{c:"X",f:"BI"},119936:{c:"Y",f:"BI"},119937:{c:"Z",f:"BI"},119938:{c:"a",f:"BI"},119939:{c:"b",f:"BI"},119940:{c:"c",f:"BI"},119941:{c:"d",f:"BI"},119942:{c:"e",f:"BI"},119943:{c:"f",f:"BI"},119944:{c:"g",f:"BI"},119945:{c:"h",f:"BI"},119946:{c:"i",f:"BI"},119947:{c:"j",f:"BI"},119948:{c:"k",f:"BI"},119949:{c:"l",f:"BI"},119950:{c:"m",f:"BI"},119951:{c:"n",f:"BI"},119952:{c:"o",f:"BI"},119953:{c:"p",f:"BI"},119954:{c:"q",f:"BI"},119955:{c:"r",f:"BI"},119956:{c:"s",f:"BI"},119957:{c:"t",f:"BI"},119958:{c:"u",f:"BI"},119959:{c:"v",f:"BI"},119960:{c:"w",f:"BI"},119961:{c:"x",f:"BI"},119962:{c:"y",f:"BI"},119963:{c:"z",f:"BI"},119964:{c:"A",f:"SC"},119966:{c:"C",f:"SC"},119967:{c:"D",f:"SC"},119970:{c:"G",f:"SC"},119973:{c:"J",f:"SC"},119974:{c:"K",f:"SC"},119977:{c:"N",f:"SC"},119978:{c:"O",f:"SC"},119979:{c:"P",f:"SC"},119980:{c:"Q",f:"SC"},119982:{c:"S",f:"SC"},119983:{c:"T",f:"SC"},119984:{c:"U",f:"SC"},119985:{c:"V",f:"SC"},119986:{c:"W",f:"SC"},119987:{c:"X",f:"SC"},119988:{c:"Y",f:"SC"},119989:{c:"Z",f:"SC"},120068:{c:"A",f:"FR"},120069:{c:"B",f:"FR"},120071:{c:"D",f:"FR"},120072:{c:"E",f:"FR"},120073:{c:"F",f:"FR"},120074:{c:"G",f:"FR"},120077:{c:"J",f:"FR"},120078:{c:"K",f:"FR"},120079:{c:"L",f:"FR"},120080:{c:"M",f:"FR"},120081:{c:"N",f:"FR"},120082:{c:"O",f:"FR"},120083:{c:"P",f:"FR"},120084:{c:"Q",f:"FR"},120086:{c:"S",f:"FR"},120087:{c:"T",f:"FR"},120088:{c:"U",f:"FR"},120089:{c:"V",f:"FR"},120090:{c:"W",f:"FR"},120091:{c:"X",f:"FR"},120092:{c:"Y",f:"FR"},120094:{c:"a",f:"FR"},120095:{c:"b",f:"FR"},120096:{c:"c",f:"FR"},120097:{c:"d",f:"FR"},120098:{c:"e",f:"FR"},120099:{c:"f",f:"FR"},120100:{c:"g",f:"FR"},120101:{c:"h",f:"FR"},120102:{c:"i",f:"FR"},120103:{c:"j",f:"FR"},120104:{c:"k",f:"FR"},120105:{c:"l",f:"FR"},120106:{c:"m",f:"FR"},120107:{c:"n",f:"FR"},120108:{c:"o",f:"FR"},120109:{c:"p",f:"FR"},120110:{c:"q",f:"FR"},120111:{c:"r",f:"FR"},120112:{c:"s",f:"FR"},120113:{c:"t",f:"FR"},120114:{c:"u",f:"FR"},120115:{c:"v",f:"FR"},120116:{c:"w",f:"FR"},120117:{c:"x",f:"FR"},120118:{c:"y",f:"FR"},120119:{c:"z",f:"FR"},120120:{c:"A",f:"A"},120121:{c:"B",f:"A"},120123:{c:"D",f:"A"},120124:{c:"E",f:"A"},120125:{c:"F",f:"A"},120126:{c:"G",f:"A"},120128:{c:"I",f:"A"},120129:{c:"J",f:"A"},120130:{c:"K",f:"A"},120131:{c:"L",f:"A"},120132:{c:"M",f:"A"},120134:{c:"O",f:"A"},120138:{c:"S",f:"A"},120139:{c:"T",f:"A"},120140:{c:"U",f:"A"},120141:{c:"V",f:"A"},120142:{c:"W",f:"A"},120143:{c:"X",f:"A"},120144:{c:"Y",f:"A"},120172:{c:"A",f:"FRB"},120173:{c:"B",f:"FRB"},120174:{c:"C",f:"FRB"},120175:{c:"D",f:"FRB"},120176:{c:"E",f:"FRB"},120177:{c:"F",f:"FRB"},120178:{c:"G",f:"FRB"},120179:{c:"H",f:"FRB"},120180:{c:"I",f:"FRB"},120181:{c:"J",f:"FRB"},120182:{c:"K",f:"FRB"},120183:{c:"L",f:"FRB"},120184:{c:"M",f:"FRB"},120185:{c:"N",f:"FRB"},120186:{c:"O",f:"FRB"},120187:{c:"P",f:"FRB"},120188:{c:"Q",f:"FRB"},120189:{c:"R",f:"FRB"},120190:{c:"S",f:"FRB"},120191:{c:"T",f:"FRB"},120192:{c:"U",f:"FRB"},120193:{c:"V",f:"FRB"},120194:{c:"W",f:"FRB"},120195:{c:"X",f:"FRB"},120196:{c:"Y",f:"FRB"},120197:{c:"Z",f:"FRB"},120198:{c:"a",f:"FRB"},120199:{c:"b",f:"FRB"},120200:{c:"c",f:"FRB"},120201:{c:"d",f:"FRB"},120202:{c:"e",f:"FRB"},120203:{c:"f",f:"FRB"},120204:{c:"g",f:"FRB"},120205:{c:"h",f:"FRB"},120206:{c:"i",f:"FRB"},120207:{c:"j",f:"FRB"},120208:{c:"k",f:"FRB"},120209:{c:"l",f:"FRB"},120210:{c:"m",f:"FRB"},120211:{c:"n",f:"FRB"},120212:{c:"o",f:"FRB"},120213:{c:"p",f:"FRB"},120214:{c:"q",f:"FRB"},120215:{c:"r",f:"FRB"},120216:{c:"s",f:"FRB"},120217:{c:"t",f:"FRB"},120218:{c:"u",f:"FRB"},120219:{c:"v",f:"FRB"},120220:{c:"w",f:"FRB"},120221:{c:"x",f:"FRB"},120222:{c:"y",f:"FRB"},120223:{c:"z",f:"FRB"},120224:{c:"A",f:"SS"},120225:{c:"B",f:"SS"},120226:{c:"C",f:"SS"},120227:{c:"D",f:"SS"},120228:{c:"E",f:"SS"},120229:{c:"F",f:"SS"},120230:{c:"G",f:"SS"},120231:{c:"H",f:"SS"},120232:{c:"I",f:"SS"},120233:{c:"J",f:"SS"},120234:{c:"K",f:"SS"},120235:{c:"L",f:"SS"},120236:{c:"M",f:"SS"},120237:{c:"N",f:"SS"},120238:{c:"O",f:"SS"},120239:{c:"P",f:"SS"},120240:{c:"Q",f:"SS"},120241:{c:"R",f:"SS"},120242:{c:"S",f:"SS"},120243:{c:"T",f:"SS"},120244:{c:"U",f:"SS"},120245:{c:"V",f:"SS"},120246:{c:"W",f:"SS"},120247:{c:"X",f:"SS"},120248:{c:"Y",f:"SS"},120249:{c:"Z",f:"SS"},120250:{c:"a",f:"SS"},120251:{c:"b",f:"SS"},120252:{c:"c",f:"SS"},120253:{c:"d",f:"SS"},120254:{c:"e",f:"SS"},120255:{c:"f",f:"SS"},120256:{c:"g",f:"SS"},120257:{c:"h",f:"SS"},120258:{c:"i",f:"SS"},120259:{c:"j",f:"SS"},120260:{c:"k",f:"SS"},120261:{c:"l",f:"SS"},120262:{c:"m",f:"SS"},120263:{c:"n",f:"SS"},120264:{c:"o",f:"SS"},120265:{c:"p",f:"SS"},120266:{c:"q",f:"SS"},120267:{c:"r",f:"SS"},120268:{c:"s",f:"SS"},120269:{c:"t",f:"SS"},120270:{c:"u",f:"SS"},120271:{c:"v",f:"SS"},120272:{c:"w",f:"SS"},120273:{c:"x",f:"SS"},120274:{c:"y",f:"SS"},120275:{c:"z",f:"SS"},120276:{c:"A",f:"SSB"},120277:{c:"B",f:"SSB"},120278:{c:"C",f:"SSB"},120279:{c:"D",f:"SSB"},120280:{c:"E",f:"SSB"},120281:{c:"F",f:"SSB"},120282:{c:"G",f:"SSB"},120283:{c:"H",f:"SSB"},120284:{c:"I",f:"SSB"},120285:{c:"J",f:"SSB"},120286:{c:"K",f:"SSB"},120287:{c:"L",f:"SSB"},120288:{c:"M",f:"SSB"},120289:{c:"N",f:"SSB"},120290:{c:"O",f:"SSB"},120291:{c:"P",f:"SSB"},120292:{c:"Q",f:"SSB"},120293:{c:"R",f:"SSB"},120294:{c:"S",f:"SSB"},120295:{c:"T",f:"SSB"},120296:{c:"U",f:"SSB"},120297:{c:"V",f:"SSB"},120298:{c:"W",f:"SSB"},120299:{c:"X",f:"SSB"},120300:{c:"Y",f:"SSB"},120301:{c:"Z",f:"SSB"},120302:{c:"a",f:"SSB"},120303:{c:"b",f:"SSB"},120304:{c:"c",f:"SSB"},120305:{c:"d",f:"SSB"},120306:{c:"e",f:"SSB"},120307:{c:"f",f:"SSB"},120308:{c:"g",f:"SSB"},120309:{c:"h",f:"SSB"},120310:{c:"i",f:"SSB"},120311:{c:"j",f:"SSB"},120312:{c:"k",f:"SSB"},120313:{c:"l",f:"SSB"},120314:{c:"m",f:"SSB"},120315:{c:"n",f:"SSB"},120316:{c:"o",f:"SSB"},120317:{c:"p",f:"SSB"},120318:{c:"q",f:"SSB"},120319:{c:"r",f:"SSB"},120320:{c:"s",f:"SSB"},120321:{c:"t",f:"SSB"},120322:{c:"u",f:"SSB"},120323:{c:"v",f:"SSB"},120324:{c:"w",f:"SSB"},120325:{c:"x",f:"SSB"},120326:{c:"y",f:"SSB"},120327:{c:"z",f:"SSB"},120328:{c:"A",f:"SSI"},120329:{c:"B",f:"SSI"},120330:{c:"C",f:"SSI"},120331:{c:"D",f:"SSI"},120332:{c:"E",f:"SSI"},120333:{c:"F",f:"SSI"},120334:{c:"G",f:"SSI"},120335:{c:"H",f:"SSI"},120336:{c:"I",f:"SSI"},120337:{c:"J",f:"SSI"},120338:{c:"K",f:"SSI"},120339:{c:"L",f:"SSI"},120340:{c:"M",f:"SSI"},120341:{c:"N",f:"SSI"},120342:{c:"O",f:"SSI"},120343:{c:"P",f:"SSI"},120344:{c:"Q",f:"SSI"},120345:{c:"R",f:"SSI"},120346:{c:"S",f:"SSI"},120347:{c:"T",f:"SSI"},120348:{c:"U",f:"SSI"},120349:{c:"V",f:"SSI"},120350:{c:"W",f:"SSI"},120351:{c:"X",f:"SSI"},120352:{c:"Y",f:"SSI"},120353:{c:"Z",f:"SSI"},120354:{c:"a",f:"SSI"},120355:{c:"b",f:"SSI"},120356:{c:"c",f:"SSI"},120357:{c:"d",f:"SSI"},120358:{c:"e",f:"SSI"},120359:{c:"f",f:"SSI"},120360:{c:"g",f:"SSI"},120361:{c:"h",f:"SSI"},120362:{c:"i",f:"SSI"},120363:{c:"j",f:"SSI"},120364:{c:"k",f:"SSI"},120365:{c:"l",f:"SSI"},120366:{c:"m",f:"SSI"},120367:{c:"n",f:"SSI"},120368:{c:"o",f:"SSI"},120369:{c:"p",f:"SSI"},120370:{c:"q",f:"SSI"},120371:{c:"r",f:"SSI"},120372:{c:"s",f:"SSI"},120373:{c:"t",f:"SSI"},120374:{c:"u",f:"SSI"},120375:{c:"v",f:"SSI"},120376:{c:"w",f:"SSI"},120377:{c:"x",f:"SSI"},120378:{c:"y",f:"SSI"},120379:{c:"z",f:"SSI"},120432:{c:"A",f:"T"},120433:{c:"B",f:"T"},120434:{c:"C",f:"T"},120435:{c:"D",f:"T"},120436:{c:"E",f:"T"},120437:{c:"F",f:"T"},120438:{c:"G",f:"T"},120439:{c:"H",f:"T"},120440:{c:"I",f:"T"},120441:{c:"J",f:"T"},120442:{c:"K",f:"T"},120443:{c:"L",f:"T"},120444:{c:"M",f:"T"},120445:{c:"N",f:"T"},120446:{c:"O",f:"T"},120447:{c:"P",f:"T"},120448:{c:"Q",f:"T"},120449:{c:"R",f:"T"},120450:{c:"S",f:"T"},120451:{c:"T",f:"T"},120452:{c:"U",f:"T"},120453:{c:"V",f:"T"},120454:{c:"W",f:"T"},120455:{c:"X",f:"T"},120456:{c:"Y",f:"T"},120457:{c:"Z",f:"T"},120458:{c:"a",f:"T"},120459:{c:"b",f:"T"},120460:{c:"c",f:"T"},120461:{c:"d",f:"T"},120462:{c:"e",f:"T"},120463:{c:"f",f:"T"},120464:{c:"g",f:"T"},120465:{c:"h",f:"T"},120466:{c:"i",f:"T"},120467:{c:"j",f:"T"},120468:{c:"k",f:"T"},120469:{c:"l",f:"T"},120470:{c:"m",f:"T"},120471:{c:"n",f:"T"},120472:{c:"o",f:"T"},120473:{c:"p",f:"T"},120474:{c:"q",f:"T"},120475:{c:"r",f:"T"},120476:{c:"s",f:"T"},120477:{c:"t",f:"T"},120478:{c:"u",f:"T"},120479:{c:"v",f:"T"},120480:{c:"w",f:"T"},120481:{c:"x",f:"T"},120482:{c:"y",f:"T"},120483:{c:"z",f:"T"},120488:{c:"A",f:"B"},120489:{c:"B",f:"B"},120490:{c:"\\393",f:"B"},120491:{c:"\\394",f:"B"},120492:{c:"E",f:"B"},120493:{c:"Z",f:"B"},120494:{c:"H",f:"B"},120495:{c:"\\398",f:"B"},120496:{c:"I",f:"B"},120497:{c:"K",f:"B"},120498:{c:"\\39B",f:"B"},120499:{c:"M",f:"B"},120500:{c:"N",f:"B"},120501:{c:"\\39E",f:"B"},120502:{c:"O",f:"B"},120503:{c:"\\3A0",f:"B"},120504:{c:"P",f:"B"},120506:{c:"\\3A3",f:"B"},120507:{c:"T",f:"B"},120508:{c:"\\3A5",f:"B"},120509:{c:"\\3A6",f:"B"},120510:{c:"X",f:"B"},120511:{c:"\\3A8",f:"B"},120512:{c:"\\3A9",f:"B"},120513:{c:"\\2207",f:"B"},120546:{c:"A",f:"I"},120547:{c:"B",f:"I"},120548:{c:"\\393",f:"I"},120549:{c:"\\394",f:"I"},120550:{c:"E",f:"I"},120551:{c:"Z",f:"I"},120552:{c:"H",f:"I"},120553:{c:"\\398",f:"I"},120554:{c:"I",f:"I"},120555:{c:"K",f:"I"},120556:{c:"\\39B",f:"I"},120557:{c:"M",f:"I"},120558:{c:"N",f:"I"},120559:{c:"\\39E",f:"I"},120560:{c:"O",f:"I"},120561:{c:"\\3A0",f:"I"},120562:{c:"P",f:"I"},120564:{c:"\\3A3",f:"I"},120565:{c:"T",f:"I"},120566:{c:"\\3A5",f:"I"},120567:{c:"\\3A6",f:"I"},120568:{c:"X",f:"I"},120569:{c:"\\3A8",f:"I"},120570:{c:"\\3A9",f:"I"},120572:{c:"\\3B1",f:"I"},120573:{c:"\\3B2",f:"I"},120574:{c:"\\3B3",f:"I"},120575:{c:"\\3B4",f:"I"},120576:{c:"\\3B5",f:"I"},120577:{c:"\\3B6",f:"I"},120578:{c:"\\3B7",f:"I"},120579:{c:"\\3B8",f:"I"},120580:{c:"\\3B9",f:"I"},120581:{c:"\\3BA",f:"I"},120582:{c:"\\3BB",f:"I"},120583:{c:"\\3BC",f:"I"},120584:{c:"\\3BD",f:"I"},120585:{c:"\\3BE",f:"I"},120586:{c:"\\3BF",f:"I"},120587:{c:"\\3C0",f:"I"},120588:{c:"\\3C1",f:"I"},120589:{c:"\\3C2",f:"I"},120590:{c:"\\3C3",f:"I"},120591:{c:"\\3C4",f:"I"},120592:{c:"\\3C5",f:"I"},120593:{c:"\\3C6",f:"I"},120594:{c:"\\3C7",f:"I"},120595:{c:"\\3C8",f:"I"},120596:{c:"\\3C9",f:"I"},120597:{c:"\\2202"},120598:{c:"\\3F5",f:"I"},120599:{c:"\\3D1",f:"I"},120600:{c:"\\E009",f:"A"},120601:{c:"\\3D5",f:"I"},120602:{c:"\\3F1",f:"I"},120603:{c:"\\3D6",f:"I"},120604:{c:"A",f:"BI"},120605:{c:"B",f:"BI"},120606:{c:"\\393",f:"BI"},120607:{c:"\\394",f:"BI"},120608:{c:"E",f:"BI"},120609:{c:"Z",f:"BI"},120610:{c:"H",f:"BI"},120611:{c:"\\398",f:"BI"},120612:{c:"I",f:"BI"},120613:{c:"K",f:"BI"},120614:{c:"\\39B",f:"BI"},120615:{c:"M",f:"BI"},120616:{c:"N",f:"BI"},120617:{c:"\\39E",f:"BI"},120618:{c:"O",f:"BI"},120619:{c:"\\3A0",f:"BI"},120620:{c:"P",f:"BI"},120622:{c:"\\3A3",f:"BI"},120623:{c:"T",f:"BI"},120624:{c:"\\3A5",f:"BI"},120625:{c:"\\3A6",f:"BI"},120626:{c:"X",f:"BI"},120627:{c:"\\3A8",f:"BI"},120628:{c:"\\3A9",f:"BI"},120630:{c:"\\3B1",f:"BI"},120631:{c:"\\3B2",f:"BI"},120632:{c:"\\3B3",f:"BI"},120633:{c:"\\3B4",f:"BI"},120634:{c:"\\3B5",f:"BI"},120635:{c:"\\3B6",f:"BI"},120636:{c:"\\3B7",f:"BI"},120637:{c:"\\3B8",f:"BI"},120638:{c:"\\3B9",f:"BI"},120639:{c:"\\3BA",f:"BI"},120640:{c:"\\3BB",f:"BI"},120641:{c:"\\3BC",f:"BI"},120642:{c:"\\3BD",f:"BI"},120643:{c:"\\3BE",f:"BI"},120644:{c:"\\3BF",f:"BI"},120645:{c:"\\3C0",f:"BI"},120646:{c:"\\3C1",f:"BI"},120647:{c:"\\3C2",f:"BI"},120648:{c:"\\3C3",f:"BI"},120649:{c:"\\3C4",f:"BI"},120650:{c:"\\3C5",f:"BI"},120651:{c:"\\3C6",f:"BI"},120652:{c:"\\3C7",f:"BI"},120653:{c:"\\3C8",f:"BI"},120654:{c:"\\3C9",f:"BI"},120655:{c:"\\2202",f:"B"},120656:{c:"\\3F5",f:"BI"},120657:{c:"\\3D1",f:"BI"},120658:{c:"\\E009",f:"A"},120659:{c:"\\3D5",f:"BI"},120660:{c:"\\3F1",f:"BI"},120661:{c:"\\3D6",f:"BI"},120662:{c:"A",f:"SSB"},120663:{c:"B",f:"SSB"},120664:{c:"\\393",f:"SSB"},120665:{c:"\\394",f:"SSB"},120666:{c:"E",f:"SSB"},120667:{c:"Z",f:"SSB"},120668:{c:"H",f:"SSB"},120669:{c:"\\398",f:"SSB"},120670:{c:"I",f:"SSB"},120671:{c:"K",f:"SSB"},120672:{c:"\\39B",f:"SSB"},120673:{c:"M",f:"SSB"},120674:{c:"N",f:"SSB"},120675:{c:"\\39E",f:"SSB"},120676:{c:"O",f:"SSB"},120677:{c:"\\3A0",f:"SSB"},120678:{c:"P",f:"SSB"},120680:{c:"\\3A3",f:"SSB"},120681:{c:"T",f:"SSB"},120682:{c:"\\3A5",f:"SSB"},120683:{c:"\\3A6",f:"SSB"},120684:{c:"X",f:"SSB"},120685:{c:"\\3A8",f:"SSB"},120686:{c:"\\3A9",f:"SSB"},120782:{c:"0",f:"B"},120783:{c:"1",f:"B"},120784:{c:"2",f:"B"},120785:{c:"3",f:"B"},120786:{c:"4",f:"B"},120787:{c:"5",f:"B"},120788:{c:"6",f:"B"},120789:{c:"7",f:"B"},120790:{c:"8",f:"B"},120791:{c:"9",f:"B"},120802:{c:"0",f:"SS"},120803:{c:"1",f:"SS"},120804:{c:"2",f:"SS"},120805:{c:"3",f:"SS"},120806:{c:"4",f:"SS"},120807:{c:"5",f:"SS"},120808:{c:"6",f:"SS"},120809:{c:"7",f:"SS"},120810:{c:"8",f:"SS"},120811:{c:"9",f:"SS"},120812:{c:"0",f:"SSB"},120813:{c:"1",f:"SSB"},120814:{c:"2",f:"SSB"},120815:{c:"3",f:"SSB"},120816:{c:"4",f:"SSB"},120817:{c:"5",f:"SSB"},120818:{c:"6",f:"SSB"},120819:{c:"7",f:"SSB"},120820:{c:"8",f:"SSB"},120821:{c:"9",f:"SSB"},120822:{c:"0",f:"T"},120823:{c:"1",f:"T"},120824:{c:"2",f:"T"},120825:{c:"3",f:"T"},120826:{c:"4",f:"T"},120827:{c:"5",f:"T"},120828:{c:"6",f:"T"},120829:{c:"7",f:"T"},120830:{c:"8",f:"T"},120831:{c:"9",f:"T"}})},3518:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.sansSerifBoldItalic=void 0;var t=i(73),e=i(6949);f.sansSerifBoldItalic=t.AddCSS(e.sansSerifBoldItalic,{305:{f:"SSB"},567:{f:"SSB"}})},965:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.sansSerifBold=void 0;var t=i(73),e=i(5193);f.sansSerifBold=t.AddCSS(e.sansSerifBold,{8213:{c:"\\2014"},8215:{c:"_"},8260:{c:"/"},8710:{c:"\\394"}})},9169:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.sansSerifItalic=void 0;var t=i(73),e=i(2632);f.sansSerifItalic=t.AddCSS(e.sansSerifItalic,{913:{c:"A"},914:{c:"B"},917:{c:"E"},918:{c:"Z"},919:{c:"H"},921:{c:"I"},922:{c:"K"},924:{c:"M"},925:{c:"N"},927:{c:"O"},929:{c:"P"},932:{c:"T"},935:{c:"X"},8213:{c:"\\2014"},8215:{c:"_"},8260:{c:"/"},8710:{c:"\\394"}})},6736:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.sansSerif=void 0;var t=i(73),e=i(4214);f.sansSerif=t.AddCSS(e.sansSerif,{913:{c:"A"},914:{c:"B"},917:{c:"E"},918:{c:"Z"},919:{c:"H"},921:{c:"I"},922:{c:"K"},924:{c:"M"},925:{c:"N"},927:{c:"O"},929:{c:"P"},932:{c:"T"},935:{c:"X"},8213:{c:"\\2014"},8215:{c:"_"},8260:{c:"/"},8710:{c:"\\394"}})},2290:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.scriptBold=void 0;var t=i(6466);Object.defineProperty(f,"scriptBold",{enumerable:!0,get:function(){return t.scriptBold}})},3012:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.script=void 0;var t=i(3776);Object.defineProperty(f,"script",{enumerable:!0,get:function(){return t.script}})},8787:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.smallop=void 0;var t=i(73),e=i(7405);f.smallop=t.AddCSS(e.smallop,{8260:{c:"/"},9001:{c:"\\27E8"},9002:{c:"\\27E9"},10072:{c:"\\2223"},10764:{c:"\\222C\\222C"},12296:{c:"\\27E8"},12297:{c:"\\27E9"}})},5392:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.texCalligraphicBold=void 0;var t=i(73),e=i(8105);f.texCalligraphicBold=t.AddCSS(e.texCalligraphicBold,{305:{f:"B"},567:{f:"B"}})},6318:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.texCalligraphic=void 0;var t=i(2518);Object.defineProperty(f,"texCalligraphic",{enumerable:!0,get:function(){return t.texCalligraphic}})},5351:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.texMathit=void 0;var t=i(5595);Object.defineProperty(f,"texMathit",{enumerable:!0,get:function(){return t.texMathit}})},873:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.texOldstyleBold=void 0;var t=i(6357);Object.defineProperty(f,"texOldstyleBold",{enumerable:!0,get:function(){return t.texOldstyleBold}})},7611:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.texOldstyle=void 0;var t=i(9474);Object.defineProperty(f,"texOldstyle",{enumerable:!0,get:function(){return t.texOldstyle}})},6590:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.texSize3=void 0;var t=i(73),e=i(584);f.texSize3=t.AddCSS(e.texSize3,{8260:{c:"/"},9001:{c:"\\27E8"},9002:{c:"\\27E9"},12296:{c:"\\27E8"},12297:{c:"\\27E9"}})},8798:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.texSize4=void 0;var t=i(73),e=i(4324);f.texSize4=t.AddCSS(e.texSize4,{8260:{c:"/"},9001:{c:"\\27E8"},9002:{c:"\\27E9"},12296:{c:"\\27E8"},12297:{c:"\\27E9"},57685:{c:"\\E153\\E152"},57686:{c:"\\E151\\E150"}})},2138:function(c,f,i){Object.defineProperty(f,"__esModule",{value:!0}),f.texVariant=void 0;var t=i(73),e=i(8135);f.texVariant=t.AddCSS(e.texVariant,{1008:{c:"\\E009"},8463:{f:""},8740:{c:"\\E006"},8742:{c:"\\E007"},8808:{c:"\\E00C"},8809:{c:"\\E00D"},8816:{c:"\\E011"},8817:{c:"\\E00E"},8840:{c:"\\E016"},8841:{c:"\\E018"},8842:{c:"\\E01A"},8843:{c:"\\E01B"},10887:{c:"\\E010"},10888:{c:"\\E00F"},10955:{c:"\\E017"},10956:{c:"\\E019"}})},2176:function(c,f){var i,t=this&&this.__extends||(i=function(c,f){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(c,f){c.__proto__=f}||function(c,f){for(var i in f)Object.prototype.hasOwnProperty.call(f,i)&&(c[i]=f[i])})(c,f)},function(c,f){if("function"!=typeof f&&null!==f)throw new TypeError("Class extends value "+String(f)+" is not a constructor or null");function t(){this.constructor=c}i(c,f),c.prototype=null===f?Object.create(f):(t.prototype=f.prototype,new t)}),e=this&&this.__assign||function(){return(e=Object.assign||function(c){for(var f,i=1,t=arguments.length;i0)&&!(t=s.next()).done;)r.push(t.value)}catch(c){e={error:c}}finally{try{t&&!t.done&&(i=s.return)&&i.call(s)}finally{if(e)throw e.error}}return r},r=this&&this.__spreadArray||function(c,f){for(var i=0,t=f.length,e=c.length;i%*?_q#ebr>oXftyQafy85bTs@)aD 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 literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..57819c51537046bcb02f0a05f62cb681b093c79b GIT binary patch literal 9908 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 D{&5N{ literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..eb66c4617f4772e838453ff7403b87cc0c51b86f GIT binary patch literal 9600 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 D9aAb` literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..f5df02348b3ad03c4828e77e172cd1cee1bef4dc GIT binary patch literal 22340 zcmZr%V{j+D*RAb#Yg=2}wry{1+qP}n`qj2=duzM@ecsP+W=>|#nVUQLlH^Wu;wCR9 zCa*l;pa5iRX!GZv<_rV`()CX^<--?&vbS^k$JH7B#i0HJ;ZG!IHOlFt*UjdKYo}i?)x3=Qm6lhH-F~Z^SJ!iJ4<7N^h26@MZehvXR|lY@3@9yiyb?_zpsUbvYv|#6 z?2g*yGb8U&f0&-^DVx{%?#^uM`R1L5-`(6%ssqS;K`iK$zLlQvv#MM876U|`!cKgr zi!xQQtHhQ}SVV>GMrGM~?NRx7?iaIczwhlQ)57w6J=&ip2WNYDw!V+|5uWP3-v;&Z z@!#(2KMQ(%Ck@24JHI0d*mT{7f3t5pkHWWYIX}p%^K3CK&98V3ca+Aa?v$ny=-;{9 zEy{oQUf=%QtylK+=jv`_e^$=x6l)e%EH+#96aqvZRHsWa>nu_aHam14Tx7e&s@_7y z!tCy}GwrOtg&**y-B@=QS|GytcVu)D?EjovBFR3Cl1r7nZGhASZN&A z)U0wBs^_bhs~4+Rt>>(ltQV|Tcxu)Q*R_PL@aOWeOG9h+E%QU`kS&L>E5vG{&0Q;- zZhd$#)rxeEy3hI&aZ-oq_1qi+1csi5jDvexd)O4+P~Ty&e->85>c zrdPGnc0AWVSHu45zmBAEeQU4!c^zu4ae2G}oRl*Tye>-G6VQc7C!TgCTFc%{PUAp2D2~ zuV)>jLUDGU2Ol)Ptq1J#_!^vD?^*J?ZKZ}+tE-l)u5~_twg=wtA^wik%5LWNIKf&| z*>QFpAJgoaA14&Cir-m{&)ZS2s*hByuHy6BE$qwB@+-AHMwalU1ybK+T=XjRD0e^J z#QkZhqtfSAFIwyYx?(=h#%x3i;(@A``EyCcn=h7j zeEw$x5%*Mo^(ppgYE~w-tgJJ16NigClq>CbcjbM3Vr4|v!vMFmT()+>1L*V{CpKlM zp)Q(i0CJ`Wl?z$qHhLI`zS2sc#Q=^gxQ##G`D#EO?4QbAk@3pVJrE@G@Vf^2@Y@fe z&{-j9e>hK2y&VLI`q#eLRJFp^-Jj3)`wc-dM&(}p3Ys5DfDqA+hxA{rm(+VeEQY*L zP;V+2w?@16nb+EAK2|Pz2k@H*CNW&!Eqr2q+H4<4+RO3OVCWi<^B}siwmyLo*g&#v z-g%xmPbGo=2R>8Gv}r{5n@M?k_U}JTcaAI+>Xq)VK>cMXU-U8YXQOe0hLl$hE@VMb1lsMz4=Ft~7JQjY)*}?t! zx5Yc}YD_vvB}WJmq)?MR4a^R|RUpSCxeW+(hKZ4SlDRA+@ICNPEsplnU+$2OOHhd6 zX@o0C;KzUAzT+PFRVj1o5ZK_ZEm_^ie7S*W+8><9*p9OTpq-mp+)O+hi@FQ2tG<;X z!KqMZ#GE2Ti16b!*DwcEZo@F8vei6RGE>clV+Aflu1_#(&w?{-%{A^w@hZ z3TCKctY<%=;$Fm)?aur9YOiiT?-u$k@+qbtkz3!#yO(SSqeUWoA8XHj2;@|^wYjZP zu%HSN=Woj2kq_pLGC37*E_|T?2?kU@UwTB2K3_t^@a-Y3cs26Tzn|bIgvkoca-Ga_ z_%^@S6#2}ek>54^7SP5H@emi zYZv*3wL>TZgak5Bg89y2vXaKI?h?Qx-|ng|(;H*g6b8gOwysVD%N9?IZ-WJ=#@K0V zWT+lktG~Nv%*a3SH(>qSU-szv@Fy{i5u8?#PKsHah;b>)L9-AeK$@U`nbLh=S;RPQO8 zg)za~GG=hlS@t*4_8rO8GPLpQ#fu3H08srIF!n*5XH*$txfXMw$cVLy-C-aM`=Ej( z@S45kr;B^6q~~PN$;&>* zu&_TNI`>_FlxG?4$62K+>{otLyrGSD9D*bxZk> zVU5b8_WEuR;ncW0!@u6#YU#AtT*+AP; z$<^l3;7ZMelN}_@zT#ToT4mly+0J#8_##3_@=tSJG+*uRV2`=kP3sZAy|wkGme zwAt*CtttSA7#LAGGv>ij&Igq{ z14r01Hp|R(4Ti7(^|acL?aJ%H+Jx5^<^-|^d=>&3ECv+&;E#Gvv%^IvEdVC2CorJv zB7cxk=)r%JhLw@Rz~h53vHFVj=3&TlTOCeiEC&=|^@^oJaQg(JMeXf6O^~AhQ>f>m z_mq>YaO4=)G0lFK;FjnEw>(6kRTPXdAVZ?UCI? z|HV=+fKL@}s+-)}?R0Yb395(CD(rL|``yVKti{WbdRfjZhKIv*%>|7$cCV=jV5!l$ z$5H5kw;(^1Cmzfg#ju4-;Hmg#qLY&YgEPPtQ_aONU~{yzZ4Ynuqe(6@=0J9o8AxK8 zX+_}2yT2pP<{MM&od0R|unNm+=I$n?PLG&>)M+FP1DlFw0N3=}$ZQ&#nv1plHvC|P zm);MVaLgX~DbeW>d9!k9k_%4n;%EK!TbyAndeN>ASIS!f1E1n3IeE_`SiIG^4d)et zr%=*2uJ}_!g)Yp;ygK&R7$rUv=9!?UKieFKQ+sFKx8f6%icfuInBK9C{}b3_AKgrN zyQz>t5GkFO+i|fR$VmHTKbI{t&&X50i)xYzhH6JLZ$fCva?8WSsCbIix5~pj<8Z!C zr$__MUl%{V`sdUOvX?E@uZza*Iox&5XltUpqA((zq9X7#A*?)e=x1siHUMXp1_(TMUUv3tcXc*>>vY6r8+xN51?sCl#%8Z$McN+Yvx z`{)-ZshtEL{lDc^>4T`{j?Y@H7q>v`?0$ZA!QM>se9&s?Oc4YM+m~M1PfEzwa^JI{ z6R_RZ#~itCw)!4D2FUC+Lf`ezjV9gTa&s~E)UgoEj3MKLf&~*E$1x+5OBICkkDv-> zg#FM7^jWZ}Mry^qs@$_o`IG$@UK zKE2=XzUAiVX1qfR9n~r+J#ju((jBO&j!$V3fPx#X_f_ex?C73Q^P_NwYH4>E13<|b zkE;d}#u>$968Tnrev%xQ_R{dDZ~bi5iI*ubzzsj}AVt^xQ1}Cn`kifwivLBVbU~z& zPTq!bsA>e0o-#gdJ-3k?AKO&gb{!Q*9{m_I@rwf{@r9ww@lnhcgTjCB<8ib%hY)< z@D0M@Bo^`Vpe&<*mT^ydg-Zh5CCG62hak!I5T+JUKHOmE;L9|UMUSp?Y#m3!wTLes=i?}C?vSAH<`jaLF4 zhMpHTndq3)SBH5qOmT2dH0@k!z-BR2Jw_L8d&PV>AoTJ+3}m%q*N%(`-chx%(o^s1 zI(FQH>e@LN_|9J5E?G+#0Bp*8s|0JQJ7yXUjb?KnbwG#Ofv_A95QjE^U9h}5{?({j zZyFUf{3YIYlT{n26D28lTvESFVSD;!GkN5SsN+4PZo}ZQHdzebP)4B%(Bhv(h*abT zI5MpXENt?tPF>_yBJAL4j6(mI6HM>jJAzUufuK5uY&UU>^qvjvo@(~(fO=<#m3*oR zH;Kkc*rmfM2jjv?%nx09`_eLE&x9*Z;iJw(1VAs=Q=IMH#n-QiNi(u-cb^(p`^ixe zoY%IKI^umdJi3Gx*63KM`1bVr?r*R3e(C_8F(yPe;*v|9c2{}b>^Cp|)US6`)ouQD z^_q^wFPdNBT&tm=TDWRmM4;VtOOs9FW6>p?I44vqPNxJHvmeg3(0M~W)?p0F<(EKX z8n#xD6LSLC^XSLQ8}-HySz|VKHJcu&xnrs!*jfo~%s!ezJ}b>(H>L5K)5#wdo-@oS z8;EGNnvR1N&s|Q9H9%bXsnhfQudW;>N@_~-A?d_%b$)iH)z)&z?R*{1Y?ux4 zqkf@&ii)%y2Vn58B+SKj{{o!}v_X5qJ5B@(UMGet;&r)I69DMCqxXL{7~2NqgGv3W z*U4&VAlL?^0LAk4qP-(wO7PGB(OYhq4BbL5bZVwq#d8|BogXKOQIqV%^e$hN99O%$6UuqO{o0k?Cq{|( zg#6+3FfK$$92|mVS_G%r7@Z;hWXwx>p?0xDuD9Xl4IzEbUbTu!jA05+gs)QjF45xj z4Y%@FdX{(TD}YHy;hK$V*S+E7v8>Rsp-{D_0(Wr8r}km%s{{V%>*~ekRwr7uUIwwXVA{*QyaA+kN%8Uyo1&dvC<{Iv`Z z7-Hb0ZjE7gIN6u&#Hqs>e9#I0VEo}mi|jiB^C(d&ELZ&oiljBClj6bNnD@P^LWpK` z&daQayr!JSv%Bf%A1;t01_Td76)~ziH;RbCL$3)QecWmMups+6PF98$b^w`=Hkcye zgNTGeCZBrHNNEG&aJpb4IyMOqou_wAKG`wd$4G+NPTI?`4xmQ>C*M*!Wuc#idxHF~ zE0QOVN+udTaWKLgNSY;F30Z!Nio58qf_u_g)(S~24F53)=ZkBQW3{IaWKQw8Z4qVY z9iwlk2?Jq0pDJvSZau6|BVRpTP-W(MO~MDr*dUF%4>?7+1Y7#wkPEcwPE?1las$@i-M*&JU&mgnl)J-`4^GFuE?e=!yLK$=)way3mBgs| ziovv@;k)uY{HZjhO#vWN2ON+NrI7(OqPwAFc&puB5kUFc^9&;wWsmdNGZt;b0JirvVZu0^<00r5f)k1 zYiz&z^oE`7n(qegkBn@3=)X1$yz^?$Y~2mZ9a~}s1ai<7h2L%(4+YEvi0kz7gMZ&# z_!cg%92RpT8lO((hw=3j@}lYUbzY5)n7!|WBJQ^pnIWVp5T~b-}cua zSi=xCr9&Z;MBH7P7bM3GgK`p)mb({Tl&7=*m2#Oqo2*645U>WyLcfF?HXy`}j)^wF zbd;i-Kcv|Bz1jH@8Y*)lxDhxE5;Q(Y;zjbzV489;C!)T9R!`hthXQt{tCj_6kt6m3 zapNh_ex@@N3E%0~W+P~5H zW`QY4@YjiP$gQ&xxw1^!LWK&)ukZ*%9>Dr~06Ss| zGCq78YTqzB`y!YT9g1)K*Mq38h5S7GK=4}rMI{3s@IZ7Q*BQ>o^6#JSsKL&n)pJ9QY*)QOwNo~CFpS_|r!S}>@LsKj;YB=1FJ1M=-93JNhUuuKgA#x{ zW=hmoN4Vij%q`1pAx}=8U3>5?Kl^Da=@nF`Kisie@*t(d^0{6&*4SyDvQL@3QqC$K ztK~Y6Vc5rx#|I0ie(IKKN67@|?od#I4jkEu2ONw{Gya-{j3F-j?1LNSYM*u_9sA~g z^|8eWM=*{D_l-k>lFCjH@cds0WkkaOC{YKv z$f=XVI^=mi&l!|-X;A$KJj0AFoe_LN%Qg=lpQO?U2MQR#v_@{tR9|!Lj7xaysTT;& zz*@jr@Htmkc<;KcE#a=Q8c^1>WJE~(={Z!#$_>`$U?=+p*cZN0m$y_2)dD3oO;T+$=q+H@dB(x2W}X9bhD?j z3u;#OVnXZf7YqthCQ4ebKLhlyqAo-;vz}+VspP_&NRDqC#{BC-I^BGP9+#nVr`Q?@ z%hLMQE8o6{=O^k zZ0+~oDXQ?XO~CTIMFlA((@|?Lk8$mXn|he;u)d&n7|%JGF=5gPgrD-9@Bj%0l1hXv zo?jM%8e?t;f}hUMgGBP7%rO3McW1fq6~uy*7A&wdVt&m`CCZ>`8us zz)5`FN*tD8Oz5I1bI}e0v0XL`y%8^Rf)Oy#ajbkvJSGg{=SGHp^%vdaeN{wy=~?-7N1gozMup4bDv z1B0I~pI9|kOCU3Mx$p?$JZXGgfw~-G_uOg2p`)+p+1<5G8}>PtGO_6>-B$4h$gIeq z!Ej_O;8VtF6xzQ}ALUOuU!^OpsVn_-X8aarI+#=Tzk~EQGHlmhIJCTpx3SJnsXmcI zPIvZL44Iv+Z%xQ7c=DH))N9pxTuTa;?_uT!pGi1uw}(1jqHB#PeEGu>{9eOot;}5GI{)) z{a}6#!$lXk<4x6z-ayl)b$|B$@6puQQ@#je5P4Ke91{V$Y1%w6mUKwva+Ipp} z28#;1c@AC*Y~V47LmarS!<0k5cLgC7e>QlQ*ov>tq^@kcW(^N5NPN&;oklZ&vDL%S zRdCOxttG5sVg^CI)Q3LCX6>GLq z-x4&LMy8e@`QpG}gee|!XTZarObvUe&?(gr0pJpu78`nFFlnnWGSH3;RVb*@_JJLp z*Rz{RvW==0&OO`vRS;N|CB{xP+4 zjc9!SwSKYF24H)1S%=%xCVt%I!#I*BQ{#GCT>2=m+|E3&L&m#iUG~(k!BGK64ctl+ zGfks zQA9Xy^U<0TB8^DJpSm;W6S+uIn8baMU~YvSOI~)p0HV4p3e45>p=*P=-_>kIt=ew1 zq^|}9%ISbIKtsCP+f|jmJef zlXwc>v6WFip;klko^Y;r8$T%8OCP&r&(O32K?Rd^bX%c#JWp5}+!?S3(N+6)6Y5Xp z>^tOjf>OV$QrYNyJtxIea?G@L zb?zL$#zqtTsN0O(-ino~Xb1ft79dmDQ|iv@?MiZz`>*b5z2P)}Gb-~)nJ_!(*egOc zIxq>2bVFlAz9xI6a1US^&DJsJ`QQJ-G!|dOJvb2YxERVE^lvUe3?({_(E`O}H}*L{ z@u!iIlvyJ%0FO1?=1ZD3y}1z=78XEurfAEp@&)f#h1Cwv*hVR#i=~e9F0l1tuxRn* zZH{77#23r4mEkA87(1Iy^gBh_;$TbndR;7NCX`m9b1-mZVa66eX8?ny8@QuAEzI#9 z1@|o#5gt-(+z&TQOi4iJyN#slhlao`{^-7l&ugu@+1Ls6CdDF$IMIsydwaYT@`?BT zB#e`puqiGI{n@f()+W!3F1j&~=-q{{9k{ta9`L)9%hQWX2ckFZDFCsnQ2sgCM)AX) z0{bdb8w4TuL^W-#w{vt)1fO~j(Tj{6ne^m=Ipl7^oJzCL4W%S#rkeda7V%e@6#k`p zmjf4!As8|zZp?|Jq6LDH1`c?;gXI8QA%9PkJ&q2fx~5;N5AXdO#3LWJq3ja#PTta? zB*#+#N1E2C*#*kj^mE8;Ur7V8lSNOE6A1P+YB=RBQv{4j(wh-ra2E?a)R__?}m z^ilYmeyShFR}@Dyyf%uX%!s~-3^n0lG-j+B2r!X<&M;K*7S*PT__Ja=s;~Kv;I7uj zUxvy~s?8TT7Fu5Q%^%-CS$AZMb^4X`Ka8&-jaU?0J`=;WiT4Cu&we@OCN#EqrmUKY zu!m7mMQzLjZQ>9?x$UrC$MyUJdt2)2?Hm*GN*dcQSP%)%e!DlW zf=>nuGDnc1UdEYjJN%A+*H*+(iHWXMZcTz;y(iONXf(#qnR4nWyyV@ z^%GOJGI}Ins{>lzm$ezEv!)E|YA6BJY_j%peIw8QGQpK+v7=DEDU3OQ zg{J}9)>+!ZunIm3Z;d^xuuUNmklb}TQ>FL4NwW8qH0d7Y*43IqYnUBpgL z8sg>p0{DDM#HHlqrl`XPGE)AP!J=~c#wx{!?yLL2(GM!w{hGl72cq~Ct7BvC5Mb(< zINX+FC$}sw&FpU0cS$T2&kV0*WoNM3MJw~q>v)P*h4j`5cp85_>!F_8AGcSg=}d}n zR#Am{OABDx4-ge74-!O-n$u&@g`hdfNgWuc-u0Mr8qq2kEr;|CVCoUkiNznz)KTfL zz3wDfqG{;(T%2D;j*@lImcAMlO{etnRZ?r$^uM&QY!KU|5heeMbnl^Vq14oF4G&;G zB9EfdLJ&*o!mgz6#Q8j13L!7{ehptVJt_M^T#C~xJ5LnsTJ>@iu;fe{CY2ZmUQgz6dFCLc0QL}2TVVq(6F|YG zMD9W3+z-UMr)jp+&}ketc-it+gPEdPPa=<(PSz0}vc@leX*Gpt@VJ8b)QnKM31s=8 zG*e;O&O2;!v<&L?GDsQNOt#Cj$x=rULW`Z3wX>*@!T%Fo3+-9BY=l4;UkR=g|Ljc;w<{?JuA*}-sua)7=U^X>2u!BC=7EHns_CqY4i6JdV* zr0M;rtI?=4R4h{81`>{Eh>*t1qYor;XdhluMkvW-`;I>*o`<^;;08pD<)TGOASbRd zyEp!5lE5&9Krvzy$m7%IuhRxEs|lyb7-V&e{vOSo@1^`rc04h3TDMHOJ9Oz9q*Q0E zluGL%zVDpDz=(?{IW#yc6Dt$Bc!2`?>&}$tZpT4=Dk1-m+g3tJ0~A6<=LpiTVt1Y_ z$*>8KL`w>T4b}qwCYys=*chkli^ShC_;u6!q{}%>PL*CpK#M^gtQ$uj_8!_eYCyp< z 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 literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..175301efed3ab7a99605d591e2ddcbbf044c833e GIT binary patch literal 21480 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 O3`PuC001aN0k{Cua6@hY literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..2805af50f1fb0f5fb5a8429873de45d1fe713759 GIT binary patch literal 34464 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 IC`19c01G*mZU6uP literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff new file mode 100644 index 0000000000000000000000000000000000000000..6496d17f5261f2f4e4cf47522441db9f9dc52ad8 GIT binary patch literal 19776 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..43e40714ba84da3b6f55a93891c08acda602a730 GIT binary patch literal 19288 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 l7O_&IR3{JqM% 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 IC`19c09yIgT>t<8 literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..bd27726416adae36691dfff87dd82506eb1c5079 GIT binary patch literal 15944 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 Tz@KRt5g0W90CFL~Hh}*Jpe^L2 literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..22e5eff737c68962138420de5742413683fbcea0 GIT binary patch literal 14628 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�^WvtjDB>~ zr7`)5NAs|OvT49e-LqK}7{vt@P(y#5@L82vSXhF=mXpPU1eWCu5lzq2Z|=egEh!U+ zEq~(^;$myN28!|<JYQMGIM~(6 zrg|z-Pi=90_?ygYb$>eroJsT88yhhW=QaID|MW3b;uTCz))V%> zo%OH8%Im+}RVm9z;Rs;*swO3uP#+T)mkM}>$6NE>8X^iL^OI&;7A5N%OAlX)*F$w! z6_n;k`tdV-A#^=LH1_1Re8=Z_Q5Mfk$C}I2pf^l*&vbrt&wNaBF$32g()J46)9h9a z!8^WM-@!7y+TPLLPfJB9xnFlSaKxBRV?;4O{xrJd_b9~rHn}6UIh#AcXFQ5A zag~*3cQL9MUg~2J(B|~vc584)sl6IFK%1DY?6a)gkL|;L>Cxr5`A%`yxdT^}@u4#q z0=DWo!rC6atPR4E=n?q~ArO#$-Jj}*G)FS!vZBJ3+e2Cl5wX zx&~D3of6OTFd)B8NB&Knuo|*#Q$*jOUgXhh6` z92e&@2#@Z>^YC3v^<7mBD5>bfjo|HZq04-~tbUzzt3} z!$Wy!#Z(_F6|J{wF%NQq9%T~DssVur<I$X<%Mue;fuah+U$)*e_s zjGO5jVbpE3qvBUo)*9gxM1p@=xz-f!X-RnpHHQA8FRVAJnZMV5=^inDq3pTBO1J4L zC*teFl+g-`U#R49eJ^WWiCT|>=dsTpLXBBb9c~x3ngr_dkkh($nO{0UTExR0LCz!} z2#+st6ei94&_RuuP%7Lphgy|zpR{X=ERVP{-IEo+rcE@W_;Tc+2URE*`F`9TuRwPp zGWS=NI^++3T)`}y3m>|!TSal#EN@3tu Gp#KL;gZVB1 literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..d1ff7c6bd3e49326fd38d631fb05d5106f3455e6 GIT binary patch literal 12660 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 KP>2Av0saq+%YZom literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..a9d1f345bff3b0131f7759f0022778e393fb2b00 GIT binary patch literal 11852 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 literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..e735ddf8505afa47b3901ab90560caa72e57b755 GIT binary patch literal 5792 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 literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..4048e4bd6e17113e418683b90b8a5d6b4352d72a GIT binary patch literal 5464 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 literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..510a8dacfa0a6e6db1e139cf1ae6095c689f3849 GIT binary patch literal 17604 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�b+sF`RrI?)m4(N8^D=VyOjehxpW?A$1X$E=6b9 z^#N+AD?(s8$M6uQ9?#TJ#kfww!td<0exz&txFfjYmAJ}(3M%{%aH|AY5k55`Y47`{ z_hnR5hFQc)_9HHP<^PcMX2&Ccg9DQCY(iN2c+|7P&x~--ZrT>|A+x(!=5$!rlF@wMLY^@ zX&(l#DdbXy>!RmHD6c{3-!5?e(i_aHn@N&*JshPo|J=hJ*;>nI6RAT!6iUP+(dP8_ z@V?&@m)sjKn>`Apn)tN(r#kUt!7y}XAR>k6Vbm5e>rim3-r(jyL=)%6#39eWTm)}I zW@FhO`LgSubOjqhKG(Jn*w@u;sP&*)EUF-u1FbwIyQ%VGvxf3-)w8Pz|Mg$ocU`n! b1ONa4c${NkW&nf#%?w5iSO5ShL;<(}ylj?N literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff b/docs/assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..ed55c4cf1d1ae68e01948a903d8a67e2f5a38d48 GIT binary patch literal 1116 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 Zk;3@N@sSS?4+EPBgC{8K1Ir;0007CfCp7>7 literal 0 HcmV?d00001 diff --git a/docs/assets/vendor/mathjax/startup.js b/docs/assets/vendor/mathjax/startup.js new file mode 100644 index 0000000..02ce33c --- /dev/null +++ b/docs/assets/vendor/mathjax/startup.js @@ -0,0 +1 @@ +!function(){"use strict";var e={515:function(e,t,r){var n=this&&this.__values||function(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")};function o(e){return"object"==typeof e&&null!==e}function a(e,t){var r,i;try{for(var u=n(Object.keys(t)),c=u.next();!c.done;c=u.next()){var s=c.value;"__esModule"!==s&&(!o(e[s])||!o(t[s])||t[s]instanceof Promise?null!==t[s]&&void 0!==t[s]&&(e[s]=t[s]):a(e[s],t[s]))}}catch(e){r={error:e}}finally{try{c&&!c.done&&(i=u.return)&&i.call(u)}finally{if(r)throw r.error}}return e}Object.defineProperty(t,"__esModule",{value:!0}),t.MathJax=t.combineWithMathJax=t.combineDefaults=t.combineConfig=t.isObject=void 0,t.isObject=o,t.combineConfig=a,t.combineDefaults=function e(t,r,a){var i,u;t[r]||(t[r]={}),t=t[r];try{for(var c=n(Object.keys(a)),s=c.next();!s.done;s=c.next()){var l=s.value;o(t[l])&&o(a[l])?e(t,l,a[l]):null==t[l]&&null!=a[l]&&(t[l]=a[l])}}catch(e){i={error:e}}finally{try{s&&!s.done&&(u=c.return)&&u.call(c)}finally{if(i)throw i.error}}return t},t.combineWithMathJax=function(e){return a(t.MathJax,e)},void 0===r.g.MathJax&&(r.g.MathJax={}),r.g.MathJax.version||(r.g.MathJax={version:"3.1.4",_:{},config:r.g.MathJax}),t.MathJax=r.g.MathJax},235:function(e,t,r){var n=this&&this.__values||function(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")};Object.defineProperty(t,"__esModule",{value:!0}),t.CONFIG=t.MathJax=t.Loader=t.PathFilters=t.PackageError=t.Package=void 0;var o=r(515),a=r(265),i=r(265);Object.defineProperty(t,"Package",{enumerable:!0,get:function(){return i.Package}}),Object.defineProperty(t,"PackageError",{enumerable:!0,get:function(){return i.PackageError}});var u,c=r(525);t.PathFilters={source:function(e){return t.CONFIG.source.hasOwnProperty(e.name)&&(e.name=t.CONFIG.source[e.name]),!0},normalize:function(e){var t=e.name;return t.match(/^(?:[a-z]+:\/)?\/|[a-z]:\\|\[/i)||(e.name="[mathjax]/"+t.replace(/^\.\//,"")),e.addExtension&&!t.match(/\.[^\/]+$/)&&(e.name+=".js"),!0},prefix:function(e){for(var r;(r=e.name.match(/^\[([^\]]*)\]/))&&t.CONFIG.paths.hasOwnProperty(r[1]);)e.name=t.CONFIG.paths[r[1]]+e.name.substr(r[0].length);return!0}},function(e){e.ready=function(){for(var e,t,r=[],o=0;o=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")},i=this&&this.__read||function(e,t){var r="function"==typeof Symbol&&e[Symbol.iterator];if(!r)return e;var n,o,a=r.call(e),i=[];try{for(;(void 0===t||t-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(e){o={error:e}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},u=this&&this.__spreadArray||function(e,t){for(var r=0,n=t.length,o=e.length;r=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")},a=this&&this.__read||function(e,t){var r="function"==typeof Symbol&&e[Symbol.iterator];if(!r)return e;var n,o,a=r.call(e),i=[];try{for(;(void 0===t||t-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(e){o={error:e}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},i=this&&this.__spreadArray||function(e,t){for(var r=0,n=t.length,o=e.length;r=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")},i=this&&this.__read||function(e,t){var r="function"==typeof Symbol&&e[Symbol.iterator];if(!r)return e;var n,o,a=r.call(e),i=[];try{for(;(void 0===t||t-- >0)&&!(n=a.next()).done;)i.push(n.value)}catch(e){o={error:e}}finally{try{n&&!n.done&&(r=a.return)&&r.call(a)}finally{if(o)throw o.error}}return i},u=this&&this.__spreadArray||function(e,t){for(var r=0,n=t.length,o=e.length;rt.length}}}},e.prototype.add=function(t,r){void 0===r&&(r=e.DEFAULTPRIORITY);var n=this.items.length;do{n--}while(n>=0&&r=0&&this.items[t].item!==e);t>=0&&this.items.splice(t,1)},e.prototype.toArray=function(){return Array.from(this)},e.DEFAULTPRIORITY=5,e}();t.PrioritizedList=r}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n].call(a.exports,a,a.exports,r),a.exports}r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),function(){var e=r(515),t=r(235),n=r(265),o=r(388);(0,e.combineWithMathJax)({_:{components:{loader:t,package:n,startup:o}}});var a,i={tex:"[mathjax]/input/tex/extensions",sre:"[mathjax]/sre/"+("undefined"==typeof window?"sre-node":"sre_browser")},u=["[tex]/action","[tex]/ams","[tex]/amscd","[tex]/bbox","[tex]/boldsymbol","[tex]/braket","[tex]/bussproofs","[tex]/cancel","[tex]/color","[tex]/configmacros","[tex]/enclose","[tex]/extpfeil","[tex]/html","[tex]/mhchem","[tex]/newcommand","[tex]/noerrors","[tex]/noundefined","[tex]/physics","[tex]/require","[tex]/tagformat","[tex]/textmacros","[tex]/unicode","[tex]/verb"],c={startup:["loader"],"input/tex":["input/tex-base","[tex]/ams","[tex]/newcommand","[tex]/noundefined","[tex]/require","[tex]/autoload","[tex]/configmacros"],"input/tex-full":["input/tex-base","[tex]/all-packages"].concat(u),"[tex]/all-packages":u};function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r { + MathJax.typesetPromise?.( + document.querySelectorAll(".arithmatex") + ).catch((error) => console.error(error)); +}); diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..d70a077 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,13 @@ +.md-typeset .arithmatex { + overflow-x: auto; +} + +.md-typeset .doc-contents { + overflow-wrap: anywhere; +} + +.md-typeset h1 code, +.md-typeset h2 code, +.md-typeset h3 code { + word-break: break-word; +} diff --git a/make_docs.sh b/make_docs.sh index 6d99b5f..67a2a02 100755 --- a/make_docs.sh +++ b/make_docs.sh @@ -2,18 +2,12 @@ set -Eeuo pipefail -cd ~/projects/meanas +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" -# Approach 1: pdf to html? -#pdoc3 --pdf --force --template-dir pdoc_templates -o doc . | \ -# pandoc --metadata=title:"meanas" --toc --toc-depth=4 --from=markdown+abbreviations --to=html --output=doc.html --gladtex -s - +mkdocs build --clean -# Approach 2: pdf to html with gladtex -rm -rf _doc_mathimg -pdoc --pdf --force --template-dir pdoc_templates -o doc . > doc.md -pandoc --metadata=title:"meanas" --from=markdown+abbreviations --to=html --output=doc.htex --gladtex -s --css pdoc_templates/pdoc.css doc.md -gladtex -a -n -d _doc_mathimg -c white -b black doc.htex - -# Approach 3: html with gladtex -#pdoc3 --html --force --template-dir pdoc_templates -o doc . -#find doc -iname '*.html' -exec gladtex -a -n -d _mathimg -c white {} \; +PRINT_PAGE='site/print_page/index.html' +if [[ -f "$PRINT_PAGE" ]] && command -v htmlark >/dev/null 2>&1; then + htmlark "$PRINT_PAGE" -o site/standalone.html +fi diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..a6ab1de --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,76 @@ +site_name: meanas +site_description: Electromagnetic simulation tools +site_url: "" +repo_url: https://mpxd.net/code/jan/meanas +repo_name: meanas +docs_dir: docs +site_dir: site +strict: false + +theme: + name: material + font: false + features: + - navigation.indexes + - navigation.sections + - navigation.top + - content.code.copy + - toc.follow + +nav: + - Home: index.md + - API: + - Overview: api/index.md + - meanas: api/meanas.md + - eigensolvers: api/eigensolvers.md + - fdfd: api/fdfd.md + - waveguides: api/waveguides.md + - fdtd: api/fdtd.md + - fdmath: api/fdmath.md + +plugins: + - search + - mkdocstrings: + handlers: + python: + paths: + - . + options: + show_root_heading: true + show_root_toc_entry: false + show_source: false + show_signature_annotations: true + show_symbol_type_heading: true + show_symbol_type_toc: true + members_order: source + separate_signature: true + merge_init_into_class: true + docstring_style: google + - print-site + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.arithmatex: + generic: true + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + +extra_css: + - stylesheets/extra.css + +extra_javascript: + - javascripts/mathjax.js + - assets/vendor/mathjax/startup.js + +watch: + - meanas diff --git a/pdoc_templates/config.mako b/pdoc_templates/config.mako deleted file mode 100644 index 93cf716..0000000 --- a/pdoc_templates/config.mako +++ /dev/null @@ -1,47 +0,0 @@ -<%! - # Template configuration. Copy over in your template directory - # (used with --template-dir) and adapt as required. - html_lang = 'en' - show_inherited_members = False - extract_module_toc_into_sidebar = True - list_class_variables_in_index = True - sort_identifiers = True - show_type_annotations = True - - # Show collapsed source code block next to each item. - # Disabling this can improve rendering speed of large modules. - show_source_code = True - - # If set, format links to objects in online source code repository - # according to this template. Supported keywords for interpolation - # are: commit, path, start_line, end_line. - #git_link_template = 'https://github.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' - #git_link_template = 'https://gitlab.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' - #git_link_template = 'https://bitbucket.org/USER/PROJECT/src/{commit}/{path}#lines-{start_line}:{end_line}' - #git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start_line}' - #git_link_template = None - git_link_template = 'https://mpxd.net/code/jan/meanas/src/commit/{commit}/{path}#L{start_line}-L{end_line}' - - # A prefix to use for every HTML hyperlink in the generated documentation. - # No prefix results in all links being relative. - link_prefix = '' - - # Enable syntax highlighting for code/source blocks by including Highlight.js - syntax_highlighting = True - - # Set the style keyword such as 'atom-one-light' or 'github-gist' - # Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles - # Demo: https://highlightjs.org/static/demo/ - hljs_style = 'github' - - # If set, insert Google Analytics tracking code. Value is GA - # tracking id (UA-XXXXXX-Y). - google_analytics = '' - - # If set, render LaTeX math syntax within \(...\) (inline equations), - # or within \[...\] or $$...$$ or `.. math::` (block equations) - # as nicely-formatted math formulas using MathJax. - # Note: in Python docstrings, either all backslashes need to be escaped (\\) - # or you need to use raw r-strings. - latex_math = True -%> diff --git a/pdoc_templates/css.mako b/pdoc_templates/css.mako deleted file mode 100644 index 39a77ed..0000000 --- a/pdoc_templates/css.mako +++ /dev/null @@ -1,389 +0,0 @@ -<%! - from pdoc.html_helpers import minify_css -%> - -<%def name="mobile()" filter="minify_css"> - .flex { - display: flex !important; - } - - body { - line-height: 1.5em; - background: black; - color: #DDD; - } - - #content { - padding: 20px; - } - - #sidebar { - padding: 30px; - overflow: hidden; - } - - .http-server-breadcrumbs { - font-size: 130%; - margin: 0 0 15px 0; - } - - #footer { - font-size: .75em; - padding: 5px 30px; - border-top: 1px solid #ddd; - text-align: right; - } - #footer p { - margin: 0 0 0 1em; - display: inline-block; - } - #footer p:last-child { - margin-right: 30px; - } - - h1, h2, h3, h4, h5 { - font-weight: 300; - } - h1 { - font-size: 2.5em; - line-height: 1.1em; - } - h2 { - font-size: 1.75em; - margin: 1em 0 .50em 0; - } - h3 { - font-size: 1.4em; - margin: 25px 0 10px 0; - } - h4 { - margin: 0; - font-size: 105%; - } - - a { - color: #999; - text-decoration: none; - transition: color .3s ease-in-out; - } - a:hover { - color: #18d; - } - - .title code { - font-weight: bold; - } - h2[id^="header-"] { - margin-top: 2em; - } - .ident { - color: #7ff; - } - - pre code { - background: transparent; - font-size: .8em; - line-height: 1.4em; - } - code { - background: #0d0d0e; - padding: 1px 4px; - overflow-wrap: break-word; - } - h1 code { background: transparent } - - pre { - background: #111; - border: 0; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; - margin: 1em 0; - padding: 1ex; - } - - #http-server-module-list { - display: flex; - flex-flow: column; - } - #http-server-module-list div { - display: flex; - } - #http-server-module-list dt { - min-width: 10%; - } - #http-server-module-list p { - margin-top: 0; - } - - .toc ul, - #index { - list-style-type: none; - margin: 0; - padding: 0; - } - #index code { - background: transparent; - } - #index h3 { - border-bottom: 1px solid #ddd; - } - #index ul { - padding: 0; - } - #index h4 { - font-weight: bold; - } - #index h4 + ul { - margin-bottom:.6em; - } - /* Make TOC lists have 2+ columns when viewport is wide enough. - Assuming ~20-character identifiers and ~30% wide sidebar. */ - @media (min-width: 200ex) { #index .two-column { column-count: 2 } } - @media (min-width: 300ex) { #index .two-column { column-count: 3 } } - - dl { - margin-bottom: 2em; - } - dl dl:last-child { - margin-bottom: 4em; - } - dd { - margin: 0 0 1em 3em; - } - #header-classes + dl > dd { - margin-bottom: 3em; - } - dd dd { - margin-left: 2em; - } - dd p { - margin: 10px 0; - } - .name { - background: #111; - font-weight: bold; - font-size: .85em; - padding: 5px 10px; - display: inline-block; - min-width: 40%; - } - .name:hover { - background: #101010; - } - .name > span:first-child { - white-space: nowrap; - } - .name.class > span:nth-child(2) { - margin-left: .4em; - } - .inherited { - color: #777; - border-left: 5px solid #eee; - padding-left: 1em; - } - .inheritance em { - font-style: normal; - font-weight: bold; - } - - /* Docstrings titles, e.g. in numpydoc format */ - .desc h2 { - font-weight: 400; - font-size: 1.25em; - } - .desc h3 { - font-size: 1em; - } - .desc dt code { - background: inherit; /* Don't grey-back parameters */ - } - - .source summary, - .git-link-div { - color: #aaa; - text-align: right; - font-weight: 400; - font-size: .8em; - text-transform: uppercase; - } - .source summary > * { - white-space: nowrap; - cursor: pointer; - } - .git-link { - color: inherit; - margin-left: 1em; - } - .source pre { - max-height: 500px; - overflow: auto; - margin: 0; - } - .source pre code { - font-size: 12px; - overflow: visible; - } - .hlist { - list-style: none; - } - .hlist li { - display: inline; - } - .hlist li:after { - content: ',\2002'; - } - .hlist li:last-child:after { - content: none; - } - .hlist .hlist { - display: inline; - padding-left: 1em; - } - - img { - max-width: 100%; - } - - .admonition { - padding: .1em .5em; - margin-bottom: 1em; - } - .admonition-title { - font-weight: bold; - } - .admonition.note, - .admonition.info, - .admonition.important { - background: #610; - } - .admonition.todo, - .admonition.versionadded, - .admonition.tip, - .admonition.hint { - background: #202; - } - .admonition.warning, - .admonition.versionchanged, - .admonition.deprecated { - background: #02b; - } - .admonition.error, - .admonition.danger, - .admonition.caution { - background: darkpink; - } - - -<%def name="desktop()" filter="minify_css"> - @media screen and (min-width: 700px) { - #sidebar { - width: 30%; - } - #content { - width: 70%; - max-width: 100ch; - padding: 3em 4em; - border-left: 1px solid #ddd; - } - pre code { - font-size: 1em; - } - .item .name { - font-size: 1em; - } - main { - display: flex; - flex-direction: row-reverse; - justify-content: flex-end; - } - .toc ul ul, - #index ul { - padding-left: 1.5em; - } - .toc > ul > li { - margin-top: .5em; - } - } - - -<%def name="print()" filter="minify_css"> -@media print { - #sidebar h1 { - page-break-before: always; - } - .source { - display: none; - } -} -@media print { - * { - background: transparent !important; - color: #000 !important; /* Black prints faster: h5bp.com/s */ - box-shadow: none !important; - text-shadow: none !important; - } - - a[href]:after { - content: " (" attr(href) ")"; - font-size: 90%; - } - /* Internal, documentation links, recognized by having a title, - don't need the URL explicity stated. */ - a[href][title]:after { - content: none; - } - - abbr[title]:after { - content: " (" attr(title) ")"; - } - - /* - * Don't show links for images, or javascript/internal links - */ - - .ir a:after, - a[href^="javascript:"]:after, - a[href^="#"]:after { - content: ""; - } - - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - - thead { - display: table-header-group; /* h5bp.com/t */ - } - - tr, - img { - page-break-inside: avoid; - } - - img { - max-width: 100% !important; - } - - @page { - margin: 0.5cm; - } - - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - - h1, - h2, - h3, - h4, - h5, - h6 { - page-break-after: avoid; - } -} - diff --git a/pdoc_templates/html.mako b/pdoc_templates/html.mako deleted file mode 100644 index 6b3326f..0000000 --- a/pdoc_templates/html.mako +++ /dev/null @@ -1,445 +0,0 @@ -<% - import os - - import pdoc - from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link, _md, to_markdown - - from markdown.inlinepatterns import InlineProcessor - from markdown.util import AtomicString, etree - - - def link(d, name=None, fmt='{}'): - name = fmt.format(name or d.qualname + ('()' if isinstance(d, pdoc.Function) else '')) - if not isinstance(d, pdoc.Doc) or isinstance(d, pdoc.External) and not external_links: - return name - url = d.url(relative_to=module, link_prefix=link_prefix, - top_ancestor=not show_inherited_members) - return '{}'.format(d.refname, url, name) - - - # Altered latex delimeters (allow inline $...$, wrap in ) - class _MathPattern(InlineProcessor): - NAME = 'pdoc-math' - PATTERN = r'(? - -<%def name="ident(name)">${name} - -<%def name="show_source(d)"> - % if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None): - <% git_link = format_git_link(git_link_template, d) %> - % if show_source_code: -
- - Expand source code - % if git_link: - Browse git - %endif - -
${d.source | h}
-
- % elif git_link: - - %endif - %endif - - -<%def name="show_desc(d, short=False)"> - <% - inherits = ' inherited' if d.inherits else '' - docstring = glimpse(d.docstring) if short or inherits else d.docstring - %> - % if d.inherits: -

- Inherited from: - % if hasattr(d.inherits, 'cls'): - ${link(d.inherits.cls)}.${link(d.inherits, d.name)} - % else: - ${link(d.inherits)} - % endif -

- % endif -
${docstring | to_html}
- % if not isinstance(d, pdoc.Module): - ${show_source(d)} - % endif - - -<%def name="show_module_list(modules)"> -

Python module list

- -% if not modules: -

No modules found.

-% else: -
- % for name, desc in modules: -
-
${name}
-
${desc | glimpse, to_html}
-
- % endfor -
-% endif - - -<%def name="show_column_list(items)"> - <% - two_column = len(items) >= 6 and all(len(i.name) < 20 for i in items) - %> -
    - % for item in items: -
  • ${link(item, item.name)}
  • - % endfor -
- - -<%def name="show_module(module)"> - <% - variables = module.variables(sort=sort_identifiers) - classes = module.classes(sort=sort_identifiers) - functions = module.functions(sort=sort_identifiers) - submodules = module.submodules() - %> - - <%def name="show_func(f)"> -
- <% - params = ', '.join(f.params(annotate=show_type_annotations, link=link)) - returns = show_type_annotations and f.return_annotation(link=link) or '' - if returns: - returns = ' ->\N{NBSP}' + returns - %> - ${f.funcdef()} ${ident(f.name)}(${params})${returns} -
-
${show_desc(f)}
- - -
- % if http_server: - - % endif -

${'Namespace' if module.is_namespace else 'Module'} ${module.name}

-
- -
- ${module.docstring | to_html} - ${show_source(module)} -
- -
- % if submodules: -

Sub-modules

-
- % for m in submodules: -
${link(m)}
-
${show_desc(m, short=True)}
- % endfor -
- % endif -
- -
- % if variables: -

Global variables

-
- % for v in variables: -
var ${ident(v.name)}
-
${show_desc(v)}
- % endfor -
- % endif -
- -
- % if functions: -

Functions

-
- % for f in functions: - ${show_func(f)} - % endfor -
- % endif -
- -
- % if classes: -

Classes

-
- % for c in classes: - <% - class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers) - smethods = c.functions(show_inherited_members, sort=sort_identifiers) - inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers) - methods = c.methods(show_inherited_members, sort=sort_identifiers) - mro = c.mro() - subclasses = c.subclasses() - params = ', '.join(c.params(annotate=show_type_annotations, link=link)) - %> -
- class ${ident(c.name)} - % if params: - (${params}) - % endif -
- -
${show_desc(c)} - - % if mro: -

Ancestors

-
    - % for cls in mro: -
  • ${link(cls)}
  • - % endfor -
- %endif - - % if subclasses: -

Subclasses

-
    - % for sub in subclasses: -
  • ${link(sub)}
  • - % endfor -
- % endif - % if class_vars: -

Class variables

-
- % for v in class_vars: -
var ${ident(v.name)}
-
${show_desc(v)}
- % endfor -
- % endif - % if smethods: -

Static methods

-
- % for f in smethods: - ${show_func(f)} - % endfor -
- % endif - % if inst_vars: -

Instance variables

-
- % for v in inst_vars: -
var ${ident(v.name)}
-
${show_desc(v)}
- % endfor -
- % endif - % if methods: -

Methods

-
- % for f in methods: - ${show_func(f)} - % endfor -
- % endif - - % if not show_inherited_members: - <% - members = c.inherited_members() - %> - % if members: -

Inherited members

-
    - % for cls, mems in members: -
  • ${link(cls)}: -
      - % for m in mems: -
    • ${link(m, name=m.name)}
    • - % endfor -
    - -
  • - % endfor -
- % endif - % endif - -
- % endfor -
- % endif -
- - -<%def name="module_index(module)"> - <% - variables = module.variables(sort=sort_identifiers) - classes = module.classes(sort=sort_identifiers) - functions = module.functions(sort=sort_identifiers) - submodules = module.submodules() - supermodule = module.supermodule - %> - - - - - - - - - - -<% - module_list = 'modules' in context.keys() # Whether we're showing module list in server mode -%> - - % if module_list: - Python module list - - % else: - ${module.name} API documentation - - % endif - - - - % if syntax_highlighting: - - %endif - - <%namespace name="css" file="css.mako" /> - - - - - % if google_analytics: - - % endif - - <%include file="head.mako"/> - - -
- % if module_list: -
- ${show_module_list(modules)} -
- % else: -
- ${show_module(module)} -
- ${module_index(module)} - % endif -
- - - -% if syntax_highlighting: - - -% endif - -% if http_server and module: ## Auto-reload on file change in dev mode - -% endif - - diff --git a/pdoc_templates/html_helpers.py b/pdoc_templates/html_helpers.py deleted file mode 100644 index 5e58405..0000000 --- a/pdoc_templates/html_helpers.py +++ /dev/null @@ -1,539 +0,0 @@ -""" -Helper functions for HTML output. -""" -import inspect -import os -import re -import subprocess -import traceback -from functools import partial, lru_cache -from typing import Callable, Match -from warnings import warn - -import markdown -from markdown.inlinepatterns import InlineProcessor -from markdown.util import AtomicString, etree - -import pdoc - - -@lru_cache() -def minify_css(css: str, - _whitespace=partial(re.compile(r'\s*([,{:;}])\s*').sub, r'\1'), - _comments=partial(re.compile(r'/\*.*?\*/', flags=re.DOTALL).sub, ''), - _trailing_semicolon=partial(re.compile(r';\s*}').sub, '}')): - """ - Minify CSS by removing extraneous whitespace, comments, and trailing semicolons. - """ - return _trailing_semicolon(_whitespace(_comments(css))).strip() - - -def minify_html(html: str, - _minify=partial( - re.compile(r'(.*?)()|(.*)', re.IGNORECASE | re.DOTALL).sub, - lambda m, _norm_space=partial(re.compile(r'\s\s+').sub, '\n'): ( - _norm_space(m.group(1) or '') + - (m.group(2) or '') + - _norm_space(m.group(3) or '')))): - """ - Minify HTML by replacing all consecutive whitespace with a single space - (or newline) character, except inside `
` tags.
-    """
-    return _minify(html)
-
-
-def glimpse(text: str, max_length=153, *, paragraph=True,
-            _split_paragraph=partial(re.compile(r'\s*\n\s*\n\s*').split, maxsplit=1),
-            _trim_last_word=partial(re.compile(r'\S+$').sub, ''),
-            _remove_titles=partial(re.compile(r'^(#+|-{4,}|={4,})', re.MULTILINE).sub, ' ')):
-    """
-    Returns a short excerpt (e.g. first paragraph) of text.
-    If `paragraph` is True, the first paragraph will be returned,
-    but never longer than `max_length` characters.
-    """
-    text = text.lstrip()
-    if paragraph:
-        text, *rest = _split_paragraph(text)
-        if rest:
-            text = text.rstrip('.')
-            text += ' …'
-        text = _remove_titles(text).strip()
-
-    if len(text) > max_length:
-        text = _trim_last_word(text[:max_length - 2])
-        if not text.endswith('.') or not paragraph:
-            text = text.rstrip('. ') + ' …'
-    return text
-
-
-_md = markdown.Markdown(
-    output_format='html5',
-    extensions=[
-        "markdown.extensions.abbr",
-        "markdown.extensions.attr_list",
-        "markdown.extensions.def_list",
-        "markdown.extensions.fenced_code",
-        "markdown.extensions.footnotes",
-        "markdown.extensions.tables",
-        "markdown.extensions.admonition",
-        "markdown.extensions.smarty",
-        "markdown.extensions.toc",
-    ],
-    extension_configs={
-        "markdown.extensions.smarty": dict(
-            smart_dashes=True,
-            smart_ellipses=True,
-            smart_quotes=False,
-            smart_angled_quotes=False,
-        ),
-    },
-)
-
-
-class _ToMarkdown:
-    """
-    This class serves as a namespace for methods converting common
-    documentation formats into markdown our Python-Markdown with
-    addons can ingest.
-
-    If debugging regexs (I can't imagine why that would be necessary
-    — they are all perfect!) an insta-preview tool such as RegEx101.com
-    will come in handy.
-    """
-    @staticmethod
-    def _deflist(name, type, desc,
-                 # Wraps any identifiers and string literals in parameter type spec
-                 # in backticks while skipping common "stopwords" such as 'or', 'of',
-                 # 'optional' ... See §4 Parameters:
-                 # https://numpydoc.readthedocs.io/en/latest/format.html#sections
-                 _type_parts=partial(
-                     re.compile(r'[\w.\'"]+').sub,
-                     lambda m: ('{}' if m.group(0) in ('of', 'or', 'default', 'optional') else
-                                '`{}`').format(m.group(0)))):
-        """
-        Returns `name`, `type`, and `desc` formatted as a
-        Python-Markdown definition list entry. See also:
-        https://python-markdown.github.io/extensions/definition_lists/
-        """
-        type = _type_parts(type or '')
-        desc = desc or ' '
-        assert _ToMarkdown._is_indented_4_spaces(desc)
-        assert name or type
-        ret = ""
-        if name:
-            ret += '**`{}`**'.format(name)
-        if type:
-            ret += ' : {}'.format(type) if ret else type
-        ret += '\n:   {}\n\n'.format(desc)
-        return ret
-
-    @staticmethod
-    def _numpy_params(match,
-                      _name_parts=partial(re.compile(', ').sub, '`**, **`')):
-        """ Converts NumpyDoc parameter (etc.) sections into Markdown. """
-        name, type, desc = match.group("name", "type", "desc")
-        type = type or match.groupdict().get('just_type', None)
-        desc = desc.strip()
-        name = name and _name_parts(name)
-        return _ToMarkdown._deflist(name, type, desc)
-
-    @staticmethod
-    def _numpy_seealso(match):
-        """
-        Converts NumpyDoc "See Also" section either into referenced code,
-        optionally within a definition list.
-        """
-        spec_with_desc, simple_list = match.groups()
-        if spec_with_desc:
-            return '\n\n'.join('`{}`\n:   {}'.format(*map(str.strip, line.split(':', 1)))
-                               for line in filter(None, spec_with_desc.split('\n')))
-        return ', '.join('`{}`'.format(i) for i in simple_list.split(', '))
-
-    @staticmethod
-    def _numpy_sections(match):
-        """
-        Convert sections with parameter, return, and see also lists to Markdown
-        lists.
-        """
-        section, body = match.groups()
-        if section.title() == 'See Also':
-            body = re.sub(r'^((?:\n?[\w.]* ?: .*)+)|(.*\w.*)',
-                          _ToMarkdown._numpy_seealso, body)
-        elif section.title() in ('Returns', 'Yields', 'Raises', 'Warns'):
-            body = re.sub(r'^(?:(?P\*{0,2}\w+(?:, \*{0,2}\w+)*)'
-                          r'(?: ?: (?P.*))|'
-                          r'(?P\w[^\n`*]*))(?(?:\n(?: {4}.*|$))*)',
-                          _ToMarkdown._numpy_params, body, flags=re.MULTILINE)
-        else:
-            body = re.sub(r'^(?P\*{0,2}\w+(?:, \*{0,2}\w+)*)'
-                          r'(?: ?: (?P.*))?(?(?:\n(?: {4}.*|$))*)',
-                          _ToMarkdown._numpy_params, body, flags=re.MULTILINE)
-        return section + '\n-----\n' + body
-
-    @staticmethod
-    def numpy(text):
-        """
-        Convert `text` in numpydoc docstring format to Markdown
-        to be further converted later.
-        """
-        return re.sub(r'^(\w[\w ]+)\n-{3,}\n'
-                      r'((?:(?!.+\n-+).*$\n?)*)',
-                      _ToMarkdown._numpy_sections, text, flags=re.MULTILINE)
-
-    @staticmethod
-    def _is_indented_4_spaces(txt, _3_spaces_or_less=re.compile(r'\n\s{0,3}\S').search):
-        return '\n' not in txt or not _3_spaces_or_less(txt)
-
-    @staticmethod
-    def _fix_indent(name, type, desc):
-        """Maybe fix indent from 2 to 4 spaces."""
-        if not _ToMarkdown._is_indented_4_spaces(desc):
-            desc = desc.replace('\n', '\n  ')
-        return name, type, desc
-
-    @staticmethod
-    def indent(indent, text, *, clean_first=False):
-        if clean_first:
-            text = inspect.cleandoc(text)
-        return re.sub(r'\n', '\n' + indent, indent + text.rstrip())
-
-    @staticmethod
-    def google(text,
-               _googledoc_sections=partial(
-                   re.compile(r'^([A-Z]\w+):$\n((?:\n?(?: {2,}.*|$))+)', re.MULTILINE).sub,
-                   lambda m, _params=partial(
-                           re.compile(r'^([\w*]+)(?: \(([\w.,=\[\] ]+)\))?: '
-                                      r'((?:.*)(?:\n(?: {2,}.*|$))*)', re.MULTILINE).sub,
-                           lambda m: _ToMarkdown._deflist(*_ToMarkdown._fix_indent(*m.groups()))): (
-                       m.group() if not m.group(2) else '\n{}\n-----\n{}'.format(
-                           m.group(1), _params(inspect.cleandoc('\n' + m.group(2))))))):
-        """
-        Convert `text` in Google-style docstring format to Markdown
-        to be further converted later.
-        """
-        return _googledoc_sections(text)
-
-    @staticmethod
-    def _admonition(match, module=None, limit_types=None):
-        indent, type, value, text = match.groups()
-
-        if limit_types and type not in limit_types:
-            return match.group(0)
-
-        if type == 'include' and module:
-            try:
-                return _ToMarkdown._include_file(indent, value,
-                                                 _ToMarkdown._directive_opts(text), module)
-            except Exception as e:
-                raise RuntimeError('`.. include:: {}` error in module {!r}: {}'
-                                   .format(value, module.name, e))
-        if type in ('image', 'figure'):
-            return '{}![{}]({})\n'.format(
-                indent, text.translate(str.maketrans({'\n': ' ',
-                                                      '[': '\\[',
-                                                      ']': '\\]'})).strip(), value)
-        if type == 'math':
-            return _ToMarkdown.indent(indent,
-                                      '\\[ ' + text.strip() + ' \\]',
-                                      clean_first=True)
-
-        if type == 'versionchanged':
-            title = 'Changed in version: ' + value
-        elif type == 'versionadded':
-            title = 'Added in version: ' + value
-        elif type == 'deprecated' and value:
-            title = 'Deprecated since version: ' + value
-        elif type == 'admonition':
-            title = value
-        elif type.lower() == 'todo':
-            title = 'TODO'
-            text = value + ' ' + text
-        else:
-            title = type.capitalize()
-            if value:
-                title += ': ' + value
-
-        text = _ToMarkdown.indent(indent + '    ', text, clean_first=True)
-        return '{}!!! {} "{}"\n{}\n'.format(indent, type, title, text)
-
-    @staticmethod
-    def admonitions(text, module, limit_types=None):
-        """
-        Process reStructuredText's block directives such as
-        `.. warning::`, `.. deprecated::`, `.. versionadded::`, etc.
-        and turn them into Python-M>arkdown admonitions.
-
-        `limit_types` is optionally a set of directives to limit processing to.
-
-        See: https://python-markdown.github.io/extensions/admonition/
-        """
-        substitute = partial(re.compile(r'^(?P *)\.\. ?(\w+)::(?: *(.*))?'
-                                        r'((?:\n(?:(?P=indent) +.*| *$))*)', re.MULTILINE).sub,
-                             partial(_ToMarkdown._admonition, module=module,
-                                     limit_types=limit_types))
-        # Apply twice for nested (e.g. image inside warning)
-        return substitute(substitute(text))
-
-    @staticmethod
-    def _include_file(indent: str, path: str, options: dict, module: pdoc.Module) -> str:
-        start_line = int(options.get('start-line', 0))
-        end_line = int(options.get('end-line', 0)) or None
-        start_after = options.get('start-after')
-        end_before = options.get('end-before')
-
-        with open(os.path.join(os.path.dirname(module.obj.__file__), path),
-                  encoding='utf-8') as f:
-            text = ''.join(list(f)[start_line:end_line])
-
-        if start_after:
-            text = text[text.index(start_after) + len(start_after):]
-        if end_before:
-            text = text[:text.index(end_before)]
-
-        return _ToMarkdown.indent(indent, text)
-
-    @staticmethod
-    def _directive_opts(text: str) -> dict:
-        return dict(re.findall(r'^ *:([^:]+): *(.*)', text, re.MULTILINE))
-
-    @staticmethod
-    def doctests(text,
-                 _indent_doctests=partial(
-                     re.compile(r'(?:^(?P```|~~~).*\n)?'
-                                r'(?:^>>>.*'
-                                r'(?:\n(?:(?:>>>|\.\.\.).*))*'
-                                r'(?:\n.*)?\n\n?)+'
-                                r'(?P=fence)?', re.MULTILINE).sub,
-                     lambda m: (m.group(0) if m.group('fence') else
-                                ('\n    ' + '\n    '.join(m.group(0).split('\n')) + '\n\n')))):
-        """
-        Indent non-fenced (`~~~`) top-level (0-indented)
-        doctest blocks so they render as code.
-        """
-        if not text.endswith('\n'):  # Needed for the r'(?:\n.*)?\n\n?)+' line (GH-72)
-            text += '\n'
-        return _indent_doctests(text)
-
-    @staticmethod
-    def raw_urls(text):
-        """Wrap URLs in Python-Markdown-compatible ."""
-        return re.sub(r'(?)\s]+)(\s*)', r'\1<\2>\3', text)
-
-
-class _MathPattern(InlineProcessor):
-    NAME = 'pdoc-math'
-    PATTERN = r'(?'):  # CUT was put into its own paragraph
-        toc = toc[:-3].rstrip()
-    return toc
-
-
-def format_git_link(template: str, dobj: pdoc.Doc):
-    """
-    Interpolate `template` as a formatted string literal using values extracted
-    from `dobj` and the working environment.
-    """
-    if not template:
-        return None
-    try:
-        if 'commit' in _str_template_fields(template):
-            commit = _git_head_commit()
-        abs_path = inspect.getfile(inspect.unwrap(dobj.obj))
-        path = _project_relative_path(abs_path)
-        lines, start_line = inspect.getsourcelines(dobj.obj)
-        end_line = start_line + len(lines) - 1
-        url = template.format(**locals())
-        return url
-    except Exception:
-        warn('format_git_link for {} failed:\n{}'.format(dobj.obj, traceback.format_exc()))
-        return None
-
-
-@lru_cache()
-def _git_head_commit():
-    """
-    If the working directory is part of a git repository, return the
-    head git commit hash. Otherwise, raise a CalledProcessError.
-    """
-    process_args = ['git', 'rev-parse', 'HEAD']
-    try:
-        commit = subprocess.check_output(process_args, universal_newlines=True).strip()
-        return commit
-    except OSError as error:
-        warn("git executable not found on system:\n{}".format(error))
-    except subprocess.CalledProcessError as error:
-        warn(
-            "Ensure pdoc is run within a git repository.\n"
-            "`{}` failed with output:\n{}"
-            .format(' '.join(process_args), error.output)
-        )
-    return None
-
-
-@lru_cache()
-def _git_project_root():
-    """
-    Return the path to project root directory or None if indeterminate.
-    """
-    path = None
-    for cmd in (['git', 'rev-parse', '--show-superproject-working-tree'],
-                ['git', 'rev-parse', '--show-toplevel']):
-        try:
-            path = subprocess.check_output(cmd, universal_newlines=True).rstrip('\r\n')
-            if path:
-                break
-        except (subprocess.CalledProcessError, OSError):
-            pass
-    return path
-
-
-@lru_cache()
-def _project_relative_path(absolute_path):
-    """
-    Convert an absolute path of a python source file to a project-relative path.
-    Assumes the project's path is either the current working directory or
-    Python library installation.
-    """
-    from distutils.sysconfig import get_python_lib
-    for prefix_path in (_git_project_root() or os.getcwd(),
-                        get_python_lib()):
-        common_path = os.path.commonpath([prefix_path, absolute_path])
-        if common_path == prefix_path:
-            # absolute_path is a descendant of prefix_path
-            return os.path.relpath(absolute_path, prefix_path)
-    raise RuntimeError(
-        "absolute path {!r} is not a descendant of the current working directory "
-        "or of the system's python library."
-        .format(absolute_path)
-    )
-
-
-@lru_cache()
-def _str_template_fields(template):
-    """
-    Return a list of `str.format` field names in a template string.
-    """
-    from string import Formatter
-    return [
-        field_name
-        for _, field_name, _, _ in Formatter().parse(template)
-        if field_name is not None
-    ]
diff --git a/pdoc_templates/pdf.mako b/pdoc_templates/pdf.mako
deleted file mode 100644
index 50e7989..0000000
--- a/pdoc_templates/pdf.mako
+++ /dev/null
@@ -1,185 +0,0 @@
-<%!
-    import re
-    import pdoc
-    from pdoc.html_helpers import to_markdown, format_git_link
-
-    def link(d, fmt='{}'):
-        name = fmt.format(d.qualname + ('()' if isinstance(d, pdoc.Function) else ''))
-        if isinstance(d, pdoc.External):
-            return name
-        return '[{}](#{})'.format(name, d.refname)
-
-    def _to_md(text, module):
-        text = to_markdown(text, module=module, link=link)
-        # Setext H2 headings to atx H2 headings
-        text = re.sub(r'\n(.+)\n-{3,}\n', r'\n## \1\n\n', text)
-        # Convert admonitions into simpler paragraphs, dedent contents
-        text = re.sub(r'^(?P( *))!!! \w+ \"([^\"]*)\"(.*(?:\n(?P=indent) +.*)*)',
-                      lambda m: '{}**{}:** {}'.format(m.group(2), m.group(3),
-                                                      re.sub('\n {,4}', '\n', m.group(4))),
-                      text, flags=re.MULTILINE)
-        return text
-
-    def subh(text, level=2):
-        # Deepen heading levels so H2 becomes H4 etc.
-        return re.sub(r'\n(#+) +(.+)\n', r'\n%s\1 \2\n' % ('#' * level), text)
-%>
-
-<%def name="title(level, string, id=None)">
-    <% id = ' {#%s}' % id if id is not None else '' %>
-${('#' * level) + ' ' + string + id}
-
-
-<%def name="funcdef(f)">
-    <%
-        returns = show_type_annotations and f.return_annotation() or ''
-        if returns:
-            returns = ' -> ' + returns
-    %>
-> `${f.funcdef()} ${f.name}(${', '.join(f.params(annotate=show_type_annotations))})${returns}`
-
-
-<%def name="classdef(c)">
-> `class ${c.name}(${', '.join(c.params(annotate=show_type_annotations))})`
-
-
-<%def name="show_source(d)">
-  % if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None):
-    <% git_link = format_git_link(git_link_template, d) %>
-[[view code]](${git_link})
-  %endif
-
-
----
-description: |
-    API documentation for modules: ${', '.join(m.name for m in modules)}.
-
-lang: en
-
-classoption: oneside
-geometry: margin=1in
-papersize: a4
-
-linkcolor: blue
-links-as-notes: true
-...
-% for module in modules:
-<%
-    submodules = module.submodules()
-    variables = module.variables()
-    functions = module.functions()
-    classes = module.classes()
-
-    def to_md(text):
-        return _to_md(text, module)
-%>
-
--------------------------------------------
-
-${title(1, ('Namespace' if module.is_namespace else 'Module') + ' `%s`' % module.name, module.refname)}
-${module.docstring | to_md}
-
-% if submodules:
-${title(2, 'Sub-modules')}
-    % for m in submodules:
-* [${m.name}](#${m.refname})
-    % endfor
-% endif
-
-% if variables:
-${title(2, 'Variables')}
-    % for v in variables:
-${title(3, 'Variable `%s`' % v.name, v.refname)}
-${show_source(v)}
-${v.docstring | to_md, subh, subh}
-    % endfor
-% endif
-
-% if functions:
-${title(2, 'Functions')}
-    % for f in functions:
-${title(3, 'Function `%s`' % f.name, f.refname)}
-${show_source(f)}
-
-${funcdef(f)}
-
-${f.docstring | to_md, subh, subh}
-    % endfor
-% endif
-
-% if classes:
-${title(2, 'Classes')}
-    % for cls in classes:
-${title(3, 'Class `%s`' % cls.name, cls.refname)}
-${show_source(cls)}
-
-${classdef(cls)}
-
-${cls.docstring | to_md, subh}
-<%
-    class_vars = cls.class_variables(show_inherited_members, sort=sort_identifiers)
-    static_methods = cls.functions(show_inherited_members, sort=sort_identifiers)
-    inst_vars = cls.instance_variables(show_inherited_members, sort=sort_identifiers)
-    methods = cls.methods(show_inherited_members, sort=sort_identifiers)
-    mro = cls.mro()
-    subclasses = cls.subclasses()
-%>
-        % if mro:
-${title(4, 'Ancestors (in MRO)')}
-            % for c in mro:
-* [${c.refname}](#${c.refname})
-            % endfor
-        % endif
-
-        % if subclasses:
-${title(4, 'Descendants')}
-            % for c in subclasses:
-* [${c.refname}](#${c.refname})
-            % endfor
-        % endif
-
-        % if class_vars:
-${title(4, 'Class variables')}
-            % for v in class_vars:
-${title(5, 'Variable `%s`' % v.name, v.refname)}
-${v.docstring | to_md, subh, subh}
-            % endfor
-        % endif
-
-        % if inst_vars:
-${title(4, 'Instance variables')}
-            % for v in inst_vars:
-${title(5, 'Variable `%s`' % v.name, v.refname)}
-${v.docstring | to_md, subh, subh}
-            % endfor
-        % endif
-
-        % if static_methods:
-${title(4, 'Static methods')}
-            % for f in static_methods:
-${title(5, '`Method %s`' % f.name, f.refname)}
-
-${funcdef(f)}
-
-${f.docstring | to_md, subh, subh}
-            % endfor
-        % endif
-
-        % if methods:
-${title(4, 'Methods')}
-            % for f in methods:
-${title(5, 'Method `%s`' % f.name, f.refname)}
-
-${funcdef(f)}
-
-${f.docstring | to_md, subh, subh}
-            % endfor
-        % endif
-    % endfor
-% endif
-
-##\## for module in modules:
-% endfor
-
------
-Generated by *pdoc* ${pdoc.__version__} ().
diff --git a/pdoc_templates/pdoc.css b/pdoc_templates/pdoc.css
deleted file mode 100644
index a563b44..0000000
--- a/pdoc_templates/pdoc.css
+++ /dev/null
@@ -1,381 +0,0 @@
-  .flex {
-    display: flex !important;
-  }
-
-  body {
-    line-height: 1.5em;
-    background: black;
-    color: #DDD;
-    max-width: 140ch;
-  }
-
-  #content {
-    padding: 20px;
-  }
-
-  #sidebar {
-    padding: 30px;
-    overflow: hidden;
-  }
-
-  .http-server-breadcrumbs {
-    font-size: 130%;
-    margin: 0 0 15px 0;
-  }
-
-  #footer {
-    font-size: .75em;
-    padding: 5px 30px;
-    border-top: 1px solid #ddd;
-    text-align: right;
-  }
-    #footer p {
-      margin: 0 0 0 1em;
-      display: inline-block;
-    }
-    #footer p:last-child {
-      margin-right: 30px;
-    }
-
-  h1, h2, h3, h4, h5 {
-    font-weight: 300;
-  }
-  h1 {
-    font-size: 2.5em;
-    line-height: 1.1em;
-    border-top: 20px white;
-  }
-  h2 {
-    font-size: 1.75em;
-    margin: 1em 0 .50em 0;
-  }
-  h3 {
-    font-size: 1.4em;
-    margin: 25px 0 10px 0;
-  }
-  h4 {
-    margin: 0;
-    font-size: 105%;
-  }
-
-  a {
-    color: #999;
-    text-decoration: none;
-    transition: color .3s ease-in-out;
-  }
-  a:hover {
-    color: #18d;
-  }
-
-  .title code {
-    font-weight: bold;
-  }
-  h2[id^="header-"] {
-    margin-top: 2em;
-  }
-  .ident {
-    color: #7ff;
-  }
-
-  pre code {
-    background: transparent;
-    font-size: .8em;
-    line-height: 1.4em;
-  }
-  code {
-    background: #0d0d0e;
-    padding: 1px 4px;
-    overflow-wrap: break-word;
-  }
-  h1 code { background: transparent }
-
-  pre {
-    background: #111;
-    border: 0;
-    border-top: 1px solid #ccc;
-    border-bottom: 1px solid #ccc;
-    margin: 1em 0;
-    padding: 1ex;
-  }
-
-  #http-server-module-list {
-    display: flex;
-    flex-flow: column;
-  }
-    #http-server-module-list div {
-      display: flex;
-    }
-    #http-server-module-list dt {
-      min-width: 10%;
-    }
-    #http-server-module-list p {
-      margin-top: 0;
-    }
-
-  .toc ul,
-  #index {
-    list-style-type: none;
-    margin: 0;
-    padding: 0;
-  }
-    #index code {
-      background: transparent;
-    }
-    #index h3 {
-      border-bottom: 1px solid #ddd;
-    }
-    #index ul {
-      padding: 0;
-    }
-    #index h4 {
-      font-weight: bold;
-    }
-    #index h4 + ul {
-      margin-bottom:.6em;
-    }
-    /* Make TOC lists have 2+ columns when viewport is wide enough.
-       Assuming ~20-character identifiers and ~30% wide sidebar. */
-    @media (min-width: 200ex) { #index .two-column { column-count: 2 } }
-    @media (min-width: 300ex) { #index .two-column { column-count: 3 } }
-
-  dl {
-    margin-bottom: 2em;
-  }
-    dl dl:last-child {
-      margin-bottom: 4em;
-    }
-  dd {
-    margin: 0 0 1em 3em;
-  }
-    #header-classes + dl > dd {
-      margin-bottom: 3em;
-    }
-    dd dd {
-      margin-left: 2em;
-    }
-    dd p {
-      margin: 10px 0;
-    }
-    blockquote code {
-      background: #111;
-      font-weight: bold;
-      font-size: .85em;
-      padding: 5px 10px;
-      display: inline-block;
-      min-width: 40%;
-    }
-      blockquote code:hover {
-        background: #101010;
-      }
-      .name > span:first-child {
-        white-space: nowrap;
-      }
-      .name.class > span:nth-child(2) {
-        margin-left: .4em;
-      }
-    .inherited {
-      color: #777;
-      border-left: 5px solid #eee;
-      padding-left: 1em;
-    }
-    .inheritance em {
-      font-style: normal;
-      font-weight: bold;
-    }
-
-    /* Docstrings titles, e.g. in numpydoc format */
-    .desc h2 {
-      font-weight: 400;
-      font-size: 1.25em;
-    }
-    .desc h3 {
-      font-size: 1em;
-    }
-    .desc dt code {
-      background: inherit;  /* Don't grey-back parameters */
-    }
-
-    .source summary,
-    .git-link-div {
-      color: #aaa;
-      text-align: right;
-      font-weight: 400;
-      font-size: .8em;
-      text-transform: uppercase;
-    }
-      .source summary > * {
-        white-space: nowrap;
-        cursor: pointer;
-      }
-      .git-link {
-        color: inherit;
-        margin-left: 1em;
-      }
-    .source pre {
-      max-height: 500px;
-      overflow: auto;
-      margin: 0;
-    }
-    .source pre code {
-      font-size: 12px;
-      overflow: visible;
-    }
-  .hlist {
-    list-style: none;
-  }
-    .hlist li {
-      display: inline;
-    }
-    .hlist li:after {
-      content: ',\2002';
-    }
-    .hlist li:last-child:after {
-      content: none;
-    }
-    .hlist .hlist {
-      display: inline;
-      padding-left: 1em;
-    }
-
-  img {
-    max-width: 100%;
-  }
-
-  .admonition {
-    padding: .1em .5em;
-    margin-bottom: 1em;
-  }
-    .admonition-title {
-      font-weight: bold;
-    }
-    .admonition.note,
-    .admonition.info,
-    .admonition.important {
-      background: #610;
-    }
-    .admonition.todo,
-    .admonition.versionadded,
-    .admonition.tip,
-    .admonition.hint {
-      background: #202;
-    }
-    .admonition.warning,
-    .admonition.versionchanged,
-    .admonition.deprecated {
-      background: #02b;
-    }
-    .admonition.error,
-    .admonition.danger,
-    .admonition.caution {
-      background: darkpink;
-    }
-
-  @media screen and (min-width: 700px) {
-    #sidebar {
-      width: 30%;
-    }
-    #content {
-      width: 70%;
-      max-width: 100ch;
-      padding: 3em 4em;
-      border-left: 1px solid #ddd;
-    }
-    pre code {
-      font-size: 1em;
-    }
-    .item .name {
-      font-size: 1em;
-    }
-    main {
-      display: flex;
-      flex-direction: row-reverse;
-      justify-content: flex-end;
-    }
-    .toc ul ul,
-    #index ul {
-      padding-left: 1.5em;
-    }
-    .toc > ul > li {
-      margin-top: .5em;
-    }
-  }
-
-@media print {
-  #sidebar h1 {
-    page-break-before: always;
-  }
-  .source {
-    display: none;
-  }
-}
-@media print {
-    * {
-        background: transparent !important;
-        color: #000 !important; /* Black prints faster: h5bp.com/s */
-        box-shadow: none !important;
-        text-shadow: none !important;
-    }
-
-    a[href]:after {
-        content: " (" attr(href) ")";
-        font-size: 90%;
-    }
-    /* Internal, documentation links, recognized by having a title,
-       don't need the URL explicity stated. */
-    a[href][title]:after {
-        content: none;
-    }
-
-    abbr[title]:after {
-        content: " (" attr(title) ")";
-    }
-
-    /*
-     * Don't show links for images, or javascript/internal links
-     */
-
-    .ir a:after,
-    a[href^="javascript:"]:after,
-    a[href^="#"]:after {
-        content: "";
-    }
-
-    pre,
-    blockquote {
-        border: 1px solid #999;
-        page-break-inside: avoid;
-    }
-
-    thead {
-        display: table-header-group; /* h5bp.com/t */
-    }
-
-    tr,
-    img {
-        page-break-inside: avoid;
-    }
-
-    img {
-        max-width: 100% !important;
-    }
-
-    @page {
-        margin: 0.5cm;
-    }
-
-    p,
-    h2,
-    h3 {
-        orphans: 3;
-        widows: 3;
-    }
-
-    h1,
-    h2,
-    h3,
-    h4,
-    h5,
-    h6 {
-        page-break-after: avoid;
-    }
-}
diff --git a/pyproject.toml b/pyproject.toml
index 97df3f9..87d538c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,7 +49,27 @@ dependencies = [
 path = "meanas/__init__.py"
 
 [project.optional-dependencies]
-dev = ["pytest", "coverage", "pdoc", "gridlock"]
+dev = [
+    "pytest",
+    "coverage",
+    "gridlock",
+    "mkdocs>=1.6",
+    "mkdocs-material>=9.5",
+    "mkdocstrings[python]>=0.25",
+    "mkdocs-print-site-plugin>=2.3",
+    "pymdown-extensions>=10.7",
+    "htmlark>=1.0",
+    "ruff>=0.6",
+    ]
+docs = [
+    "mkdocs>=1.6",
+    "mkdocs-material>=9.5",
+    "mkdocstrings[python]>=0.25",
+    "mkdocs-print-site-plugin>=2.3",
+    "pymdown-extensions>=10.7",
+    "htmlark>=1.0",
+    "ruff>=0.6",
+    ]
 examples = [
     "gridlock>=2.1",
     "matplotlib>=3.10.8",

From bedb338ac92da80af53f4f47e39d291d166740dc Mon Sep 17 00:00:00 2001
From: Jan Petykiewicz 
Date: Sat, 18 Apr 2026 22:58:38 -0700
Subject: [PATCH 412/437] [docs] add push_with_docs script

---
 README.md                      | 34 +++++++++++++++++
 mkdocs.yml                     |  2 +-
 scripts/configure_docs_url.sh  | 25 +++++++++++++
 scripts/publish_docs_branch.sh | 52 ++++++++++++++++++++++++++
 scripts/push_with_docs.sh      | 68 ++++++++++++++++++++++++++++++++++
 5 files changed, 180 insertions(+), 1 deletion(-)
 create mode 100755 scripts/configure_docs_url.sh
 create mode 100755 scripts/publish_docs_branch.sh
 create mode 100755 scripts/push_with_docs.sh

diff --git a/README.md b/README.md
index 01bc257..c7809df 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,37 @@ API and workflow docs are generated from the package docstrings with
 [MkDocs](https://www.mkdocs.org/), [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
 and [mkdocstrings](https://mkdocstrings.github.io/).
 
+When hosted on a Forgejo instance, the intended setup is:
+
+- publish the generated site from a dedicated `docs-site` branch
+- serve that branch from the instance's static-pages host
+- point the repository's **Wiki** tab at the published docs URL
+
+This repository now uses a version-controlled wrapper script rather than a
+Forgejo runner. After a successful `git push`, the wrapper builds the docs
+locally and force-updates the `docs-site` branch from the same machine.
+
+Use the wrapper instead of `git push` when publishing from your development
+machine:
+
+```bash
+./scripts/push_with_docs.sh
+```
+
+It only auto-publishes after successful pushes of local `master` to `origin`.
+For unusual refspecs or other remotes, push manually and publish the docs
+branch separately if needed.
+
+To persist the published docs URL for canonical MkDocs links in this clone, set
+the local git config value with:
+
+```bash
+./scripts/configure_docs_url.sh 'https://docs.example.com/meanas/'
+```
+
+The wrapper will also respect a shell-level `DOCS_SITE_URL` override if one is
+set.
+
 Install the docs toolchain with:
 
 ```bash
@@ -138,6 +169,9 @@ This produces:
 - a combined printable single-page HTML site under `site/print_page/`
 - an optional fully inlined `site/standalone.html` when `htmlark` is available
 
+The version-controlled push wrapper publishes this same output to the
+`docs-site` branch.
+
 The docs build uses a local MathJax bundle vendored under `docs/assets/`, so
 the rendered HTML does not rely on external services for equation rendering.
 
diff --git a/mkdocs.yml b/mkdocs.yml
index a6ab1de..837284c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,6 +1,6 @@
 site_name: meanas
 site_description: Electromagnetic simulation tools
-site_url: ""
+site_url: !ENV [DOCS_SITE_URL, ""]
 repo_url: https://mpxd.net/code/jan/meanas
 repo_name: meanas
 docs_dir: docs
diff --git a/scripts/configure_docs_url.sh b/scripts/configure_docs_url.sh
new file mode 100755
index 0000000..ceacc18
--- /dev/null
+++ b/scripts/configure_docs_url.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+set -Eeuo pipefail
+
+ROOT="$(git rev-parse --show-toplevel)"
+cd "$ROOT"
+
+if [[ $# -gt 1 ]]; then
+    echo "usage: $0 [docs-site-url]" >&2
+    exit 2
+fi
+
+if [[ $# -eq 1 ]]; then
+    git config meanas.docsSiteUrl "$1"
+    echo "Configured meanas.docsSiteUrl=$1"
+    exit 0
+fi
+
+CURRENT_URL="$(git config --get meanas.docsSiteUrl || true)"
+if [[ -n "$CURRENT_URL" ]]; then
+    echo "$CURRENT_URL"
+else
+    echo "meanas.docsSiteUrl is not configured" >&2
+    exit 1
+fi
diff --git a/scripts/publish_docs_branch.sh b/scripts/publish_docs_branch.sh
new file mode 100755
index 0000000..0fe1c4e
--- /dev/null
+++ b/scripts/publish_docs_branch.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+set -Eeuo pipefail
+
+if [[ $# -ne 2 ]]; then
+    echo "usage: $0  " >&2
+    exit 2
+fi
+
+SITE_DIR="$1"
+BRANCH="$2"
+
+if [[ ! -d "$SITE_DIR" ]]; then
+    echo "site directory not found: $SITE_DIR" >&2
+    exit 1
+fi
+
+if ! git rev-parse --git-dir >/dev/null 2>&1; then
+    echo "must be run inside a git repository" >&2
+    exit 1
+fi
+
+TMP_DIR="$(mktemp -d)"
+cleanup() {
+    git worktree remove --force "$TMP_DIR" >/dev/null 2>&1 || true
+}
+trap cleanup EXIT
+
+git fetch origin "$BRANCH" || true
+
+if git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
+    git worktree add --detach "$TMP_DIR" "origin/$BRANCH"
+else
+    git worktree add --detach "$TMP_DIR"
+    git -C "$TMP_DIR" checkout --orphan "$BRANCH"
+fi
+
+find "$TMP_DIR" -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
+cp -R "$SITE_DIR"/. "$TMP_DIR"/
+touch "$TMP_DIR/.nojekyll"
+
+git -C "$TMP_DIR" config user.name "${GIT_AUTHOR_NAME:-Forgejo Actions}"
+git -C "$TMP_DIR" config user.email "${GIT_AUTHOR_EMAIL:-forgejo-actions@localhost}"
+git -C "$TMP_DIR" add -A
+
+if git -C "$TMP_DIR" diff --cached --quiet; then
+    echo "no docs changes to publish"
+    exit 0
+fi
+
+git -C "$TMP_DIR" commit -m "Publish docs for ${GITHUB_SHA:-local build}"
+git -C "$TMP_DIR" push --force origin "HEAD:${BRANCH}"
diff --git a/scripts/push_with_docs.sh b/scripts/push_with_docs.sh
new file mode 100755
index 0000000..1b6e259
--- /dev/null
+++ b/scripts/push_with_docs.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+
+set -Eeuo pipefail
+
+ROOT="$(git rev-parse --show-toplevel)"
+cd "$ROOT"
+
+CURRENT_BRANCH="$(git branch --show-current)"
+
+resolve_remote_name() {
+    local branch="$1"
+    shift || true
+
+    for arg in "$@"; do
+        case "$arg" in
+            --)
+                break
+                ;;
+            -*)
+                continue
+                ;;
+            *)
+                if git remote get-url "$arg" >/dev/null 2>&1; then
+                    echo "$arg"
+                    return 0
+                fi
+                ;;
+        esac
+    done
+
+    git config --get "branch.${branch}.pushRemote" \
+        || git config --get remote.pushDefault \
+        || git config --get "branch.${branch}.remote" \
+        || echo origin
+}
+
+REMOTE_NAME="$(resolve_remote_name "$CURRENT_BRANCH" "$@")"
+
+git push "$@"
+
+if [[ "$CURRENT_BRANCH" != "master" ]]; then
+    echo "[meanas docs push] current branch is '${CURRENT_BRANCH:-}' not 'master'; skipping docs publish" >&2
+    exit 0
+fi
+
+if [[ "$REMOTE_NAME" != "origin" ]]; then
+    echo "[meanas docs push] push remote is '${REMOTE_NAME}' not 'origin'; skipping docs publish" >&2
+    exit 0
+fi
+
+if ! command -v mkdocs >/dev/null 2>&1; then
+    echo "[meanas docs push] mkdocs not found; skipping docs publish" >&2
+    exit 0
+fi
+
+DOCS_SITE_URL="${DOCS_SITE_URL:-$(git config --get meanas.docsSiteUrl || true)}"
+export DOCS_SITE_URL
+
+if [[ -n "$DOCS_SITE_URL" ]]; then
+    echo "[meanas docs push] publishing docs for ${DOCS_SITE_URL}" >&2
+else
+    echo "[meanas docs push] DOCS_SITE_URL is unset; building docs with relative site_url" >&2
+fi
+
+echo "[meanas docs push] building docs" >&2
+./make_docs.sh
+echo "[meanas docs push] publishing docs-site branch" >&2
+./scripts/publish_docs_branch.sh site docs-site

From 3dcc4407663d895285d0de7778067ed9359e545c Mon Sep 17 00:00:00 2001
From: Forgejo Actions 
Date: Sun, 19 Apr 2026 00:22:42 -0700
Subject: [PATCH 413/437] Publish docs for local build

---
 .nojekyll                                     |     0
 404.html                                      |   578 +
 api/eigensolvers/index.html                   |  1189 ++
 api/fdfd/index.html                           |  5211 +++++
 api/fdmath/index.html                         |  4603 +++++
 api/fdtd/index.html                           |  3818 ++++
 api/index.html                                |   621 +
 api/meanas/index.html                         |   730 +
 api/waveguides/index.html                     |  6832 +++++++
 assets/_mkdocstrings.css                      |   237 +
 assets/images/favicon.png                     |   Bin 0 -> 1870 bytes
 assets/javascripts/bundle.79ae519e.min.js     |    16 +
 assets/javascripts/bundle.79ae519e.min.js.map |     7 +
 assets/javascripts/lunr/min/lunr.ar.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.da.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.de.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.du.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.el.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.es.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.fi.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.fr.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.he.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.hi.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.hu.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.hy.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.it.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.ja.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.jp.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.kn.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.ko.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.multi.min.js |     1 +
 assets/javascripts/lunr/min/lunr.nl.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.no.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.pt.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.ro.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.ru.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.sa.min.js    |     1 +
 .../lunr/min/lunr.stemmer.support.min.js      |     1 +
 assets/javascripts/lunr/min/lunr.sv.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.ta.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.te.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.th.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.tr.min.js    |    18 +
 assets/javascripts/lunr/min/lunr.vi.min.js    |     1 +
 assets/javascripts/lunr/min/lunr.zh.min.js    |     1 +
 assets/javascripts/lunr/tinyseg.js            |   206 +
 assets/javascripts/lunr/wordcut.js            |  6708 +++++++
 .../workers/search.2c215733.min.js            |    42 +
 .../workers/search.2c215733.min.js.map        |     7 +
 assets/stylesheets/main.484c7ddc.min.css      |     1 +
 assets/stylesheets/main.484c7ddc.min.css.map  |     1 +
 assets/stylesheets/palette.ab4e12ef.min.css   |     1 +
 .../stylesheets/palette.ab4e12ef.min.css.map  |     1 +
 assets/vendor/mathjax/core.js                 |     1 +
 assets/vendor/mathjax/input/asciimath.js      |     1 +
 assets/vendor/mathjax/input/mml.js            |     1 +
 assets/vendor/mathjax/input/mml/entities.js   |     1 +
 assets/vendor/mathjax/input/tex-full.js       |    34 +
 assets/vendor/mathjax/loader.js               |     1 +
 assets/vendor/mathjax/manifest.json           |    38 +
 assets/vendor/mathjax/output/chtml.js         |     1 +
 .../vendor/mathjax/output/chtml/fonts/tex.js  |     1 +
 .../fonts/woff-v2/MathJax_AMS-Regular.woff    |   Bin 0 -> 40808 bytes
 .../woff-v2/MathJax_Calligraphic-Bold.woff    |   Bin 0 -> 9908 bytes
 .../woff-v2/MathJax_Calligraphic-Regular.woff |   Bin 0 -> 9600 bytes
 .../fonts/woff-v2/MathJax_Fraktur-Bold.woff   |   Bin 0 -> 22340 bytes
 .../woff-v2/MathJax_Fraktur-Regular.woff      |   Bin 0 -> 21480 bytes
 .../fonts/woff-v2/MathJax_Main-Bold.woff      |   Bin 0 -> 34464 bytes
 .../fonts/woff-v2/MathJax_Main-Italic.woff    |   Bin 0 -> 20832 bytes
 .../fonts/woff-v2/MathJax_Main-Regular.woff   |   Bin 0 -> 34160 bytes
 .../woff-v2/MathJax_Math-BoldItalic.woff      |   Bin 0 -> 19776 bytes
 .../fonts/woff-v2/MathJax_Math-Italic.woff    |   Bin 0 -> 19360 bytes
 .../fonts/woff-v2/MathJax_Math-Regular.woff   |   Bin 0 -> 19288 bytes
 .../fonts/woff-v2/MathJax_SansSerif-Bold.woff |   Bin 0 -> 15944 bytes
 .../woff-v2/MathJax_SansSerif-Italic.woff     |   Bin 0 -> 14628 bytes
 .../woff-v2/MathJax_SansSerif-Regular.woff    |   Bin 0 -> 12660 bytes
 .../fonts/woff-v2/MathJax_Script-Regular.woff |   Bin 0 -> 11852 bytes
 .../fonts/woff-v2/MathJax_Size1-Regular.woff  |   Bin 0 -> 5792 bytes
 .../fonts/woff-v2/MathJax_Size2-Regular.woff  |   Bin 0 -> 5464 bytes
 .../fonts/woff-v2/MathJax_Size3-Regular.woff  |   Bin 0 -> 3244 bytes
 .../fonts/woff-v2/MathJax_Size4-Regular.woff  |   Bin 0 -> 5148 bytes
 .../woff-v2/MathJax_Typewriter-Regular.woff   |   Bin 0 -> 17604 bytes
 .../fonts/woff-v2/MathJax_Vector-Bold.woff    |   Bin 0 -> 1116 bytes
 .../fonts/woff-v2/MathJax_Vector-Regular.woff |   Bin 0 -> 1136 bytes
 .../chtml/fonts/woff-v2/MathJax_Zero.woff     |   Bin 0 -> 1368 bytes
 assets/vendor/mathjax/startup.js              |     1 +
 css/print-site-material.css                   |    38 +
 css/print-site.css                            |   144 +
 index.html                                    |   733 +
 javascripts/mathjax.js                        |    19 +
 js/print-site.js                              |    42 +
 objects.inv                                   |   Bin 0 -> 1378 bytes
 print_page/index.html                         | 15641 ++++++++++++++++
 search/search_index.json                      |     1 +
 sitemap.xml                                   |    35 +
 sitemap.xml.gz                                |   Bin 0 -> 234 bytes
 standalone.html                               | 13182 +++++++++++++
 stylesheets/extra.css                         |    13 +
 98 files changed, 61024 insertions(+)
 create mode 100644 .nojekyll
 create mode 100644 404.html
 create mode 100644 api/eigensolvers/index.html
 create mode 100644 api/fdfd/index.html
 create mode 100644 api/fdmath/index.html
 create mode 100644 api/fdtd/index.html
 create mode 100644 api/index.html
 create mode 100644 api/meanas/index.html
 create mode 100644 api/waveguides/index.html
 create mode 100644 assets/_mkdocstrings.css
 create mode 100644 assets/images/favicon.png
 create mode 100644 assets/javascripts/bundle.79ae519e.min.js
 create mode 100644 assets/javascripts/bundle.79ae519e.min.js.map
 create mode 100644 assets/javascripts/lunr/min/lunr.ar.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.da.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.de.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.du.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.el.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.es.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.fi.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.fr.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.he.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.hi.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.hu.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.hy.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.it.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.ja.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.jp.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.kn.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.ko.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.multi.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.nl.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.no.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.pt.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.ro.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.ru.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.sa.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.stemmer.support.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.sv.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.ta.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.te.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.th.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.tr.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.vi.min.js
 create mode 100644 assets/javascripts/lunr/min/lunr.zh.min.js
 create mode 100644 assets/javascripts/lunr/tinyseg.js
 create mode 100644 assets/javascripts/lunr/wordcut.js
 create mode 100644 assets/javascripts/workers/search.2c215733.min.js
 create mode 100644 assets/javascripts/workers/search.2c215733.min.js.map
 create mode 100644 assets/stylesheets/main.484c7ddc.min.css
 create mode 100644 assets/stylesheets/main.484c7ddc.min.css.map
 create mode 100644 assets/stylesheets/palette.ab4e12ef.min.css
 create mode 100644 assets/stylesheets/palette.ab4e12ef.min.css.map
 create mode 100644 assets/vendor/mathjax/core.js
 create mode 100644 assets/vendor/mathjax/input/asciimath.js
 create mode 100644 assets/vendor/mathjax/input/mml.js
 create mode 100644 assets/vendor/mathjax/input/mml/entities.js
 create mode 100644 assets/vendor/mathjax/input/tex-full.js
 create mode 100644 assets/vendor/mathjax/loader.js
 create mode 100644 assets/vendor/mathjax/manifest.json
 create mode 100644 assets/vendor/mathjax/output/chtml.js
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/tex.js
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff
 create mode 100644 assets/vendor/mathjax/output/chtml/fonts/woff-v2/MathJax_Zero.woff
 create mode 100644 assets/vendor/mathjax/startup.js
 create mode 100644 css/print-site-material.css
 create mode 100644 css/print-site.css
 create mode 100644 index.html
 create mode 100644 javascripts/mathjax.js
 create mode 100644 js/print-site.js
 create mode 100644 objects.inv
 create mode 100644 print_page/index.html
 create mode 100644 search/search_index.json
 create mode 100644 sitemap.xml
 create mode 100644 sitemap.xml.gz
 create mode 100644 standalone.html
 create mode 100644 stylesheets/extra.css

diff --git a/.nojekyll b/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/404.html b/404.html
new file mode 100644
index 0000000..336ee28
--- /dev/null
+++ b/404.html
@@ -0,0 +1,578 @@
+
+
+
+  
+    
+      
+      
+      
+        
+      
+      
+      
+      
+      
+      
+        
+      
+      
+      
+      
+    
+    
+      
+        meanas
+      
+    
+    
+      
+      
+      
+
+
+    
+    
+      
+    
+    
+      
+    
+    
+      
+    
+      
+    
+      
+    
+      
+    
+    
+    
+      
+
+    
+    
+  
+  
+  
+    
+  
+    
+    
+    
+    
+    
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ +

404 - Not found

+ +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/eigensolvers/index.html b/api/eigensolvers/index.html new file mode 100644 index 0000000..38ea24e --- /dev/null +++ b/api/eigensolvers/index.html @@ -0,0 +1,1189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigensolvers - meanas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + +

eigensolvers

+ + +
+ + + +

+ meanas.eigensolvers + + +

+ +
+ +

Solvers for eigenvalue / eigenvector problems

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ power_iteration + + +

+
power_iteration(
+    operator: spmatrix,
+    guess_vector: NDArray[complex128] | None = None,
+    iterations: int = 20,
+) -> tuple[complex, NDArray[numpy.complex128]]
+
+ +
+ +

Use power iteration to estimate the dominant eigenvector of a matrix.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ operator + + spmatrix + +
+

Matrix to analyze.

+
+
+ required +
+ guess_vector + + NDArray[complex128] | None + +
+

Starting point for the eigenvector. Default is a randomly chosen vector.

+
+
+ None +
+ iterations + + int + +
+

Number of iterations to perform. Default 20.

+
+
+ 20 +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple[complex, NDArray[complex128]] + +
+

(Largest-magnitude eigenvalue, Corresponding eigenvector estimate)

+
+
+ + +
+ +
+ +
+ + +

+ rayleigh_quotient_iteration + + +

+
rayleigh_quotient_iteration(
+    operator: spmatrix | LinearOperator,
+    guess_vector: NDArray[complex128],
+    iterations: int = 40,
+    tolerance: float = 1e-13,
+    solver: Callable[..., NDArray[complex128]]
+    | None = None,
+) -> tuple[complex, NDArray[numpy.complex128]]
+
+ +
+ +

Use Rayleigh quotient iteration to refine an eigenvector guess.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ operator + + spmatrix | LinearOperator + +
+

Matrix to analyze.

+
+
+ required +
+ guess_vector + + NDArray[complex128] + +
+

Eigenvector to refine.

+
+
+ required +
+ iterations + + int + +
+

Maximum number of iterations to perform. Default 40.

+
+
+ 40 +
+ tolerance + + float + +
+

Stop iteration if (A - I*eigenvalue) @ v < num_vectors * tolerance, + Default 1e-13.

+
+
+ 1e-13 +
+ solver + + Callable[..., NDArray[complex128]] | None + +
+

Solver function of the form x = solver(A, b). + By default, use scipy.sparse.spsolve for sparse matrices and + scipy.sparse.bicgstab for general LinearOperator instances.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple[complex, NDArray[complex128]] + +
+

(eigenvalues, eigenvectors)

+
+
+ + +
+ +
+ +
+ + +

+ signed_eigensolve + + +

+
signed_eigensolve(
+    operator: spmatrix | LinearOperator,
+    how_many: int,
+    negative: bool = False,
+) -> tuple[
+    NDArray[numpy.complex128], NDArray[numpy.complex128]
+]
+
+ +
+ +

Find the largest-magnitude positive-only (or negative-only) eigenvalues and + eigenvectors of the provided matrix.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ operator + + spmatrix | LinearOperator + +
+

Matrix to analyze.

+
+
+ required +
+ how_many + + int + +
+

How many eigenvalues to find.

+
+
+ required +
+ negative + + bool + +
+

Whether to find negative-only eigenvalues. + Default False (positive only).

+
+
+ False +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ NDArray[complex128] + +
+

(sorted list of eigenvalues, 2D ndarray of corresponding eigenvectors)

+
+
+ NDArray[complex128] + +
+

eigenvectors[:, k] corresponds to the k-th eigenvalue

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/fdfd/index.html b/api/fdfd/index.html new file mode 100644 index 0000000..07b78ef --- /dev/null +++ b/api/fdfd/index.html @@ -0,0 +1,5211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fdfd - meanas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + +

fdfd

+ + +
+ + + +

+ meanas.fdfd + + +

+ +
+ +

Tools for finite difference frequency-domain (FDFD) simulations and calculations.

+

These mostly involve picking a single frequency, then setting up and solving a +matrix equation (Ax=b) or eigenvalue problem.

+

Submodules:

+
    +
  • operators, functional: General FDFD problem setup.
  • +
  • solvers: Solver interface and reference implementation.
  • +
  • scpml: Stretched-coordinate perfectly matched layer (SCPML) boundary conditions.
  • +
  • waveguide_2d: Operators and mode-solver for waveguides with constant cross-section.
  • +
  • waveguide_3d: Functions for transforming waveguide_2d results into 3D, + including mode-source and overlap-window construction.
  • +
  • farfield, bloch, eme: specialized helper modules for near/far transforms, + Bloch-periodic problems, and eigenmode expansion.
  • +
+

================================================================

+

From the "Frequency domain" section of meanas.fdmath, we have

+
\[ + \begin{aligned} + \tilde{E}_{l, \vec{r}} &= \tilde{E}_{\vec{r}} e^{-\imath \omega l \Delta_t} \\ + \tilde{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &= \tilde{H}_{\vec{r} + \frac{1}{2}} e^{-\imath \omega (l - \frac{1}{2}) \Delta_t} \\ + \tilde{J}_{l, \vec{r}} &= \tilde{J}_{\vec{r}} e^{-\imath \omega (l - \frac{1}{2}) \Delta_t} \\ + \tilde{M}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &= \tilde{M}_{\vec{r} + \frac{1}{2}} e^{-\imath \omega l \Delta_t} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{\vec{r}}) + -\Omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} &= -\imath \Omega \tilde{J}_{\vec{r}} e^{\imath \omega \Delta_t / 2} \\ + \Omega &= 2 \sin(\omega \Delta_t / 2) / \Delta_t + \end{aligned} +\]
+

resulting in

+
\[ + \begin{aligned} + \tilde{\partial}_t &\Rightarrow -\imath \Omega e^{-\imath \omega \Delta_t / 2}\\ + \hat{\partial}_t &\Rightarrow -\imath \Omega e^{ \imath \omega \Delta_t / 2}\\ + \end{aligned} +\]
+

Maxwell's equations are then

+
\[ + \begin{aligned} + \tilde{\nabla} \times \tilde{E}_{\vec{r}} &= + \imath \Omega e^{-\imath \omega \Delta_t / 2} \hat{B}_{\vec{r} + \frac{1}{2}} + - \hat{M}_{\vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times \hat{H}_{\vec{r} + \frac{1}{2}} &= + -\imath \Omega e^{ \imath \omega \Delta_t / 2} \tilde{D}_{\vec{r}} + + \tilde{J}_{\vec{r}} \\ + \tilde{\nabla} \cdot \hat{B}_{\vec{r} + \frac{1}{2}} &= 0 \\ + \hat{\nabla} \cdot \tilde{D}_{\vec{r}} &= \rho_{\vec{r}} + \end{aligned} +\]
+

With \(\Delta_t \to 0\), this simplifies to

+
\[ + \begin{aligned} + \tilde{E}_{l, \vec{r}} &\to \tilde{E}_{\vec{r}} \\ + \tilde{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &\to \tilde{H}_{\vec{r} + \frac{1}{2}} \\ + \tilde{J}_{l, \vec{r}} &\to \tilde{J}_{\vec{r}} \\ + \tilde{M}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} &\to \tilde{M}_{\vec{r} + \frac{1}{2}} \\ + \Omega &\to \omega \\ + \tilde{\partial}_t &\to -\imath \omega \\ + \hat{\partial}_t &\to -\imath \omega \\ + \end{aligned} +\]
+

and then

+
\[ + \begin{aligned} + \tilde{\nabla} \times \tilde{E}_{\vec{r}} &= + \imath \omega \hat{B}_{\vec{r} + \frac{1}{2}} + - \hat{M}_{\vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times \hat{H}_{\vec{r} + \frac{1}{2}} &= + -\imath \omega \tilde{D}_{\vec{r}} + + \tilde{J}_{\vec{r}} \\ + \end{aligned} +\]
+
\[ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{\vec{r}}) + -\omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath \omega \tilde{J}_{\vec{r}} \\ +\]
+ + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +

Core operator layers

+ + +
+ + + +

+ meanas.fdfd.functional + + +

+ +
+ +

Functional versions of many FDFD operators. These can be useful for performing + FDFD calculations without needing to construct large matrices in memory.

+

The functions generated here expect cfdfield_t inputs with shape (3, X, Y, Z), +e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z)

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ e_full + + +

+
e_full(
+    omega: complex,
+    dxes: dx_lists_t,
+    epsilon: fdfield,
+    mu: fdfield | None = None,
+) -> cfdfield_updater_t
+
+ +
+ +

Wave operator for use with E-field. See operators.e_full for details.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ epsilon + + fdfield + +
+

Dielectric constant

+
+
+ required +
+ mu + + fdfield | None + +
+

Magnetic permeability (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ cfdfield_updater_t + +
+

Function f implementing the wave operator

+
+
+ cfdfield_updater_t + +
+

f(E) -> -i * omega * J

+
+
+ + +
+ +
+ +
+ + +

+ eh_full + + +

+
eh_full(
+    omega: complex,
+    dxes: dx_lists_t,
+    epsilon: fdfield,
+    mu: fdfield | None = None,
+) -> Callable[
+    [cfdfield, cfdfield], tuple[cfdfield_t, cfdfield_t]
+]
+
+ +
+ +

Wave operator for full (both E and H) field representation. +See operators.eh_full.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ epsilon + + fdfield + +
+

Dielectric constant

+
+
+ required +
+ mu + + fdfield | None + +
+

Magnetic permeability (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ Callable[[cfdfield, cfdfield], tuple[cfdfield_t, cfdfield_t]] + +
+

Function f implementing the wave operator

+
+
+ Callable[[cfdfield, cfdfield], tuple[cfdfield_t, cfdfield_t]] + +
+

f(E, H) -> (J, -M)

+
+
+ + +
+ +
+ +
+ + +

+ e2h + + +

+
e2h(
+    omega: complex,
+    dxes: dx_lists_t,
+    mu: fdfield | None = None,
+) -> cfdfield_updater_t
+
+ +
+ +

Utility operator for converting the E field into the H field. +For use with e_full -- assumes that there is no magnetic current M.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ mu + + fdfield | None + +
+

Magnetic permeability (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ cfdfield_updater_t + +
+

Function f for converting E to H,

+
+
+ cfdfield_updater_t + +
+

f(E) -> H

+
+
+ + +
+ +
+ +
+ + +

+ m2j + + +

+
m2j(
+    omega: complex,
+    dxes: dx_lists_t,
+    mu: fdfield | None = None,
+) -> cfdfield_updater_t
+
+ +
+ +

Utility operator for converting magnetic current M distribution +into equivalent electric current distribution J. +For use with e.g. e_full.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ mu + + fdfield | None + +
+

Magnetic permeability (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ cfdfield_updater_t + +
+

Function f for converting M to J,

+
+
+ cfdfield_updater_t + +
+

f(M) -> J

+
+
+ + +
+ +
+ +
+ + +

+ e_tfsf_source + + +

+
e_tfsf_source(
+    TF_region: fdfield,
+    omega: complex,
+    dxes: dx_lists_t,
+    epsilon: fdfield,
+    mu: fdfield | None = None,
+) -> cfdfield_updater_t
+
+ +
+ +

Operator that turns an E-field distribution into a total-field/scattered-field +(TFSF) source.

+

If A is the full wave operator from e_full(...) and Q is the diagonal +mask selecting the total-field region, then the TFSF source is the commutator

+
\[ +\frac{A Q - Q A}{-i \omega} E. +\]
+

This vanishes in the interior of the total-field and scattered-field regions +and is supported only at their shared boundary, where the mask discontinuity +makes A and Q fail to commute. The returned current is therefore the +distributed source needed to inject the desired total field without also +forcing the scattered-field region.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ TF_region + + fdfield + +
+

mask which is set to 1 in the total-field region, and 0 elsewhere + (i.e. in the scattered-field region). + Should have the same shape as the simulation grid, e.g. epsilon[0].shape.

+
+
+ required +
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ epsilon + + fdfield + +
+

Dielectric constant distribution

+
+
+ required +
+ mu + + fdfield | None + +
+

Magnetic permeability (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ cfdfield_updater_t + +
+

Function f which takes an E field and returns a current distribution,

+
+
+ cfdfield_updater_t + +
+

f(E) -> J

+
+
+ + +
+ +
+ +
+ + +

+ poynting_e_cross_h + + +

+
poynting_e_cross_h(
+    dxes: dx_lists_t,
+) -> Callable[[cfdfield, cfdfield], cfdfield_t]
+
+ +
+ +

Generates a function that takes the single-frequency E and H fields +and calculates the cross product E x H = \(E \times H\) as required +for the Poynting vector, \(S = E \times H\).

+

On the Yee grid, the electric and magnetic components are not stored at the +same locations. This helper therefore applies the same one-cell electric-field +shifts used by the sparse operators.poynting_e_cross(...) construction so +that the discrete cross product matches the face-centered energy flux used in +meanas.fdtd.energy.poynting(...).

+ + +
+ Note +

This function also shifts the input E field by one cell as required +for computing the Poynting cross product (see meanas.fdfd module docs).

+
+ +
+ Note +

If E and H are peak amplitudes as assumed elsewhere in this code, +the time-average of the poynting vector is <S> = Re(S)/2 = Re(E x H*) / 2. +The factor of 1/2 can be omitted if root-mean-square quantities are used +instead.

+
+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ Callable[[cfdfield, cfdfield], cfdfield_t] + +
+

Function f that returns the staggered-grid cross product E \times H.

+
+
+ Callable[[cfdfield, cfdfield], cfdfield_t] + +
+

For time-average power, call it as f(E, H.conj()) and take Re(...) / 2.

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdfd.operators + + +

+ +
+ +

Sparse matrix operators for use with electromagnetic wave equations.

+

These functions return sparse-matrix (scipy.sparse.sparray) representations of + a variety of operators, intended for use with E and H fields vectorized using the + meanas.fdmath.vectorization.vec() and meanas.fdmath.vectorization.unvec() functions.

+

E- and H-field values are defined on a Yee cell; epsilon values should be calculated for + cells centered at each E component (mu at each H component).

+

Many of these functions require a dxes parameter, of type dx_lists_t; see +the meanas.fdmath.types submodule for details.

+

The following operators are included:

+
    +
  • E-only wave operator
  • +
  • H-only wave operator
  • +
  • EH wave operator
  • +
  • Curl for use with E, H fields
  • +
  • E to H conversion
  • +
  • M to J conversion
  • +
  • Poynting cross products
  • +
  • Circular shifts
  • +
  • Discrete derivatives
  • +
  • Averaging operators
  • +
  • Cross product matrices
  • +
+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ e_full + + +

+
e_full(
+    omega: complex,
+    dxes: dx_lists_t,
+    epsilon: vfdfield | vcfdfield,
+    mu: vfdfield | None = None,
+    pec: vfdfield | None = None,
+    pmc: vfdfield | None = None,
+) -> sparse.sparray
+
+ +
+ +

Wave operator + $$ \nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon $$

+
del x (1/mu * del x) - omega**2 * epsilon
+
+

for use with the E-field, with wave equation + $$ (\nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon) E = -\imath \omega J $$

+
(del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J
+
+

To make this matrix symmetric, use the preconditioners from e_full_preconditioners().

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ epsilon + + vfdfield | vcfdfield + +
+

Vectorized dielectric constant

+
+
+ required +
+ mu + + vfdfield | None + +
+

Vectorized magnetic permeability (default 1 everywhere).

+
+
+ None +
+ pec + + vfdfield | None + +
+

Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted +as containing a perfect electrical conductor (PEC). +The PEC is applied per-field-component (i.e. pec.size == epsilon.size)

+
+
+ None +
+ pmc + + vfdfield | None + +
+

Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted +as containing a perfect magnetic conductor (PMC). +The PMC is applied per-field-component (i.e. pmc.size == epsilon.size)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix containing the wave operator.

+
+
+ + +
+ +
+ +
+ + +

+ e_full_preconditioners + + +

+
e_full_preconditioners(
+    dxes: dx_lists_t,
+) -> tuple[sparse.sparray, sparse.sparray]
+
+ +
+ +

Left and right preconditioners (Pl, Pr) for symmetrizing the e_full wave operator.

+

The preconditioned matrix A_symm = (Pl @ A @ Pr) is complex-symmetric + (non-Hermitian unless there is no loss or PMLs).

+

The preconditioner matrices are diagonal and complex, with Pr = 1 / Pl

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple[sparray, sparray] + +
+

Preconditioner matrices (Pl, Pr).

+
+
+ + +
+ +
+ +
+ + +

+ h_full + + +

+
h_full(
+    omega: complex,
+    dxes: dx_lists_t,
+    epsilon: vfdfield,
+    mu: vfdfield | None = None,
+    pec: vfdfield | None = None,
+    pmc: vfdfield | None = None,
+) -> sparse.sparray
+
+ +
+ +

Wave operator + $$ \nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu $$

+
del x (1/epsilon * del x) - omega**2 * mu
+
+

for use with the H-field, with wave equation + $$ (\nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu) E = \imath \omega M $$

+
(del x (1/epsilon * del x) - omega**2 * mu) E = i * omega * M
+
+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ epsilon + + vfdfield + +
+

Vectorized dielectric constant

+
+
+ required +
+ mu + + vfdfield | None + +
+

Vectorized magnetic permeability (default 1 everywhere)

+
+
+ None +
+ pec + + vfdfield | None + +
+

Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted +as containing a perfect electrical conductor (PEC). +The PEC is applied per-field-component (i.e. pec.size == epsilon.size)

+
+
+ None +
+ pmc + + vfdfield | None + +
+

Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted +as containing a perfect magnetic conductor (PMC). +The PMC is applied per-field-component (i.e. pmc.size == epsilon.size)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix containing the wave operator.

+
+
+ + +
+ +
+ +
+ + +

+ eh_full + + +

+
eh_full(
+    omega: complex,
+    dxes: dx_lists_t,
+    epsilon: vfdfield,
+    mu: vfdfield | None = None,
+    pec: vfdfield | None = None,
+    pmc: vfdfield | None = None,
+) -> sparse.sparray
+
+ +
+ +

Wave operator for [E, H] field representation. This operator implements Maxwell's + equations without cancelling out either E or H. The operator is +$$ \begin{bmatrix} + -\imath \omega \epsilon & \nabla \times \ + \nabla \times & \imath \omega \mu + \end{bmatrix} $$

+
[[-i * omega * epsilon,  del x         ],
+ [del x,                 i * omega * mu]]
+
+

for use with a field vector of the form cat(vec(E), vec(H)): +$$ \begin{bmatrix} + -\imath \omega \epsilon & \nabla \times \ + \nabla \times & \imath \omega \mu + \end{bmatrix} + \begin{bmatrix} E \ + H + \end{bmatrix} + = \begin{bmatrix} J \ + -M + \end{bmatrix} $$

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ epsilon + + vfdfield + +
+

Vectorized dielectric constant

+
+
+ required +
+ mu + + vfdfield | None + +
+

Vectorized magnetic permeability (default 1 everywhere)

+
+
+ None +
+ pec + + vfdfield | None + +
+

Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted +as containing a perfect electrical conductor (PEC). +The PEC is applied per-field-component (i.e. pec.size == epsilon.size)

+
+
+ None +
+ pmc + + vfdfield | None + +
+

Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted +as containing a perfect magnetic conductor (PMC). +The PMC is applied per-field-component (i.e. pmc.size == epsilon.size)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix containing the wave operator.

+
+
+ + +
+ +
+ +
+ + +

+ e2h + + +

+
e2h(
+    omega: complex,
+    dxes: dx_lists_t,
+    mu: vfdfield | None = None,
+    pmc: vfdfield | None = None,
+) -> sparse.sparray
+
+ +
+ +

Utility operator for converting the E field into the H field. +For use with e_full() -- assumes that there is no magnetic current M.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ mu + + vfdfield | None + +
+

Vectorized magnetic permeability (default 1 everywhere)

+
+
+ None +
+ pmc + + vfdfield | None + +
+

Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted +as containing a perfect magnetic conductor (PMC). +The PMC is applied per-field-component (i.e. pmc.size == epsilon.size)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix for converting E to H.

+
+
+ + +
+ +
+ +
+ + +

+ m2j + + +

+
m2j(
+    omega: complex,
+    dxes: dx_lists_t,
+    mu: vfdfield | None = None,
+) -> sparse.sparray
+
+ +
+ +

Operator for converting a magnetic current M into an electric current J. +For use with eg. e_full().

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ mu + + vfdfield | None + +
+

Vectorized magnetic permeability (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix for converting M to J.

+
+
+ + +
+ +
+ +
+ + +

+ poynting_e_cross + + +

+
poynting_e_cross(
+    e: vcfdfield, dxes: dx_lists_t
+) -> sparse.sparray
+
+ +
+ +

Operator for computing the staggered-grid (E \times) part of the Poynting vector.

+

On the Yee grid the E and H components live on different edges, so the +electric field must be shifted by one cell in the appropriate direction +before forming the discrete cross product. This sparse operator encodes that +shifted cross product directly and is the matrix equivalent of +functional.poynting_e_cross_h(...).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ e + + vcfdfield + +
+

Vectorized E-field for the ExH cross product

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix containing the (E \times) part of the staggered Poynting

+
+
+ sparray + +
+

cross product.

+
+
+ + +
+ +
+ +
+ + +

+ poynting_h_cross + + +

+
poynting_h_cross(
+    h: vcfdfield, dxes: dx_lists_t
+) -> sparse.sparray
+
+ +
+ +

Operator for computing the staggered-grid (H \times) part of the Poynting vector.

+

Together with poynting_e_cross(...), this provides the matrix form of the +Yee-grid cross product used in the flux helpers. The two are related by the +usual antisymmetry of the cross product,

+
\[ +H \times E = -(E \times H), +\]
+

once the same staggered field placement is used on both sides.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ h + + vcfdfield + +
+

Vectorized H-field for the HxE cross product

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix containing the (H \times) part of the staggered Poynting

+
+
+ sparray + +
+

cross product.

+
+
+ + +
+ +
+ +
+ + +

+ e_tfsf_source + + +

+
e_tfsf_source(
+    TF_region: vfdfield,
+    omega: complex,
+    dxes: dx_lists_t,
+    epsilon: vfdfield,
+    mu: vfdfield | None = None,
+) -> sparse.sparray
+
+ +
+ +

Operator that turns a desired E-field distribution into a + total-field/scattered-field (TFSF) source.

+

Let A be the full wave operator from e_full(...), and let +Q = \mathrm{diag}(TF_region) be the projector onto the total-field region. +Then the TFSF current operator is the commutator

+
\[ +\frac{A Q - Q A}{-i \omega}. +\]
+

Inside regions where Q is locally constant, A and Q commute and the +source vanishes. Only cells at the TF/SF boundary contribute nonzero current, +which is exactly the desired distributed source for injecting the chosen +field into the total-field region without directly forcing the +scattered-field region.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ TF_region + + vfdfield + +
+

Mask, which is set to 1 inside the total-field region and 0 in the + scattered-field region

+
+
+ required +
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ epsilon + + vfdfield + +
+

Vectorized dielectric constant

+
+
+ required +
+ mu + + vfdfield | None + +
+

Vectorized magnetic permeability (default 1 everywhere).

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix that turns an E-field into a current (J) distribution.

+
+
+ + +
+ +
+ +
+ + +

+ e_boundary_source + + +

+
e_boundary_source(
+    mask: vfdfield,
+    omega: complex,
+    dxes: dx_lists_t,
+    epsilon: vfdfield,
+    mu: vfdfield | None = None,
+    periodic_mask_edges: bool = False,
+) -> sparse.sparray
+
+ +
+ +

Operator that turns an E-field distrubtion into a current (J) distribution + along the edges (external and internal) of the provided mask. This is just an + e_tfsf_source() with an additional masking step.

+

Equivalently, this helper first constructs the TFSF commutator source for the +full mask and then zeroes out all cells except the mask boundary. The +boundary is defined as the set of cells whose mask value changes under a +one-cell shift in any Cartesian direction. With periodic_mask_edges=False +the shifts mirror at the domain boundary; with True they wrap periodically.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ mask + + vfdfield + +
+

The current distribution is generated at the edges of the mask, + i.e. any points where shifting the mask by one cell in any direction + would change its value.

+
+
+ required +
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ epsilon + + vfdfield + +
+

Vectorized dielectric constant

+
+
+ required +
+ mu + + vfdfield | None + +
+

Vectorized magnetic permeability (default 1 everywhere).

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix that turns an E-field into a current (J) distribution.

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdfd.solvers + + +

+ +
+ +

Solvers and solver interface for FDFD problems.

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ generic + + +

+
generic(
+    omega: complex,
+    dxes: dx_lists_t,
+    J: vcfdfield,
+    epsilon: vfdfield,
+    mu: vfdfield | None = None,
+    *,
+    pec: vfdfield | None = None,
+    pmc: vfdfield | None = None,
+    adjoint: bool = False,
+    matrix_solver: Callable[..., ArrayLike] = _scipy_qmr,
+    matrix_solver_opts: dict[str, Any] | None = None,
+    E_guess: vcfdfield | None = None,
+) -> vcfdfield_t
+
+ +
+ +

Conjugate gradient FDFD solver using CSR sparse matrices.

+

All ndarray arguments should be 1D arrays, as returned by meanas.fdmath.vectorization.vec().

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

Complex frequency to solve at.

+
+
+ required +
+ dxes + + dx_lists_t + +
+

[[dx_e, dy_e, dz_e], [dx_h, dy_h, dz_h]] (complex cell sizes) as +discussed in meanas.fdmath.types

+
+
+ required +
+ J + + vcfdfield + +
+

Electric current distribution (at E-field locations)

+
+
+ required +
+ epsilon + + vfdfield + +
+

Dielectric constant distribution (at E-field locations)

+
+
+ required +
+ mu + + vfdfield | None + +
+

Magnetic permeability distribution (at H-field locations)

+
+
+ None +
+ pec + + vfdfield | None + +
+

Perfect electric conductor distribution + (at E-field locations; non-zero value indicates PEC is present)

+
+
+ None +
+ pmc + + vfdfield | None + +
+

Perfect magnetic conductor distribution + (at H-field locations; non-zero value indicates PMC is present)

+
+
+ None +
+ adjoint + + bool + +
+

If true, solves the adjoint problem.

+
+
+ False +
+ matrix_solver + + Callable[..., ArrayLike] + +
+

Called as matrix_solver(A, b, **matrix_solver_opts) -> x, + where A: scipy.sparse.csr_array; + b: ArrayLike; + x: ArrayLike; + Default is a wrapped version of scipy.sparse.linalg.qmr() + which doesn't return convergence info and logs the residual + every 100 iterations.

+
+
+ _scipy_qmr +
+ matrix_solver_opts + + dict[str, Any] | None + +
+

Passed as kwargs to matrix_solver(...)

+
+
+ None +
+ E_guess + + vcfdfield | None + +
+

Guess at the solution E-field. matrix_solver must accept an + x0 argument with the same purpose.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ vcfdfield_t + +
+

E-field which solves the system.

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdfd.scpml + + +

+ +
+ +

Functions for creating stretched coordinate perfectly matched layer (PML) absorbers.

+ + + + + + + + + + +
+ + + + + + + +
+ + + +

+ s_function_t + + + + module-attribute + + +

+
s_function_t = Callable[
+    [NDArray[float64]], NDArray[float64]
+]
+
+ +
+ +

Typedef for s-functions, see prepare_s_function()

+ +
+ +
+ + + + +
+ + +

+ prepare_s_function + + +

+
prepare_s_function(
+    ln_R: float = -16, m: float = 4
+) -> s_function_t
+
+ +
+ +

Create an s_function to pass to the SCPML functions. This is used when you would like to +customize the PML parameters.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ ln_R + + float + +
+

Natural logarithm of the desired reflectance

+
+
+ -16 +
+ m + + float + +
+

Polynomial order for the PML (imaginary part increases as distance ** m)

+
+
+ 4 +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + + + + + +
TypeDescription
+ s_function_t + +
+

An s_function, which takes an ndarray (distances) and returns an ndarray (complex part

+
+
+ s_function_t + +
+

of the cell width; needs to be divided by sqrt(epilon_effective) * real(omega))

+
+
+ s_function_t + +
+

before use.

+
+
+ + +
+ +
+ +
+ + +

+ uniform_grid_scpml + + +

+
uniform_grid_scpml(
+    shape: Sequence[int],
+    thicknesses: Sequence[int],
+    omega: float,
+    epsilon_effective: float = 1.0,
+    s_function: s_function_t | None = None,
+) -> list[list[NDArray[numpy.float64]]]
+
+ +
+ +

Create dx arrays for a uniform grid with a cell width of 1 and a pml.

+

If you want something more fine-grained, check out stretch_with_scpml(...).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ shape + + Sequence[int] + +
+

Shape of the grid, including the PMLs (which are 2*thicknesses thick)

+
+
+ required +
+ thicknesses + + Sequence[int] + +
+

[th_x, th_y, th_z] + Thickness of the PML in each direction. + Both polarities are added. + Each th_ of pml is applied twice, once on each edge of the grid along the given axis. + th_* may be zero, in which case no pml is added.

+
+
+ required +
+ omega + + float + +
+

Angular frequency for the simulation

+
+
+ required +
+ epsilon_effective + + float + +
+

Effective epsilon of the PML. Match this to the material + at the edge of your grid. + Default 1.

+
+
+ 1.0 +
+ s_function + + s_function_t | None + +
+

created by prepare_s_function(...), allowing customization of pml parameters. + Default uses prepare_s_function() with no parameters.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[list[NDArray[float64]]] + +
+

Complex cell widths (dx_lists_mut) as discussed in meanas.fdmath.types.

+
+
+ + +
+ +
+ +
+ + +

+ stretch_with_scpml + + +

+
stretch_with_scpml(
+    dxes: list[list[NDArray[float64]]],
+    axis: int,
+    polarity: int,
+    omega: float,
+    epsilon_effective: float = 1.0,
+    thickness: int = 10,
+    s_function: s_function_t | None = None,
+) -> list[list[NDArray[numpy.float64]]]
+
+ +
+ +

Stretch dxes to contain a stretched-coordinate PML (SCPML) in one direction along one axis.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dxes + + list[list[NDArray[float64]]] + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ axis + + int + +
+

axis to stretch (0=x, 1=y, 2=z)

+
+
+ required +
+ polarity + + int + +
+

direction to stretch (-1 for -ve, +1 for +ve)

+
+
+ required +
+ omega + + float + +
+

Angular frequency for the simulation

+
+
+ required +
+ epsilon_effective + + float + +
+

Effective epsilon of the PML. Match this to the material at the + edge of your grid. Default 1.

+
+
+ 1.0 +
+ thickness + + int + +
+

number of cells to use for pml (default 10)

+
+
+ 10 +
+ s_function + + s_function_t | None + +
+

Created by prepare_s_function(...), allowing customization + of pml parameters. Default uses prepare_s_function() with no parameters.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ list[list[NDArray[float64]]] + +
+

Complex cell widths (dx_lists_mut) as discussed in meanas.fdmath.types.

+
+
+ list[list[NDArray[float64]]] + +
+

Multiple calls to this function may be necessary if multiple absorpbing boundaries are needed.

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdfd.farfield + + +

+ +
+ +

Functions for performing near-to-farfield transformation (and the reverse).

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ near_to_farfield + + +

+
near_to_farfield(
+    E_near: cfdfield_t,
+    H_near: cfdfield_t,
+    dx: float,
+    dy: float,
+    padded_size: list[int] | int | None = None,
+) -> dict[str, Any]
+
+ +
+ +

Compute the farfield, i.e. the distribution of the fields after propagation + through several wavelengths of uniform medium.

+

The input fields should be complex phasors.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ E_near + + cfdfield_t + +
+

List of 2 ndarrays containing the 2D phasor field slices for the transverse + E fields (e.g. [Ex, Ey] for calculating the farfield toward the z-direction).

+
+
+ required +
+ H_near + + cfdfield_t + +
+

List of 2 ndarrays containing the 2D phasor field slices for the transverse + H fields (e.g. [Hx, hy] for calculating the farfield towrad the z-direction).

+
+
+ required +
+ dx + + float + +
+

Cell size along x-dimension, in units of wavelength.

+
+
+ required +
+ dy + + float + +
+

Cell size along y-dimension, in units of wavelength.

+
+
+ required +
+ padded_size + + list[int] | int | None + +
+

Shape of the output. A single integer n will be expanded to (n, n). + Powers of 2 are most efficient for FFT computation. + Default is the smallest power of 2 larger than the input, for each axis.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDescription
+ dict[str, Any] + +
+

Dict with keys

+
+
+ dict[str, Any] + +
+
    +
  • E_far: Normalized E-field farfield; multiply by + (i k exp(-i k r) / (4 pi r)) to get the actual field value.
  • +
+
+
+ dict[str, Any] + +
+
    +
  • H_far: Normalized H-field farfield; multiply by + (i k exp(-i k r) / (4 pi r)) to get the actual field value.
  • +
+
+
+ dict[str, Any] + +
+
    +
  • kx, ky: Wavevector values corresponding to the x- and y- axes in E_far and H_far, + normalized to wavelength (dimensionless).
  • +
+
+
+ dict[str, Any] + +
+
    +
  • dkx, dky: step size for kx and ky, normalized to wavelength.
  • +
+
+
+ dict[str, Any] + +
+
    +
  • theta: arctan2(ky, kx) corresponding to each (kx, ky). + This is the angle in the x-y plane, counterclockwise from above, starting from +x.
  • +
+
+
+ dict[str, Any] + +
+
    +
  • phi: arccos(kz / k) corresponding to each (kx, ky). + This is the angle away from +z.
  • +
+
+
+ + +
+ +
+ +
+ + +

+ far_to_nearfield + + +

+
far_to_nearfield(
+    E_far: cfdfield_t,
+    H_far: cfdfield_t,
+    dkx: float,
+    dky: float,
+    padded_size: list[int] | int | None = None,
+) -> dict[str, Any]
+
+ +
+ +

Compute the farfield, i.e. the distribution of the fields after propagation + through several wavelengths of uniform medium.

+

The input fields should be complex phasors.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ E_far + + cfdfield_t + +
+

List of 2 ndarrays containing the 2D phasor field slices for the transverse + E fields (e.g. [Ex, Ey] for calculating the nearfield toward the z-direction). + Fields should be normalized so that + E_far = E_far_actual / (i k exp(-i k r) / (4 pi r))

+
+
+ required +
+ H_far + + cfdfield_t + +
+

List of 2 ndarrays containing the 2D phasor field slices for the transverse + H fields (e.g. [Hx, hy] for calculating the nearfield toward the z-direction). + Fields should be normalized so that + H_far = H_far_actual / (i k exp(-i k r) / (4 pi r))

+
+
+ required +
+ dkx + + float + +
+

kx discretization, in units of wavelength.

+
+
+ required +
+ dky + + float + +
+

ky discretization, in units of wavelength.

+
+
+ required +
+ padded_size + + list[int] | int | None + +
+

Shape of the output. A single integer n will be expanded to (n, n). + Powers of 2 are most efficient for FFT computation. + Default is the smallest power of 2 larger than the input, for each axis.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDescription
+ dict[str, Any] + +
+

Dict with keys

+
+
+ dict[str, Any] + +
+
    +
  • E: E-field nearfield
  • +
+
+
+ dict[str, Any] + +
+
    +
  • H: H-field nearfield
  • +
+
+
+ dict[str, Any] + +
+
    +
  • dx, dy: spatial discretization, normalized to wavelength (dimensionless)
  • +
+
+
+ + +
+ +
+ + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/fdmath/index.html b/api/fdmath/index.html new file mode 100644 index 0000000..61228f3 --- /dev/null +++ b/api/fdmath/index.html @@ -0,0 +1,4603 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + fdmath - meanas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + +

fdmath

+ + +
+ + + +

+ meanas.fdmath + + +

+ +
+ +

Basic discrete calculus for finite difference (fd) simulations.

+

Fields, Functions, and Operators

+

Discrete fields are stored in one of two forms:

+
    +
  • The fdfield_t form is a multidimensional numpy.NDArray
      +
    • For a scalar field, this is just U[m, n, p], where m, n, and p are + discrete indices referring to positions on the x, y, and z axes respectively.
    • +
    • For a vector field, the first index specifies which vector component is accessed: + E[:, m, n, p] = [Ex[m, n, p], Ey[m, n, p], Ez[m, n, p]].
    • +
    +
  • +
  • The vfdfield_t form is simply a vectorzied (i.e. 1D) version of the fdfield_t, + as obtained by meanas.fdmath.vectorization.vec (effectively just numpy.ravel)
  • +
+ + +
+ Operators which act on fields also come in two forms +
    +
  • Python functions, created by the functions in meanas.fdmath.functional. + The generated functions act on fields in the fdfield_t form.
  • +
  • Linear operators, usually 2D sparse matrices using scipy.sparse, created + by meanas.fdmath.operators. These operators act on vectorized fields in the + vfdfield_t form.
  • +
+

The operations performed should be equivalent: functional.op(*args)(E) should be +equivalent to unvec(operators.op(*args) @ vec(E), E.shape[1:]).

+

Generally speaking the field_t form is easier to work with, but can be harder or less +efficient to compose (e.g. it is easy to generate a single matrix by multiplying a +series of other matrices).

+

Discrete calculus

+

This documentation and approach is roughly based on W.C. Chew's excellent +"Electromagnetic Theory on a Lattice" (doi:10.1063/1.355770), +which covers a superset of this material with similar notation and more detail.

+

Scalar derivatives and cell shifts

+

Define the discrete forward derivative as + $$ [\tilde{\partial}x f] - f_m) $$ + where }{2}} = \frac{1}{\Delta_{x, m}} (f_{m + 1\(f\) is a function defined at discrete locations on the x-axis (labeled using \(m\)). + The value at \(m\) occupies a length \(\Delta_{x, m}\) along the x-axis. Note that \(m\) + is an index along the x-axis, not necessarily an x-coordinate, since each length + \(\Delta_{x, m}, \Delta_{x, m+1}, ...\) is independently chosen.

+

If we treat f as a 1D array of values, with the i-th value f[i] taking up a length dx[i] +along the x-axis, the forward derivative is

+
deriv_forward(f)[i] = (f[i + 1] - f[i]) / dx[i]
+
+

Likewise, discrete reverse derivative is + $$ [\hat{\partial}x f ]) $$ + or}{2}} = \frac{1}{\Delta_{x, m}} (f_{m} - f_{m - 1

+
deriv_back(f)[i] = (f[i] - f[i - 1]) / dx[i]
+
+

The derivatives' values are shifted by a half-cell relative to the original function, and +will have different cell widths if all the dx[i] ( \(\Delta_{x, m}\) ) are not +identical:

+
[figure: derivatives and cell sizes]
+    dx0   dx1      dx2      dx3      cell sizes for function
+   ----- ----- ----------- -----
+   ______________________________
+        |     |           |     |
+     f0 |  f1 |     f2    |  f3 |    function
+   _____|_____|___________|_____|
+     |     |        |        |
+     | Df0 |   Df1  |   Df2  | Df3   forward derivative (periodic boundary)
+   __|_____|________|________|___
+
+ dx'3] dx'0   dx'1     dx'2  [dx'3   cell sizes for forward derivative
+   -- ----- -------- -------- ---
+ dx'0] dx'1   dx'2     dx'3  [dx'0   cell sizes for reverse derivative
+   ______________________________
+     |     |        |        |
+     | df1 |  df2   |   df3  | df0   reverse derivative (periodic boundary)
+   __|_____|________|________|___
+
+Periodic boundaries are used here and elsewhere unless otherwise noted.
+
+

In the above figure, + f0 = \(f_0\), f1 = \(f_1\) + Df0 = \([\tilde{\partial}f]_{0 + \frac{1}{2}}\) + Df1 = \([\tilde{\partial}f]_{1 + \frac{1}{2}}\) + df0 = \([\hat{\partial}f]_{0 - \frac{1}{2}}\) + etc.

+

The fractional subscript \(m + \frac{1}{2}\) is used to indicate values defined + at shifted locations relative to the original \(m\), with corresponding lengths + $$ \Delta_{x, m + \frac{1}{2}} = \frac{1}{2} * (\Delta_{x, m} + \Delta_{x, m + 1}) $$

+

Just as \(m\) is not itself an x-coordinate, neither is \(m + \frac{1}{2}\); +carefully note the positions of the various cells in the above figure vs their labels. +If the positions labeled with \(m\) are considered the "base" or "original" grid, +the positions labeled with \(m + \frac{1}{2}\) are said to lie on a "dual" or +"derived" grid.

+

For the remainder of the Discrete calculus section, all figures will show +constant-length cells in order to focus on the vector derivatives themselves. +See the Grid description section below for additional information on this topic +and generalization to three dimensions.

+

Gradients and fore-vectors

+

Expanding to three dimensions, we can define two gradients + $$ [\tilde{\nabla} f]{m,n,p} = \vec{x} [\tilde{\partial}_x f] + + \vec{y} [\tilde{\partial}}{2},n,py f] + + \vec{z} [\tilde{\partial}}{2},pz f] $$ + $$ [\hat{\nabla} f]}{2}{m,n,p} = \vec{x} [\hat{\partial}_x f] + + \vec{y} [\hat{\partial}}{2},n,py f] + + \vec{z} [\hat{\partial}}{2},pz f] $$}{2}

+

or

+
[code: gradients]
+grad_forward(f)[i,j,k] = [Dx_forward(f)[i, j, k],
+                          Dy_forward(f)[i, j, k],
+                          Dz_forward(f)[i, j, k]]
+                       = [(f[i + 1, j, k] - f[i, j, k]) / dx[i],
+                          (f[i, j + 1, k] - f[i, j, k]) / dy[i],
+                          (f[i, j, k + 1] - f[i, j, k]) / dz[i]]
+
+grad_back(f)[i,j,k] = [Dx_back(f)[i, j, k],
+                       Dy_back(f)[i, j, k],
+                       Dz_back(f)[i, j, k]]
+                    = [(f[i, j, k] - f[i - 1, j, k]) / dx[i],
+                       (f[i, j, k] - f[i, j - 1, k]) / dy[i],
+                       (f[i, j, k] - f[i, j, k - 1]) / dz[i]]
+
+

The three derivatives in the gradient cause shifts in different +directions, so the x/y/z components of the resulting "vector" are defined +at different points: the x-component is shifted in the x-direction, +y in y, and z in z.

+

We call the resulting object a "fore-vector" or "back-vector", depending +on the direction of the shift. We write it as + $$ \tilde{g}{m,n,p} = \vec{x} g^x + + \vec{y} g^y_{m,n + \frac{1}{2},p} + + \vec{z} g^z_{m,n,p + \frac{1}{2}} $$ + $$ \hat{g}}{2},n,p{m,n,p} = \vec{x} g^x + + \vec{y} g^y_{m,n - \frac{1}{2},p} + + \vec{z} g^z_{m,n,p - \frac{1}{2}} $$}{2},n,p

+
[figure: gradient / fore-vector]
+   (m, n+1, p+1) ______________ (m+1, n+1, p+1)
+                /:            /|
+               / :           / |
+              /  :          /  |
+  (m, n, p+1)/_____________/   |     The forward derivatives are defined
+             |   :         |   |     at the Dx, Dy, Dz points,
+             |   :.........|...|     but the forward-gradient fore-vector
+ z y        Dz  /          |  /      is the set of all three
+ |/_x        | Dy          | /       and is said to be "located" at (m,n,p)
+             |/            |/
+    (m, n, p)|_____Dx______| (m+1, n, p)
+
+

Divergences

+

There are also two divergences,

+

$$ d_{n,m,p} = [\tilde{\nabla} \cdot \hat{g}]{n,m,p} + = [\tilde{\partial}_x g^x] + + [\tilde{\partial}y g^y] + + [\tilde{\partial}z g^z] $$

+

$$ d_{n,m,p} = [\hat{\nabla} \cdot \tilde{g}]{n,m,p} + = [\hat{\partial}_x g^x] + + [\hat{\partial}y g^y] + + [\hat{\partial}z g^z] $$

+

or

+
[code: divergences]
+div_forward(g)[i,j,k] = Dx_forward(gx)[i, j, k] +
+                        Dy_forward(gy)[i, j, k] +
+                        Dz_forward(gz)[i, j, k]
+                      = (gx[i + 1, j, k] - gx[i, j, k]) / dx[i] +
+                        (gy[i, j + 1, k] - gy[i, j, k]) / dy[i] +
+                        (gz[i, j, k + 1] - gz[i, j, k]) / dz[i]
+
+div_back(g)[i,j,k] = Dx_back(gx)[i, j, k] +
+                     Dy_back(gy)[i, j, k] +
+                     Dz_back(gz)[i, j, k]
+                   = (gx[i, j, k] - gx[i - 1, j, k]) / dx[i] +
+                     (gy[i, j, k] - gy[i, j - 1, k]) / dy[i] +
+                     (gz[i, j, k] - gz[i, j, k - 1]) / dz[i]
+
+

where g = [gx, gy, gz] is a fore- or back-vector field.

+

Since we applied the forward divergence to the back-vector (and vice-versa), the resulting scalar value +is defined at the back-vector's (fore-vector's) location \((m,n,p)\) and not at the locations of its components +\((m \pm \frac{1}{2},n,p)\) etc.

+
[figure: divergence]
+                                ^^
+     (m-1/2, n+1/2, p+1/2) _____||_______ (m+1/2, n+1/2, p+1/2)
+                          /:    ||  ,,  /|
+                         / :    || //  / |      The divergence at (m, n, p) (the center
+                        /  :      //  /  |      of this cube) of a fore-vector field
+  (m-1/2, n-1/2, p+1/2)/_____________/   |      is the sum of the outward-pointing
+                       |   :         |   |      fore-vector components, which are
+     z y            <==|== :.........|.====>    located at the face centers.
+     |/_x              |  /          |  /
+                       | /    //     | /       Note that in a nonuniform grid, each
+                       |/    // ||   |/        dimension is normalized by the cell width.
+  (m-1/2, n-1/2, p-1/2)|____//_______| (m+1/2, n-1/2, p-1/2)
+                           ''   ||
+                                VV
+
+

Curls

+

The two curls are then

+

$$ \begin{aligned} + \hat{h}{m + \frac{1}{2}, n + \frac{1}{2}, p + \frac{1}{2}} &= \ + [\tilde{\nabla} \times \tilde{g}] &= + \vec{x} (\tilde{\partial}}{2}, n + \frac{1}{2}, p + \frac{1}{2}y g^z}{2}} - \tilde{\partialz g^y) \ + &+ \vec{y} (\tilde{\partial}}{2},pz g^x}{2},n,p} - \tilde{\partialx g^z) \ + &+ \vec{z} (\tilde{\partial}}{2}x g^y}{2},p} - \tilde{\partialy g^z) + \end{aligned} $$}{2},n,p

+

and

+

$$ \tilde{h}{m - \frac{1}{2}, n - \frac{1}{2}, p - \frac{1}{2}} = + [\hat{\nabla} \times \hat{g}] $$}{2}, n - \frac{1}{2}, p - \frac{1}{2}

+

where \(\hat{g}\) and \(\tilde{g}\) are located at \((m,n,p)\) + with components at \((m \pm \frac{1}{2},n,p)\) etc., + while \(\hat{h}\) and \(\tilde{h}\) are located at \((m \pm \frac{1}{2}, n \pm \frac{1}{2}, p \pm \frac{1}{2})\) + with components at \((m, n \pm \frac{1}{2}, p \pm \frac{1}{2})\) etc.

+
[code: curls]
+curl_forward(g)[i,j,k] = [Dy_forward(gz)[i, j, k] - Dz_forward(gy)[i, j, k],
+                          Dz_forward(gx)[i, j, k] - Dx_forward(gz)[i, j, k],
+                          Dx_forward(gy)[i, j, k] - Dy_forward(gx)[i, j, k]]
+
+curl_back(g)[i,j,k] = [Dy_back(gz)[i, j, k] - Dz_back(gy)[i, j, k],
+                       Dz_back(gx)[i, j, k] - Dx_back(gz)[i, j, k],
+                       Dx_back(gy)[i, j, k] - Dy_back(gx)[i, j, k]]
+
+

For example, consider the forward curl, at (m, n, p), of a back-vector field g, defined + on a grid containing (m + 1/2, n + 1/2, p + 1/2). + The curl will be a fore-vector, so its z-component will be defined at (m, n, p + 1/2). + Take the nearest x- and y-components of g in the xy plane where the curl's z-component + is located; these are

+
[curl components]
+(m,       n + 1/2, p + 1/2) : x-component of back-vector at (m + 1/2, n + 1/2, p + 1/2)
+(m + 1,   n + 1/2, p + 1/2) : x-component of back-vector at (m + 3/2, n + 1/2, p + 1/2)
+(m + 1/2, n      , p + 1/2) : y-component of back-vector at (m + 1/2, n + 1/2, p + 1/2)
+(m + 1/2, n + 1  , p + 1/2) : y-component of back-vector at (m + 1/2, n + 3/2, p + 1/2)
+
+

These four xy-components can be used to form a loop around the curl's z-component; its magnitude and sign + is set by their loop-oriented sum (i.e. two have their signs flipped to complete the loop).

+
[figure: z-component of curl]
+                          :             |
+    z y                   :    ^^       |
+    |/_x                  :....||.<.....|  (m+1, n+1, p+1/2)
+                          /    ||      /
+                       | v     ||   | ^
+                       |/           |/
+         (m, n, p+1/2) |_____>______|  (m+1, n, p+1/2)
+
+

Maxwell's Equations

+

If we discretize both space (m,n,p) and time (l), Maxwell's equations become

+

$$ \begin{aligned} + \tilde{\nabla} \times \tilde{E}{l,\vec{r}} &= -\tilde{\partial}_t \hat{B} + - \hat{M}}{2}, \vec{r} + \frac{1}{2}{l, \vec{r} + \frac{1}{2}} \ + \hat{\nabla} \times \hat{H}}{2},\vec{r} + \frac{1}{2}} &= \hat{\partialt \tilde{D} + + \tilde{J}}{l-\frac{1}{2},\vec{r}} \ + \tilde{\nabla} \cdot \hat{B} &= 0 \ + \hat{\nabla} \cdot \tilde{D}}{2}, \vec{r} + \frac{1}{2}{l,\vec{r}} &= \rho + \end{aligned} $$}

+

with

+

$$ \begin{aligned} + \hat{B}{\vec{r}} &= \mu} + \frac{1}{2}} \cdot \hat{H{\vec{r} + \frac{1}{2}} \ + \tilde{D} + \end{aligned} $$}} &= \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}

+

where the spatial subscripts are abbreviated as \(\vec{r} = (m, n, p)\) and +\(\vec{r} + \frac{1}{2} = (m + \frac{1}{2}, n + \frac{1}{2}, p + \frac{1}{2})\), +\(\tilde{E}\) and \(\hat{H}\) are the electric and magnetic fields, +\(\tilde{J}\) and \(\hat{M}\) are the electric and magnetic current distributions, +and \(\epsilon\) and \(\mu\) are the dielectric permittivity and magnetic permeability.

+

The above is Yee's algorithm, written in a form analogous to Maxwell's equations. +The time derivatives can be expanded to form the update equations:

+
[code: Maxwell's equations updates]
+H[i, j, k] -= dt * (curl_forward(E)[i, j, k] + M[t, i, j, k]) /      mu[i, j, k]
+E[i, j, k] += dt * (curl_back(   H)[i, j, k] + J[t, i, j, k]) / epsilon[i, j, k]
+
+

Note that the E-field fore-vector and H-field back-vector are offset by a half-cell, resulting +in distinct locations for all six E- and H-field components:

+
[figure: Field components]
+
+        (m - 1/2,=> ____________Hx__________[H] <= r + 1/2 = (m + 1/2,
+         n + 1/2,  /:           /:          /|                n + 1/2,
+   z y   p + 1/2) / :          / :         / |                p + 1/2)
+   |/_x          /  :         /  :        /  |
+                /   :       Ez__________Hy   |      Locations of the E- and
+               /    :        :   :      /|   |      H-field components for the
+ (m - 1/2,    /     :        :  Ey...../.|..Hz      [E] fore-vector at r = (m,n,p)
+  n - 1/2, =>/________________________/  |  /|      (the large cube's center)
+  p + 1/2)   |      :        : /      |  | / |      and [H] back-vector at r + 1/2
+             |      :        :/       |  |/  |      (the top right corner)
+             |      :       [E].......|.Ex   |
+             |      :.................|......| <= (m + 1/2, n + 1/2, p + 1/2)
+             |     /                  |     /
+             |    /                   |    /
+             |   /                    |   /         This is the Yee discretization
+             |  /                     |  /          scheme ("Yee cell").
+r - 1/2 =    | /                      | /
+ (m - 1/2,   |/                       |/
+  n - 1/2,=> |________________________| <= (m + 1/2, n - 1/2, p - 1/2)
+  p - 1/2)
+
+

Each component forms its own grid, offset from the others:

+
[figure: E-fields for adjacent cells]
+
+              H1__________Hx0_________H0
+  z y        /:                       /|
+  |/_x      / :                      / |    This figure shows H back-vector locations
+           /  :                     /  |    H0, H1, etc. and their associated components
+         Hy1  :                   Hy0  |    H0 = (Hx0, Hy0, Hz0) etc.
+         /    :                   /    |
+        /    Hz1                 /     Hz0
+       H2___________Hx3_________H3     |    The equivalent drawing for E would have
+       |      :                 |      |    fore-vectors located at the cube's
+       |      :                 |      |    center (and the centers of adjacent cubes),
+       |      :                 |      |    with components on the cube's faces.
+       |      H5..........Hx4...|......H4
+       |     /                  |     /
+      Hz2   /                  Hz2   /
+       |   /                    |   /
+       | Hy6                    | Hy4
+       | /                      | /
+       |/                       |/
+       H6__________Hx7__________H7
+
+

The divergence equations can be derived by taking the divergence of the curl equations +and combining them with charge continuity, + $$ \hat{\nabla} \cdot \tilde{J} + \hat{\partial}_t \rho = 0 $$ + implying that the discrete Maxwell's equations do not produce spurious charges.

+

Wave equation

+

Taking the backward curl of the \(\tilde{\nabla} \times \tilde{E}\) equation and +replacing the resulting \(\hat{\nabla} \times \hat{H}\) term using its respective equation, +and setting \(\hat{M}\) to zero, we can form the discrete wave equation:

+
\[ + \begin{aligned} + \tilde{\nabla} \times \tilde{E}_{l,\vec{r}} &= + -\tilde{\partial}_t \hat{B}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} + - \hat{M}_{l-1, \vec{r} + \frac{1}{2}} \\ + \mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}} &= + -\tilde{\partial}_t \hat{H}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &= + \hat{\nabla} \times (-\tilde{\partial}_t \hat{H}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}}) \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &= + -\tilde{\partial}_t \hat{\nabla} \times \hat{H}_{l-\frac{1}{2}, \vec{r} + \frac{1}{2}} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) &= + -\tilde{\partial}_t \hat{\partial}_t \epsilon_{\vec{r}} \tilde{E}_{l, \vec{r}} + \hat{\partial}_t \tilde{J}_{l-\frac{1}{2},\vec{r}} \\ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l,\vec{r}}) + + \tilde{\partial}_t \hat{\partial}_t \epsilon_{\vec{r}} \cdot \tilde{E}_{l, \vec{r}} + &= \tilde{\partial}_t \tilde{J}_{l - \frac{1}{2}, \vec{r}} + \end{aligned} +\]
+

Frequency domain

+

We can substitute in a time-harmonic fields

+
\[ + \begin{aligned} + \tilde{E}_{l, \vec{r}} &= \tilde{E}_{\vec{r}} e^{-\imath \omega l \Delta_t} \\ + \tilde{J}_{l, \vec{r}} &= \tilde{J}_{\vec{r}} e^{-\imath \omega (l - \frac{1}{2}) \Delta_t} + \end{aligned} +\]
+

resulting in

+
\[ + \begin{aligned} + \tilde{\partial}_t &\Rightarrow (e^{ \imath \omega \Delta_t} - 1) / \Delta_t = \frac{-2 \imath}{\Delta_t} \sin(\omega \Delta_t / 2) e^{-\imath \omega \Delta_t / 2} = -\imath \Omega e^{-\imath \omega \Delta_t / 2}\\ + \hat{\partial}_t &\Rightarrow (1 - e^{-\imath \omega \Delta_t}) / \Delta_t = \frac{-2 \imath}{\Delta_t} \sin(\omega \Delta_t / 2) e^{ \imath \omega \Delta_t / 2} = -\imath \Omega e^{ \imath \omega \Delta_t / 2}\\ + \Omega &= 2 \sin(\omega \Delta_t / 2) / \Delta_t + \end{aligned} +\]
+

This gives the frequency-domain wave equation,

+
\[ + \hat{\nabla} \times (\mu^{-1}_{\vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{\vec{r}}) + -\Omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath \Omega \tilde{J}_{\vec{r}} e^{\imath \omega \Delta_t / 2} \\ +\]
+

Plane waves and Dispersion relation

+

With uniform material distribution and no sources

+
\[ + \begin{aligned} + \mu_{\vec{r} + \frac{1}{2}} &= \mu \\ + \epsilon_{\vec{r}} &= \epsilon \\ + \tilde{J}_{\vec{r}} &= 0 \\ + \end{aligned} +\]
+

the frequency domain wave equation simplifies to

+
\[ \hat{\nabla} \times \tilde{\nabla} \times \tilde{E}_{\vec{r}} - \Omega^2 \epsilon \mu \tilde{E}_{\vec{r}} = 0 \]
+

Since \(\hat{\nabla} \cdot \tilde{E}_{\vec{r}} = 0\), we can simplify

+
\[ + \begin{aligned} + \hat{\nabla} \times \tilde{\nabla} \times \tilde{E}_{\vec{r}} + &= \tilde{\nabla}(\hat{\nabla} \cdot \tilde{E}_{\vec{r}}) - \hat{\nabla} \cdot \tilde{\nabla} \tilde{E}_{\vec{r}} \\ + &= - \hat{\nabla} \cdot \tilde{\nabla} \tilde{E}_{\vec{r}} \\ + &= - \tilde{\nabla}^2 \tilde{E}_{\vec{r}} + \end{aligned} +\]
+

and we get

+
\[ \tilde{\nabla}^2 \tilde{E}_{\vec{r}} + \Omega^2 \epsilon \mu \tilde{E}_{\vec{r}} = 0 \]
+

We can convert this to three scalar-wave equations of the form

+
\[ (\tilde{\nabla}^2 + K^2) \phi_{\vec{r}} = 0 \]
+

with \(K^2 = \Omega^2 \mu \epsilon\). Now we let

+
\[ \phi_{\vec{r}} = A e^{\imath (k_x m \Delta_x + k_y n \Delta_y + k_z p \Delta_z)} \]
+

resulting in

+
\[ + \begin{aligned} + \tilde{\partial}_x &\Rightarrow (e^{ \imath k_x \Delta_x} - 1) / \Delta_t = \frac{-2 \imath}{\Delta_x} \sin(k_x \Delta_x / 2) e^{ \imath k_x \Delta_x / 2} = \imath K_x e^{ \imath k_x \Delta_x / 2}\\ + \hat{\partial}_x &\Rightarrow (1 - e^{-\imath k_x \Delta_x}) / \Delta_t = \frac{-2 \imath}{\Delta_x} \sin(k_x \Delta_x / 2) e^{-\imath k_x \Delta_x / 2} = \imath K_x e^{-\imath k_x \Delta_x / 2}\\ + K_x &= 2 \sin(k_x \Delta_x / 2) / \Delta_x \\ + \end{aligned} +\]
+

with similar expressions for the y and z dimnsions (and \(K_y, K_z\)).

+

This implies

+
\[ + \tilde{\nabla}^2 = -(K_x^2 + K_y^2 + K_z^2) \phi_{\vec{r}} \\ + K_x^2 + K_y^2 + K_z^2 = \Omega^2 \mu \epsilon = \Omega^2 / c^2 +\]
+

where \(c = \sqrt{\mu \epsilon}\).

+

Assuming real \((k_x, k_y, k_z), \omega\) will be real only if

+
\[ c^2 \Delta_t^2 = \frac{\Delta_t^2}{\mu \epsilon} < 1/(\frac{1}{\Delta_x^2} + \frac{1}{\Delta_y^2} + \frac{1}{\Delta_z^2}) \]
+

If \(\Delta_x = \Delta_y = \Delta_z\), this simplifies to \(c \Delta_t < \Delta_x / \sqrt{3}\). +This last form can be interpreted as enforcing causality; the distance that light +travels in one timestep (i.e., \(c \Delta_t\)) must be less than the diagonal +of the smallest cell ( \(\Delta_x / \sqrt{3}\) when on a uniform cubic grid).

+

Grid description

+

As described in the section on scalar discrete derivatives above, cell widths +(dx[i], dy[j], dz[k]) along each axis can be arbitrary and independently +defined. Moreover, all field components are actually defined at "derived" or "dual" +positions, in-between the "base" grid points on one or more axes.

+

To get a better sense of how this works, let's start by drawing a grid with uniform +dy and dz and nonuniform dx. We will only draw one cell in the y and z dimensions +to make the illustration simpler; we need at least two cells in the x dimension to +demonstrate how nonuniform dx affects the various components.

+

Place the E fore-vectors at integer indices \(r = (m, n, p)\) and the H back-vectors +at fractional indices \(r + \frac{1}{2} = (m + \frac{1}{2}, n + \frac{1}{2}, +p + \frac{1}{2})\). Remember that these are indices and not coordinates; they can +correspond to arbitrary (monotonically increasing) coordinates depending on the cell widths.

+

Draw lines to denote the planes on which the H components and back-vectors are defined. +For simplicity, don't draw the equivalent planes for the E components and fore-vectors, +except as necessary to show their locations -- it's easiest to just connect them to their +associated H-equivalents.

+

The result looks something like this:

+
[figure: Component centers]
+                                                             p=
+          [H]__________Hx___________[H]_____Hx______[H]   __ +1/2
+  z y     /:           /:           /:      /:      /|     |      |
+  |/_x   / :          / :          / :     / :     / |     |      |
+        /  :         /  :         /  :    /  :    /  |     |      |
+      Hy   :       Ez...........Hy   :  Ez......Hy   |     |      |
+      /:   :        :   :       /:   :   :   :  /|   |     |      |
+     / :  Hz        :  Ey....../.:..Hz   :  Ey./.|..Hz    __ 0    | dz[0]
+    /  :  /:        :  /      /  :  /:   :  / /  |  /|     |      |
+   /_________________________/_______________/   | / |     |      |
+   |   :/  :        :/       |   :/  :   :/  |   |/  |     |      |
+   |  Ex   :       [E].......|..Ex   :  [E]..|..Ex   |     |      |
+   |       :                 |       :       |       |     |      |
+   |      [H]..........Hx....|......[H].....H|x.....[H]   __ --------- (n=+1/2, p=-1/2)
+   |      /                  |      /        |      /     /       /
+  Hz     /                  Hz     /        Hz     /     /       /
+   |    /                    |    /          |    /     /       /
+   |  Hy                     |  Hy           |  Hy    __ 0     / dy[0]
+   |  /                      |  /            |  /     /       /
+   | /                       | /             | /     /       /
+   |/                        |/              |/     /       /
+  [H]__________Hx___________[H]_____Hx______[H]   __ -1/2  /
+                                                       =n
+   |------------|------------|-------|-------|
+ -1/2           0          +1/2     +1     +3/2 = m
+
+    ------------------------- ----------------
+              dx[0]                  dx[1]
+
+  Part of a nonuniform "base grid", with labels specifying
+  positions of the various field components. [E] fore-vectors
+  are at the cell centers, and [H] back-vectors are at the
+  vertices. H components along the near (-y) top (+z) edge
+  have been omitted to make the insides of the cubes easier
+  to visualize.
+
+

The above figure shows where all the components are located; however, it is also useful to show +what volumes those components correspond to. Consider the Ex component at m = +1/2: it is +shifted in the x-direction by a half-cell from the E fore-vector at m = 0 (labeled [E] +in the figure). It corresponds to a volume between m = 0 and m = +1 (the other +dimensions are not shifted, i.e. they are still bounded by n, p = +-1/2). (See figure +below). Since m is an index and not an x-coordinate, the Ex component is not necessarily +at the center of the volume it represents, and the x-length of its volume is the derived +quantity dx'[0] = (dx[0] + dx[1]) / 2 rather than the base dx. +(See also Scalar derivatives and cell shifts).

+
[figure: Ex volumes]
+                                                             p=
+           <_________________________________________>   __ +1/2
+  z y     <<           /:           /       /:      >>    |      |
+  |/_x   < <          / :          /       / :     > >    |      |
+        <  <         /  :         /       /  :    >  >    |      |
+       <   <        /   :        /       /   :   >   >    |      |
+      <:   <       /    :        :      /    :  >:   >    |      |
+     < :   <      /     :        :     /     : > :   >   __ 0    | dz[0]
+    <  :   <     /      :        :    /      :>  :   >    |      |
+   <____________/____________________/_______>   :   >    |      |
+   <   :   <    |       :        :   |       >   :   >    |      |
+   <  Ex   <    |       :       Ex   |       >  Ex   >    |      |
+   <   :   <    |       :        :   |       >   :   >    |      |
+   <   :   <....|.......:........:...|.......>...:...>   __ --------- (n=+1/2, p=-1/2)
+   <   :  <     |      /         :  /|      />   :  >    /       /
+   <   : <      |     /          : / |     / >   : >    /       /
+   <   :<       |    /           :/  |    /  >   :>    /       /
+   <   <        |   /            :   |   /   >   >    _ 0     / dy[0]
+   <  <         |  /                 |  /    >  >    /       /
+   < <          | /                  | /     > >    /       /
+   <<           |/                   |/      >>    /       /
+   <____________|____________________|_______>   __ -1/2  /
+                                                     =n
+   |------------|------------|-------|-------|
+ -1/2           0          +1/2      +1    +3/2 = m
+
+   ~------------ -------------------- -------~
+     dx'[-1]          dx'[0]           dx'[1]
+
+ The Ex values are positioned on the x-faces of the base
+ grid. They represent the Ex field in volumes shifted by
+ a half-cell in the x-dimension, as shown here. Only the
+ center cell (with width dx'[0]) is fully shown; the
+ other two are truncated (shown using >< markers).
+
+ Note that the Ex positions are the in the same positions
+ as the previous figure; only the cell boundaries have moved.
+ Also note that the points at which Ex is defined are not
+ necessarily centered in the volumes they represent; non-
+ uniform cell sizes result in off-center volumes like the
+ center cell here.
+
+

The next figure shows the volumes corresponding to the Hy components, which +are shifted in two dimensions (x and z) compared to the base grid.

+
[figure: Hy volumes]
+                                                             p=
+  z y     mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm   __ +1/2  s
+  |/_x   <<           m:                    m:      >>    |       |
+        < <          m :                   m :     > >    |       | dz'[1]
+       <  <         m  :                  m  :    >  >    |       |
+     Hy........... m........Hy...........m......Hy   >    |       |
+     <    <       m    :                m    :  >    >    |       |
+    <     ______ m_____:_______________m_____:_>______   __ 0
+   <      <     m     /:              m     / >      >    |       |
+  mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm       >    |       |
+  <       <    |    /  :             |    /  >       >    |       | dz'[0]
+  <       <    |   /   :             |   /   >       >    |       |
+  <       <    |  /    :             |  /    >       >    |       |
+  <       wwwww|w/wwwwwwwwwwwwwwwwwww|w/wwwww>wwwwwwww   __       s
+  <      <     |/     w              |/     w>      >    /         /
+  _____________|_____________________|________     >    /         /
+  <    <       |    w                |    w  >    >    /         /
+  <  Hy........|...w........Hy.......|...w...>..Hy    _ 0       / dy[0]
+  < <          |  w                  |  w    >  >    /         /
+  <<           | w                   | w     > >    /         /
+  <            |w                    |w      >>    /         /
+  wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww   __ -1/2    /
+
+  |------------|------------|--------|-------|
+-1/2           0          +1/2      +1     +3/2 = m
+
+  ~------------ --------------------- -------~
+     dx'[-1]            dx'[0]         dx'[1]
+
+ The Hy values are positioned on the y-edges of the base
+ grid. Again here, the 'Hy' labels represent the same points
+ as in the basic grid figure above; the edges have shifted
+ by a half-cell along the x- and z-axes.
+
+ The grid lines _|:/ are edges of the area represented by
+ each Hy value, and the lines drawn using <m>.w represent
+ edges where a cell's faces extend beyond the drawn area
+ (i.e. where the drawing is truncated in the x- or z-
+ directions).
+
+

Datastructure: dx_lists_t

+

In this documentation, the E fore-vectors are placed on the base grid. An +equivalent formulation could place the H back-vectors on the base grid instead. +However, in the case of a non-uniform grid, the operation to get from the "base" +cell widths to "derived" ones is not its own inverse.

+

The base grid's cell sizes could be fully described by a list of three 1D arrays, +specifying the cell widths along all three axes:

+
[dx, dy, dz] = [[dx[0], dx[1], ...], [dy[0], ...], [dz[0], ...]]
+
+

Note that this is a list-of-arrays rather than a 2D array, as the simulation domain +may have a different number of cells along each axis.

+

Knowing the base grid's cell widths and the boundary conditions (periodic unless +otherwise noted) is enough information to calculate the cell widths dx', dy', +and dz' for the derived grids.

+

However, since most operations are trivially generalized to allow either E or H +to be defined on the base grid, they are written to take the a full set of base +and derived cell widths, distinguished by which field they apply to rather than +their "base" or "derived" status. This removes the need for each function to +generate the derived widths, and makes the "base" vs "derived" distinction +unnecessary in the code.

+

The resulting data structure containing all the cell widths takes the form of a +list-of-lists-of-arrays. The first list-of-arrays provides the cell widths for +the E-field fore-vectors, while the second list-of-arrays does the same for the +H-field back-vectors:

+
 [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...], [dz_e[0], ...]],
+  [[dx_h[0], dx_h[1], ...], [dy_h[0], ...], [dz_h[0], ...]]]
+
+

where dx_e[0] is the x-width of the m=0 cells, as used when calculating dE/dx, + and dy_h[0] is the y-width of the n=0 cells, as used when calculating dH/dy, etc.

+

Permittivity and Permeability

+

Since each vector component of E and H is defined in a different location and represents +a different volume, the value of the spatially-discrete epsilon and mu can also be +different for all three field components, even when representing a simple planar interface +between two isotropic materials.

+

As a result, epsilon and mu are taken to have the same dimensions as the field, and +composed of the three diagonal tensor components:

+
[equations: epsilon_and_mu]
+epsilon = [epsilon_xx, epsilon_yy, epsilon_zz]
+mu = [mu_xx, mu_yy, mu_zz]
+
+

or

+
\[ + \epsilon = \begin{bmatrix} \epsilon_{xx} & 0 & 0 \\ + 0 & \epsilon_{yy} & 0 \\ + 0 & 0 & \epsilon_{zz} \end{bmatrix} +$$ +$$ + \mu = \begin{bmatrix} \mu_{xx} & 0 & 0 \\ + 0 & \mu_{yy} & 0 \\ + 0 & 0 & \mu_{zz} \end{bmatrix} +\]
+

where the off-diagonal terms (e.g. epsilon_xy) are assumed to be zero.

+

High-accuracy volumetric integration of shapes on multiple grids can be performed +by the gridlock module.

+

The values of the vacuum permittivity and permability effectively become scaling +factors that appear in several locations (e.g. between the E and H fields). In +order to limit floating-point inaccuracy and simplify calculations, they are often +set to 1 and relative permittivities and permeabilities are used in their places; +the true values can be multiplied back in after the simulation is complete if non- +normalized results are needed.

+ + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +

Functional and sparse operators

+ + +
+ + + +

+ meanas.fdmath.functional + + +

+ +
+ +

Math functions for finite difference simulations

+

Basic discrete calculus etc.

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ deriv_forward + + +

+
deriv_forward(
+    dx_e: Sequence[NDArray[floating | complexfloating]]
+    | None = None,
+) -> tuple[
+    fdfield_updater_t, fdfield_updater_t, fdfield_updater_t
+]
+
+ +
+ +

Utility operators for taking discretized derivatives (backward variant).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dx_e + + Sequence[NDArray[floating | complexfloating]] | None + +
+

Lists of cell sizes for all axes + [[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...].

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t] + +
+

List of functions for taking forward derivatives along each axis.

+
+
+ + +
+ +
+ +
+ + +

+ deriv_back + + +

+
deriv_back(
+    dx_h: Sequence[NDArray[floating | complexfloating]]
+    | None = None,
+) -> tuple[
+    fdfield_updater_t, fdfield_updater_t, fdfield_updater_t
+]
+
+ +
+ +

Utility operators for taking discretized derivatives (forward variant).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dx_h + + Sequence[NDArray[floating | complexfloating]] | None + +
+

Lists of cell sizes for all axes + [[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...].

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t] + +
+

List of functions for taking forward derivatives along each axis.

+
+
+ + +
+ +
+ +
+ + +

+ curl_forward + + +

+
curl_forward(
+    dx_e: Sequence[NDArray[floating | complexfloating]]
+    | None = None,
+) -> Callable[[TT], TT]
+
+ +
+ +

Curl operator for use with the E field.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dx_e + + Sequence[NDArray[floating | complexfloating]] | None + +
+

Lists of cell sizes for all axes + [[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...].

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ Callable[[TT], TT] + +
+

Function f for taking the discrete forward curl of a field,

+
+
+ Callable[[TT], TT] + +
+

f(E) -> curlE \(= \nabla_f \times E\)

+
+
+ + +
+ +
+ +
+ + +

+ curl_back + + +

+
curl_back(
+    dx_h: Sequence[NDArray[floating | complexfloating]]
+    | None = None,
+) -> Callable[[TT], TT]
+
+ +
+ +

Create a function which takes the backward curl of a field.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dx_h + + Sequence[NDArray[floating | complexfloating]] | None + +
+

Lists of cell sizes for all axes + [[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...].

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ Callable[[TT], TT] + +
+

Function f for taking the discrete backward curl of a field,

+
+
+ Callable[[TT], TT] + +
+

f(H) -> curlH \(= \nabla_b \times H\)

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdmath.operators + + +

+ +
+ +

Matrix operators for finite difference simulations

+

Basic discrete calculus etc.

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ shift_circ + + +

+
shift_circ(
+    axis: int, shape: Sequence[int], shift_distance: int = 1
+) -> sparse.sparray
+
+ +
+ +

Utility operator for performing a circular shift along a specified axis by a + specified number of elements.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ axis + + int + +
+

Axis to shift along. x=0, y=1, z=2

+
+
+ required +
+ shape + + Sequence[int] + +
+

Shape of the grid being shifted

+
+
+ required +
+ shift_distance + + int + +
+

Number of cells to shift by. May be negative. Default 1.

+
+
+ 1 +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix for performing the circular shift.

+
+
+ + +
+ +
+ +
+ + +

+ shift_with_mirror + + +

+
shift_with_mirror(
+    axis: int, shape: Sequence[int], shift_distance: int = 1
+) -> sparse.sparray
+
+ +
+ +

Utility operator for performing an n-element shift along a specified axis, with mirror +boundary conditions applied to the cells beyond the receding edge.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ axis + + int + +
+

Axis to shift along. x=0, y=1, z=2

+
+
+ required +
+ shape + + Sequence[int] + +
+

Shape of the grid being shifted

+
+
+ required +
+ shift_distance + + int + +
+

Number of cells to shift by. May be negative. Default 1.

+
+
+ 1 +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix for performing the shift-with-mirror.

+
+
+ + +
+ +
+ +
+ + +

+ deriv_forward + + +

+
deriv_forward(
+    dx_e: Sequence[NDArray[floating | complexfloating]],
+) -> list[sparse.sparray]
+
+ +
+ +

Utility operators for taking discretized derivatives (forward variant).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dx_e + + Sequence[NDArray[floating | complexfloating]] + +
+

Lists of cell sizes for all axes + [[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...].

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[sparray] + +
+

List of operators for taking forward derivatives along each axis.

+
+
+ + +
+ +
+ +
+ + +

+ deriv_back + + +

+
deriv_back(
+    dx_h: Sequence[NDArray[floating | complexfloating]],
+) -> list[sparse.sparray]
+
+ +
+ +

Utility operators for taking discretized derivatives (backward variant).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dx_h + + Sequence[NDArray[floating | complexfloating]] + +
+

Lists of cell sizes for all axes + [[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...].

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[sparray] + +
+

List of operators for taking forward derivatives along each axis.

+
+
+ + +
+ +
+ +
+ + +

+ cross + + +

+
cross(B: Sequence[sparray]) -> sparse.sparray
+
+ +
+ +

Cross product operator

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ B + + Sequence[sparray] + +
+

List [Bx, By, Bz] of sparse matrices corresponding to the x, y, z +portions of the operator on the left side of the cross product.

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix corresponding to (B x), where x is the cross product.

+
+
+ + +
+ +
+ +
+ + +

+ vec_cross + + +

+
vec_cross(b: vfdfield_t) -> sparse.sparray
+
+ +
+ +

Vector cross product operator

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ b + + vfdfield_t + +
+

Vector on the left side of the cross product.

+
+
+ required +
+

Returns:

+
Sparse matrix corresponding to (b x), where x is the cross product.
+
+ + +
+ +
+ +
+ + +

+ avg_forward + + +

+
avg_forward(
+    axis: int, shape: Sequence[int]
+) -> sparse.sparray
+
+ +
+ +

Forward average operator (x4 = (x4 + x5) / 2)

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ axis + + int + +
+

Axis to average along (x=0, y=1, z=2)

+
+
+ required +
+ shape + + Sequence[int] + +
+

Shape of the grid to average

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix for forward average operation.

+
+
+ + +
+ +
+ +
+ + +

+ avg_back + + +

+
avg_back(axis: int, shape: Sequence[int]) -> sparse.sparray
+
+ +
+ +

Backward average operator (x4 = (x4 + x3) / 2)

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ axis + + int + +
+

Axis to average along (x=0, y=1, z=2)

+
+
+ required +
+ shape + + Sequence[int] + +
+

Shape of the grid to average

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix for backward average operation.

+
+
+ + +
+ +
+ +
+ + +

+ curl_forward + + +

+
curl_forward(
+    dx_e: Sequence[NDArray[floating | complexfloating]],
+) -> sparse.sparray
+
+ +
+ +

Curl operator for use with the E field.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dx_e + + Sequence[NDArray[floating | complexfloating]] + +
+

Lists of cell sizes for all axes + [[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...].

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix for taking the discretized curl of the E-field

+
+
+ + +
+ +
+ +
+ + +

+ curl_back + + +

+
curl_back(
+    dx_h: Sequence[NDArray[floating | complexfloating]],
+) -> sparse.sparray
+
+ +
+ +

Curl operator for use with the H field.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dx_h + + Sequence[NDArray[floating | complexfloating]] + +
+

Lists of cell sizes for all axes + [[dx_0, dx_1, ...], [dy_0, dy_1, ...], ...].

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix for taking the discretized curl of the H-field

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdmath.vectorization + + +

+ +
+ +

Functions for moving between a vector field (list of 3 ndarrays, [f_x, f_y, f_z]) +and a 1D array representation of that field [f_x0, f_x1, f_x2,... f_y0,... f_z0,...]. +Vectorized versions of the field use row-major (ie., C-style) ordering.

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ vec + + +

+
+
vec(f: None) -> None
+
vec(f: fdfield_t) -> vfdfield_t
+
vec(f: cfdfield_t) -> vcfdfield_t
+
vec(f: fdfield2_t) -> vfdfield2_t
+
vec(f: cfdfield2_t) -> vcfdfield2_t
+
vec(f: fdslice_t) -> vfdslice_t
+
vec(f: cfdslice_t) -> vcfdslice_t
+
vec(f: ArrayLike) -> NDArray
+
+
vec(
+    f: fdfield_t
+    | cfdfield_t
+    | fdfield2_t
+    | cfdfield2_t
+    | fdslice_t
+    | cfdslice_t
+    | ArrayLike
+    | None,
+) -> (
+    vfdfield_t
+    | vcfdfield_t
+    | vfdfield2_t
+    | vcfdfield2_t
+    | vfdslice_t
+    | vcfdslice_t
+    | NDArray
+    | None
+)
+
+ +
+ +

Create a 1D ndarray from a vector field which spans a 1-3D region.

+

Returns None if called with f=None.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ f + + fdfield_t | cfdfield_t | fdfield2_t | cfdfield2_t | fdslice_t | cfdslice_t | ArrayLike | None + +
+

A vector field, e.g. [f_x, f_y, f_z] where each f_ component is a 1- to +3-D ndarray (f_* should all be the same size). Doesn't fail with f=None.

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ vfdfield_t | vcfdfield_t | vfdfield2_t | vcfdfield2_t | vfdslice_t | vcfdslice_t | NDArray | None + +
+

1D ndarray containing the linearized field (or None)

+
+
+ + +
+ +
+ +
+ + +

+ unvec + + +

+
+
unvec(
+    v: None, shape: Sequence[int], nvdim: int = 3
+) -> None
+
unvec(
+    v: vfdfield_t, shape: Sequence[int], nvdim: int = 3
+) -> fdfield_t
+
unvec(
+    v: vcfdfield_t, shape: Sequence[int], nvdim: int = 3
+) -> cfdfield_t
+
unvec(
+    v: vfdfield2_t, shape: Sequence[int], nvdim: int = 3
+) -> fdfield2_t
+
unvec(
+    v: vcfdfield2_t, shape: Sequence[int], nvdim: int = 3
+) -> cfdfield2_t
+
unvec(
+    v: vfdslice_t, shape: Sequence[int], nvdim: int = 3
+) -> fdslice_t
+
unvec(
+    v: vcfdslice_t, shape: Sequence[int], nvdim: int = 3
+) -> cfdslice_t
+
unvec(
+    v: ArrayLike, shape: Sequence[int], nvdim: int = 3
+) -> NDArray
+
+
unvec(
+    v: vfdfield_t
+    | vcfdfield_t
+    | vfdfield2_t
+    | vcfdfield2_t
+    | vfdslice_t
+    | vcfdslice_t
+    | ArrayLike
+    | None,
+    shape: Sequence[int],
+    nvdim: int = 3,
+) -> (
+    fdfield_t
+    | cfdfield_t
+    | fdfield2_t
+    | cfdfield2_t
+    | fdslice_t
+    | cfdslice_t
+    | NDArray
+    | None
+)
+
+ +
+ +

Perform the inverse of vec(): take a 1D ndarray and output an nvdim-component field + of form e.g. [f_x, f_y, f_z] (nvdim=3) where each of f_* is a len(shape)-dimensional + ndarray.

+

Returns None if called with v=None.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ v + + vfdfield_t | vcfdfield_t | vfdfield2_t | vcfdfield2_t | vfdslice_t | vcfdslice_t | ArrayLike | None + +
+

1D ndarray representing a vector field of shape shape (or None)

+
+
+ required +
+ shape + + Sequence[int] + +
+

shape of the vector field

+
+
+ required +
+ nvdim + + int + +
+

Number of components in each vector

+
+
+ 3 +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ fdfield_t | cfdfield_t | fdfield2_t | cfdfield2_t | fdslice_t | cfdslice_t | NDArray | None + +
+

[f_x, f_y, f_z] where each f_ is a len(shape) dimensional ndarray (or None)

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdmath.types + + +

+ +
+ +

Types shared across multiple submodules

+ + + + + + + + + + +
+ + + + + + + +
+ + + +

+ dx_lists_t + + + + module-attribute + + +

+
dx_lists_t = Sequence[
+    Sequence[NDArray[floating | complexfloating]]
+]
+
+ +
+ +

'dxes' datastructure which contains grid cell width information in the following format:

+
[[[dx_e[0], dx_e[1], ...], [dy_e[0], ...], [dz_e[0], ...]],
+ [[dx_h[0], dx_h[1], ...], [dy_h[0], ...], [dz_h[0], ...]]]
+
+

where dx_e[0] is the x-width of the x=0 cells, as used when calculating dE/dx, + and dy_h[0] is the y-width of the y=0 cells, as used when calculating dH/dy, etc.

+ +
+ +
+ +
+ + + +

+ dx_lists2_t + + + + module-attribute + + +

+
dx_lists2_t = Sequence[
+    Sequence[NDArray[floating | complexfloating]]
+]
+
+ +
+ +

2D 'dxes' datastructure which contains grid cell width information in the following format:

+
[[[dx_e[0], dx_e[1], ...], [dy_e[0], ...]],
+ [[dx_h[0], dx_h[1], ...], [dy_h[0], ...]]]
+
+

where dx_e[0] is the x-width of the x=0 cells, as used when calculating dE/dx, + and dy_h[0] is the y-width of the y=0 cells, as used when calculating dH/dy, etc.

+ +
+ +
+ +
+ + + +

+ dx_lists_mut + + + + module-attribute + + +

+
dx_lists_mut = MutableSequence[
+    MutableSequence[NDArray[floating | complexfloating]]
+]
+
+ +
+ +

Mutable version of dx_lists_t

+ +
+ +
+ +
+ + + +

+ dx_lists2_mut + + + + module-attribute + + +

+
dx_lists2_mut = MutableSequence[
+    MutableSequence[NDArray[floating | complexfloating]]
+]
+
+ +
+ +

Mutable version of dx_lists2_t

+ +
+ +
+ +
+ + + +

+ fdfield_updater_t + + + + module-attribute + + +

+
fdfield_updater_t = Callable[..., fdfield_t]
+
+ +
+ +

Convenience type for functions which take and return an fdfield_t

+ +
+ +
+ +
+ + + +

+ cfdfield_updater_t + + + + module-attribute + + +

+
cfdfield_updater_t = Callable[..., cfdfield_t]
+
+ +
+ +

Convenience type for functions which take and return an cfdfield_t

+ +
+ +
+ + +
+ + + +

+ fdfield + + +

+
fdfield = fdfield_t | NDArray[floating]
+
+ +
+ +

Vector field with shape (3, X, Y, Z) (e.g. [E_x, E_y, E_z])

+
+ +
+ +
+ + + +

+ vfdfield + + +

+
vfdfield = vfdfield_t | NDArray[floating]
+
+ +
+ +

Linearized vector field (single vector of length 3XY*Z)

+
+ +
+ +
+ + + +

+ cfdfield + + +

+
cfdfield = cfdfield_t | NDArray[complexfloating]
+
+ +
+ +

Complex vector field with shape (3, X, Y, Z) (e.g. [E_x, E_y, E_z])

+
+ +
+ +
+ + + +

+ vcfdfield + + +

+
vcfdfield = vcfdfield_t | NDArray[complexfloating]
+
+ +
+ +

Linearized complex vector field (single vector of length 3XY*Z)

+
+ +
+ +
+ + + +

+ fdslice + + +

+
fdslice = fdslice_t | NDArray[floating]
+
+ +
+ +

Vector field slice with shape (3, X, Y) (e.g. [E_x, E_y, E_z] at a single Z position)

+
+ +
+ +
+ + + +

+ vfdslice + + +

+
vfdslice = vfdslice_t | NDArray[floating]
+
+ +
+ +

Linearized vector field slice (single vector of length 3XY)

+
+ +
+ +
+ + + +

+ cfdslice + + +

+
cfdslice = cfdslice_t | NDArray[complexfloating]
+
+ +
+ +

Complex vector field slice with shape (3, X, Y) (e.g. [E_x, E_y, E_z] at a single Z position)

+
+ +
+ +
+ + + +

+ vcfdslice + + +

+
vcfdslice = vcfdslice_t | NDArray[complexfloating]
+
+ +
+ +

Linearized complex vector field slice (single vector of length 3XY)

+
+ +
+ +
+ + + +

+ fdfield2 + + +

+
fdfield2 = fdfield2_t | NDArray[floating]
+
+ +
+ +

2D Vector field with shape (2, X, Y) (e.g. [E_x, E_y])

+
+ +
+ +
+ + + +

+ vfdfield2 + + +

+
vfdfield2 = vfdfield2_t | NDArray[floating]
+
+ +
+ +

2D Linearized vector field (single vector of length 2XY)

+
+ +
+ +
+ + + +

+ cfdfield2 + + +

+
cfdfield2 = cfdfield2_t | NDArray[complexfloating]
+
+ +
+ +

2D Complex vector field with shape (2, X, Y) (e.g. [E_x, E_y])

+
+ +
+ +
+ + + +

+ vcfdfield2 + + +

+
vcfdfield2 = vcfdfield2_t | NDArray[complexfloating]
+
+ +
+ +

2D Linearized complex vector field (single vector of length 2XY)

+
+ +
+ + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/fdtd/index.html b/api/fdtd/index.html new file mode 100644 index 0000000..6511297 --- /dev/null +++ b/api/fdtd/index.html @@ -0,0 +1,3818 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fdtd - meanas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + +

fdtd

+ + +
+ + + +

+ meanas.fdtd + + +

+ +
+ +

Utilities for running finite-difference time-domain (FDTD) simulations

+

See the discussion of Maxwell's Equations in meanas.fdmath for basic +mathematical background.

+

Timestep

+

From the discussion of "Plane waves and the Dispersion relation" in meanas.fdmath, +we have

+
\[ c^2 \Delta_t^2 = \frac{\Delta_t^2}{\mu \epsilon} < 1/(\frac{1}{\Delta_x^2} + \frac{1}{\Delta_y^2} + \frac{1}{\Delta_z^2}) \]
+

or, if \(\Delta_x = \Delta_y = \Delta_z\), then \(c \Delta_t < \frac{\Delta_x}{\sqrt{3}}\).

+

Based on this, we can set

+
dt = sqrt(mu.min() * epsilon.min()) / sqrt(1/dx_min**2 + 1/dy_min**2 + 1/dz_min**2)
+
+

The dx_min, dy_min, dz_min should be the minimum value across both the base and derived grids.

+

Poynting Vector and Energy Conservation

+

Let

+
\[ \begin{aligned} + \tilde{S}_{l, l', \vec{r}} &=& &\tilde{E}_{l, \vec{r}} \otimes \hat{H}_{l', \vec{r} + \frac{1}{2}} \\ + &=& &\vec{x} (\tilde{E}^y_{l,m+1,n,p} \hat{H}^z_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^z_{l,m+1,n,p} \hat{H}^y_{l', \vec{r} + \frac{1}{2}}) \\ + & &+ &\vec{y} (\tilde{E}^z_{l,m,n+1,p} \hat{H}^x_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^x_{l,m,n+1,p} \hat{H}^z_{l', \vec{r} + \frac{1}{2}}) \\ + & &+ &\vec{z} (\tilde{E}^x_{l,m,n,p+1} \hat{H}^y_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^y_{l,m,n,p+1} \hat{H}^z_{l', \vec{r} + \frac{1}{2}}) + \end{aligned} +\]
+

where \(\vec{r} = (m, n, p)\) and \(\otimes\) is a modified cross product +in which the \(\tilde{E}\) terms are shifted as indicated.

+

By taking the divergence and rearranging terms, we can show that

+
\[ + \begin{aligned} + \hat{\nabla} \cdot \tilde{S}_{l, l', \vec{r}} + &= \hat{\nabla} \cdot (\tilde{E}_{l, \vec{r}} \otimes \hat{H}_{l', \vec{r} + \frac{1}{2}}) \\ + &= \hat{H}_{l', \vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l, \vec{r}} - + \tilde{E}_{l, \vec{r}} \cdot \hat{\nabla} \times \hat{H}_{l', \vec{r} + \frac{1}{2}} \\ + &= \hat{H}_{l', \vec{r} + \frac{1}{2}} \cdot + (-\tilde{\partial}_t \mu_{\vec{r} + \frac{1}{2}} \hat{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} - + \hat{M}_{l, \vec{r} + \frac{1}{2}}) - + \tilde{E}_{l, \vec{r}} \cdot (\hat{\partial}_t \tilde{\epsilon}_{\vec{r}} \tilde{E}_{l'+\frac{1}{2}, \vec{r}} + + \tilde{J}_{l', \vec{r}}) \\ + &= \hat{H}_{l'} \cdot (-\mu / \Delta_t)(\hat{H}_{l + \frac{1}{2}} - \hat{H}_{l - \frac{1}{2}}) - + \tilde{E}_l \cdot (\epsilon / \Delta_t )(\tilde{E}_{l'+\frac{1}{2}} - \tilde{E}_{l'-\frac{1}{2}}) + - \hat{H}_{l'} \cdot \hat{M}_{l} - \tilde{E}_l \cdot \tilde{J}_{l'} \\ + \end{aligned} +\]
+

where in the last line the spatial subscripts have been dropped to emphasize +the time subscripts \(l, l'\), i.e.

+
\[ + \begin{aligned} + \tilde{E}_l &= \tilde{E}_{l, \vec{r}} \\ + \hat{H}_l &= \tilde{H}_{l, \vec{r} + \frac{1}{2}} \\ + \tilde{\epsilon} &= \tilde{\epsilon}_{\vec{r}} \\ + \end{aligned} +\]
+

etc. +For \(l' = l + \frac{1}{2}\) we get

+
\[ + \begin{aligned} + \hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} + &= \hat{H}_{l + \frac{1}{2}} \cdot + (-\mu / \Delta_t)(\hat{H}_{l + \frac{1}{2}} - \hat{H}_{l - \frac{1}{2}}) - + \tilde{E}_l \cdot (\epsilon / \Delta_t)(\tilde{E}_{l+1} - \tilde{E}_l) + - \hat{H}_{l'} \cdot \hat{M}_l - \tilde{E}_l \cdot \tilde{J}_{l + \frac{1}{2}} \\ + &= (-\mu / \Delta_t)(\hat{H}^2_{l + \frac{1}{2}} - \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}}) - + (\epsilon / \Delta_t)(\tilde{E}_{l+1} \cdot \tilde{E}_l - \tilde{E}^2_l) + - \hat{H}_{l'} \cdot \hat{M}_l - \tilde{E}_l \cdot \tilde{J}_{l + \frac{1}{2}} \\ + &= -(\mu \hat{H}^2_{l + \frac{1}{2}} + +\epsilon \tilde{E}_{l+1} \cdot \tilde{E}_l) / \Delta_t \\ + +(\mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} + +\epsilon \tilde{E}^2_l) / \Delta_t \\ + - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ + \end{aligned} +\]
+

and for \(l' = l - \frac{1}{2}\),

+
\[ + \begin{aligned} + \hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} + &= (\mu \hat{H}^2_{l - \frac{1}{2}} + +\epsilon \tilde{E}_{l-1} \cdot \tilde{E}_l) / \Delta_t \\ + -(\mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} + +\epsilon \tilde{E}^2_l) / \Delta_t \\ + - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ + \end{aligned} +\]
+

These two results form the discrete time-domain analogue to Poynting's theorem. +They hint at the expressions for the energy, which can be calculated at the same +time-index as either the E or H field:

+
\[ + \begin{aligned} + U_l &= \epsilon \tilde{E}^2_l + \mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} \\ + U_{l + \frac{1}{2}} &= \epsilon \tilde{E}_l \cdot \tilde{E}_{l + 1} + \mu \hat{H}^2_{l + \frac{1}{2}} \\ + \end{aligned} +\]
+

Rewriting the Poynting theorem in terms of the energy expressions,

+
\[ + \begin{aligned} + (U_{l+\frac{1}{2}} - U_l) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} \\ + - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ + (U_l - U_{l-\frac{1}{2}}) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} \\ + - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ + - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ + \end{aligned} +\]
+

This result is exact and should practically hold to within numerical precision. No time- +or spatial-averaging is necessary.

+

Note that each value of \(J\) contributes to the energy twice (i.e. once per field update) +despite only causing the value of \(E\) to change once (same for \(M\) and \(H\)).

+

Sources

+

It is often useful to excite the simulation with an arbitrary broadband pulse and then +extract the frequency-domain response by performing an on-the-fly Fourier transform +of the time-domain fields.

+

accumulate_phasor in meanas.fdtd.phasor performs the phase accumulation for one +or more target frequencies, while leaving source normalization and simulation-loop +policy to the caller. Convenience wrappers accumulate_phasor_e, +accumulate_phasor_h, and accumulate_phasor_j apply the usual Yee time offsets. +The helpers accumulate

+
\[ \Delta_t \sum_l w_l e^{-i \omega t_l} f_l \]
+

with caller-provided sample times and weights. In this codebase the matching +electric-current convention is typically E -= dt * J / epsilon in FDTD and +-i \omega J on the right-hand side of the FDFD wave equation.

+

The Ricker wavelet (normalized second derivative of a Gaussian) is commonly used for the pulse +shape. It can be written

+
\[ f_r(t) = (1 - \frac{1}{2} (\omega (t - \tau))^2) e^{-(\frac{\omega (t - \tau)}{2})^2} \]
+

with \(\tau > \frac{2 * \pi}{\omega}\) as a minimum delay to avoid a discontinuity at +t=0 (assuming the source is off for t<0 this gives \(\sim 10^{-3}\) error at t=0).

+

Boundary conditions

+

meanas.fdtd exposes two boundary-related building blocks:

+
    +
  • conducting_boundary(...) for simple perfect-electric-conductor style field + clamping at one face of the domain.
  • +
  • cpml_params(...) and updates_with_cpml(...) for convolutional perfectly + matched layers (CPMLs) attached to one or more faces of the Yee grid.
  • +
+

updates_with_cpml(...) accepts a three-by-two table of CPML parameter blocks:

+
cpml_params[axis][polarity_index]
+
+

where axis is 0, 1, or 2 and polarity_index corresponds to (-1, +1). +Passing None for one entry disables CPML on that face while leaving the other +directions unchanged. This is how mixed boundary setups such as "absorbing in x, +periodic in y/z" are expressed.

+

When comparing an FDTD run against an FDFD solve, use the same stretched +coordinate system in both places:

+
    +
  1. Build the FDTD update with the desired CPML parameters.
  2. +
  3. Stretch the FDFD dxes with the matching SCPML transform.
  4. +
  5. Compare the extracted phasor against the FDFD field or residual on those + stretched dxes.
  6. +
+

The electric-current sign convention used throughout the examples and tests is

+
\[ +E \leftarrow E - \Delta_t J / \epsilon +\]
+

which matches the FDFD right-hand side

+
\[ +-i \omega J. +\]
+ + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +

Core update and analysis helpers

+ + +
+ + + +

+ meanas.fdtd.base + + +

+ +
+ +

Basic FDTD field updates

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ maxwell_e + + +

+
maxwell_e(
+    dt: float, dxes: dx_lists_t | None = None
+) -> fdfield_updater_t
+
+ +
+ +

Build a function which performs a portion the time-domain E-field update,

+
E += curl_back(H[t]) / epsilon
+
+

The full update should be

+
E += (curl_back(H[t]) + J) / epsilon
+
+

which requires an additional step of E += J / epsilon which is not performed +by the generated function.

+

See meanas.fdmath for descriptions of

+
    +
  • This update step: "Maxwell's equations" section
  • +
  • dxes: "Datastructure: dx_lists_t" section
  • +
  • epsilon: "Permittivity and Permeability" section
  • +
+

Also see the "Timestep" section of meanas.fdtd for a discussion of +the dt parameter.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dt + + float + +
+

Timestep. See meanas.fdtd for details.

+
+
+ required +
+ dxes + + dx_lists_t | None + +
+

Grid description; see meanas.fdmath.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ fdfield_updater_t + +
+

Function f(E_old, H_old, epsilon) -> E_new.

+
+
+ + +
+ +
+ +
+ + +

+ maxwell_h + + +

+
maxwell_h(
+    dt: float, dxes: dx_lists_t | None = None
+) -> fdfield_updater_t
+
+ +
+ +

Build a function which performs part of the time-domain H-field update,

+
H -= curl_forward(E[t]) / mu
+
+

The full update should be

+
H -= (curl_forward(E[t]) + M) / mu
+
+

which requires an additional step of H -= M / mu which is not performed +by the generated function; this step can be omitted if there is no magnetic +current M.

+

See meanas.fdmath for descriptions of

+
    +
  • This update step: "Maxwell's equations" section
  • +
  • dxes: "Datastructure: dx_lists_t" section
  • +
  • mu: "Permittivity and Permeability" section
  • +
+

Also see the "Timestep" section of meanas.fdtd for a discussion of +the dt parameter.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dt + + float + +
+

Timestep. See meanas.fdtd for details.

+
+
+ required +
+ dxes + + dx_lists_t | None + +
+

Grid description; see meanas.fdmath.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ fdfield_updater_t + +
+

Function f(E_old, H_old, epsilon) -> E_new.

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdtd.pml + + +

+ +
+ +

Convolutional perfectly matched layer (CPML) support for FDTD updates.

+

The helpers in this module construct per-face CPML parameters and then wrap the +standard Yee updates with the additional auxiliary psi fields needed by the +CPML recurrence.

+

The intended call pattern is:

+
    +
  1. Build a cpml_params[axis][polarity_index] table with cpml_params(...).
  2. +
  3. Pass that table into updates_with_cpml(...) together with dt, dxes, and + epsilon.
  4. +
  5. Advance the returned update_E / update_H closures in the simulation loop.
  6. +
+

Each face can be enabled or disabled independently by replacing one table entry +with None.

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ cpml_params + + +

+
cpml_params(
+    axis: int,
+    polarity: int,
+    dt: float,
+    thickness: int = 8,
+    ln_R_per_layer: float = -1.6,
+    epsilon_eff: float = 1,
+    mu_eff: float = 1,
+    m: float = 3.5,
+    ma: float = 1,
+    cfs_alpha: float = 0,
+) -> dict[str, Any]
+
+ +
+ +

Construct the parameter block for one CPML face.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ axis + + int + +
+

Which Cartesian axis the CPML is normal to (0, 1, or 2).

+
+
+ required +
+ polarity + + int + +
+

Which face along that axis (-1 for the low-index face, ++1 for the high-index face).

+
+
+ required +
+ dt + + float + +
+

Timestep used by the Yee update.

+
+
+ required +
+ thickness + + int + +
+

Number of Yee cells occupied by the CPML region.

+
+
+ 8 +
+ ln_R_per_layer + + float + +
+

Logarithmic attenuation target per layer.

+
+
+ -1.6 +
+ epsilon_eff + + float + +
+

Effective permittivity used when choosing the CPML scaling.

+
+
+ 1 +
+ mu_eff + + float + +
+

Effective permeability used when choosing the CPML scaling.

+
+
+ 1 +
+ m + + float + +
+

Polynomial grading exponent for sigma and kappa.

+
+
+ 3.5 +
+ ma + + float + +
+

Polynomial grading exponent for the complex-frequency shift alpha.

+
+
+ 1 +
+ cfs_alpha + + float + +
+

Maximum complex-frequency shift parameter.

+
+
+ 0 +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDescription
+ dict[str, Any] + +
+

Dictionary with:

+
+
+ dict[str, Any] + +
+
    +
  • param_e: (p0, p1, p2) arrays for the E update
  • +
+
+
+ dict[str, Any] + +
+
    +
  • param_h: (p0, p1, p2) arrays for the H update
  • +
+
+
+ dict[str, Any] + +
+
    +
  • region: slice tuple selecting the CPML cells on that face
  • +
+
+
+ + +
+ +
+ +
+ + +

+ updates_with_cpml + + +

+
updates_with_cpml(
+    cpml_params: Sequence[Sequence[dict[str, Any] | None]],
+    dt: float,
+    dxes: dx_lists_t,
+    epsilon: fdfield,
+    *,
+    dtype: DTypeLike = numpy.float32,
+) -> tuple[
+    Callable[[fdfield_t, fdfield_t, fdfield_t], None],
+    Callable[[fdfield_t, fdfield_t, fdfield_t], None],
+]
+
+ +
+ +

Build Yee-step update closures augmented with CPML terms.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ cpml_params + + Sequence[Sequence[dict[str, Any] | None]] + +
+

Three-by-two sequence indexed as [axis][polarity_index]. +Entries are the dictionaries returned by cpml_params(...); use +None to disable CPML on one face.

+
+
+ required +
+ dt + + float + +
+

Timestep.

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Yee-grid spacing lists [dx_e, dx_h].

+
+
+ required +
+ epsilon + + fdfield + +
+

Electric material distribution used by the E update.

+
+
+ required +
+ dtype + + DTypeLike + +
+

Storage dtype for the auxiliary CPML state arrays.

+
+
+ float32 +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDescription
+ Callable[[fdfield_t, fdfield_t, fdfield_t], None] + +
+

(update_E, update_H) closures with the same call shape as the basic

+
+
+ Callable[[fdfield_t, fdfield_t, fdfield_t], None] + +
+

Yee updates:

+
+
+ tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None], Callable[[fdfield_t, fdfield_t, fdfield_t], None]] + +
+
    +
  • update_E(e, h, epsilon)
  • +
+
+
+ tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None], Callable[[fdfield_t, fdfield_t, fdfield_t], None]] + +
+
    +
  • update_H(e, h, mu)
  • +
+
+
+ tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None], Callable[[fdfield_t, fdfield_t, fdfield_t], None]] + +
+

The closures retain the CPML auxiliary state internally.

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdtd.boundaries + + +

+ +
+ +

Boundary conditions

+

TODO conducting boundary documentation

+ + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdtd.energy + + +

+ +
+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ poynting + + +

+
poynting(
+    e: fdfield, h: fdfield, dxes: dx_lists_t | None = None
+) -> fdfield_t
+
+ +
+ +

Calculate the poynting vector S (\(S\)).

+

This is the energy transfer rate (amount of energy U per dt transferred +between adjacent cells) in each direction that happens during the half-step +bounded by the two provided fields.

+

The returned vector field S is the energy flow across +x, +y, and +z +boundaries of the corresponding U cell. For example,

+
    mx = numpy.roll(mask, -1, axis=0)
+    my = numpy.roll(mask, -1, axis=1)
+    mz = numpy.roll(mask, -1, axis=2)
+
+    u_hstep = fdtd.energy_hstep(e0=es[ii - 1], h1=hs[ii], e2=es[ii],     **args)
+    u_estep = fdtd.energy_estep(h0=hs[ii],     e1=es[ii], h2=hs[ii + 1], **args)
+    delta_j_B = fdtd.delta_energy_j(j0=js[ii], e1=es[ii], dxes=dxes)
+    du_half_h2e = u_estep - u_hstep - delta_j_B
+
+    s_h2e = -fdtd.poynting(e=es[ii], h=hs[ii], dxes=dxes) * dt
+    planes = [s_h2e[0, mask].sum(), -s_h2e[0, mx].sum(),
+              s_h2e[1, mask].sum(), -s_h2e[1, my].sum(),
+              s_h2e[2, mask].sum(), -s_h2e[2, mz].sum()]
+
+    assert_close(sum(planes), du_half_h2e[mask])
+
+

(see meanas.tests.test_fdtd.test_poynting_planes)

+

The full relationship is +$$ + \begin{aligned} + (U_{l+\frac{1}{2}} - U_l) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}{l, l + \frac{1}{2}} \ + - \hat{H}}{2}} \cdot \hat{Ml \ + - \tilde{E}_l \cdot \tilde{J} \ + (U_l - U_{l-\frac{1}{2}}) / \Delta_t + &= -\hat{\nabla} \cdot \tilde{S}}{2}{l, l - \frac{1}{2}} \ + - \hat{H}}{2}} \cdot \hat{Ml \ + - \tilde{E}_l \cdot \tilde{J} \ + \end{aligned} +$$}{2}

+

These equalities are exact and should practically hold to within numerical precision. +No time- or spatial-averaging is necessary. (See meanas.fdtd docs for derivation.)

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ e + + fdfield + +
+

E-field

+
+
+ required +
+ h + + fdfield + +
+

H-field (one half-timestep before or after e)

+
+
+ required +
+ dxes + + dx_lists_t | None + +
+

Grid description; see meanas.fdmath.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
s + fdfield_t + +
+

Vector field. Components indicate the energy transfer rate from the +corresponding energy cell into its +x, +y, and +z neighbors during +the half-step from the time of the earlier input field until the +time of later input field.

+
+
+ + +
+ +
+ +
+ + +

+ poynting_divergence + + +

+
poynting_divergence(
+    s: fdfield | None = None,
+    *,
+    e: fdfield | None = None,
+    h: fdfield | None = None,
+    dxes: dx_lists_t | None = None,
+) -> fdfield_t
+
+ +
+ +

Calculate the divergence of the poynting vector.

+

This is the net energy flow for each cell, i.e. the change in energy U +per dt caused by transfer of energy to nearby cells (rather than +absorption/emission by currents J or M).

+

See poynting and meanas.fdtd for more details. +Args: + s: Poynting vector, as calculated with poynting. Optional; caller + can provide e and h instead. + e: E-field (optional; need either s or both e and h) + h: H-field (optional; need either s or both e and h) + dxes: Grid description; see meanas.fdmath.

+ + +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
ds + fdfield_t + +
+

Divergence of the poynting vector. +Entries indicate the net energy flow out of the corresponding +energy cell.

+
+
+ + +
+ +
+ +
+ + +

+ energy_hstep + + +

+
energy_hstep(
+    e0: fdfield,
+    h1: fdfield,
+    e2: fdfield,
+    epsilon: fdfield | None = None,
+    mu: fdfield | None = None,
+    dxes: dx_lists_t | None = None,
+) -> fdfield_t
+
+ +
+ +

Calculate energy U at the time of the provided H-field h1.

+

TODO: Figure out what this means spatially.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ e0 + + fdfield + +
+

E-field one half-timestep before the energy.

+
+
+ required +
+ h1 + + fdfield + +
+

H-field (at the same timestep as the energy).

+
+
+ required +
+ e2 + + fdfield + +
+

E-field one half-timestep after the energy.

+
+
+ required +
+ epsilon + + fdfield | None + +
+

Dielectric constant distribution.

+
+
+ None +
+ mu + + fdfield | None + +
+

Magnetic permeability distribution.

+
+
+ None +
+ dxes + + dx_lists_t | None + +
+

Grid description; see meanas.fdmath.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ fdfield_t + +
+

Energy, at the time of the H-field h1.

+
+
+ + +
+ +
+ +
+ + +

+ energy_estep + + +

+
energy_estep(
+    h0: fdfield,
+    e1: fdfield,
+    h2: fdfield,
+    epsilon: fdfield | None = None,
+    mu: fdfield | None = None,
+    dxes: dx_lists_t | None = None,
+) -> fdfield_t
+
+ +
+ +

Calculate energy U at the time of the provided E-field e1.

+

TODO: Figure out what this means spatially.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ h0 + + fdfield + +
+

H-field one half-timestep before the energy.

+
+
+ required +
+ e1 + + fdfield + +
+

E-field (at the same timestep as the energy).

+
+
+ required +
+ h2 + + fdfield + +
+

H-field one half-timestep after the energy.

+
+
+ required +
+ epsilon + + fdfield | None + +
+

Dielectric constant distribution.

+
+
+ None +
+ mu + + fdfield | None + +
+

Magnetic permeability distribution.

+
+
+ None +
+ dxes + + dx_lists_t | None + +
+

Grid description; see meanas.fdmath.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ fdfield_t + +
+

Energy, at the time of the E-field e1.

+
+
+ + +
+ +
+ +
+ + +

+ delta_energy_h2e + + +

+
delta_energy_h2e(
+    dt: float,
+    e0: fdfield,
+    h1: fdfield,
+    e2: fdfield,
+    h3: fdfield,
+    epsilon: fdfield | None = None,
+    mu: fdfield | None = None,
+    dxes: dx_lists_t | None = None,
+) -> fdfield_t
+
+ +
+ +

Change in energy during the half-step from h1 to e2.

+

This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2)

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ e0 + + fdfield + +
+

E-field one half-timestep before the start of the energy delta.

+
+
+ required +
+ h1 + + fdfield + +
+

H-field at the start of the energy delta.

+
+
+ required +
+ e2 + + fdfield + +
+

E-field at the end of the energy delta (one half-timestep after h1).

+
+
+ required +
+ h3 + + fdfield + +
+

H-field one half-timestep after the end of the energy delta.

+
+
+ required +
+ epsilon + + fdfield | None + +
+

Dielectric constant distribution.

+
+
+ None +
+ mu + + fdfield | None + +
+

Magnetic permeability distribution.

+
+
+ None +
+ dxes + + dx_lists_t | None + +
+

Grid description; see meanas.fdmath.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ fdfield_t + +
+

Change in energy from the time of h1 to the time of e2.

+
+
+ + +
+ +
+ +
+ + +

+ delta_energy_e2h + + +

+
delta_energy_e2h(
+    dt: float,
+    h0: fdfield,
+    e1: fdfield,
+    h2: fdfield,
+    e3: fdfield,
+    epsilon: fdfield | None = None,
+    mu: fdfield | None = None,
+    dxes: dx_lists_t | None = None,
+) -> fdfield_t
+
+ +
+ +

Change in energy during the half-step from e1 to h2.

+

This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2)

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ h0 + + fdfield + +
+

E-field one half-timestep before the start of the energy delta.

+
+
+ required +
+ e1 + + fdfield + +
+

H-field at the start of the energy delta.

+
+
+ required +
+ h2 + + fdfield + +
+

E-field at the end of the energy delta (one half-timestep after e1).

+
+
+ required +
+ e3 + + fdfield + +
+

H-field one half-timestep after the end of the energy delta.

+
+
+ required +
+ epsilon + + fdfield | None + +
+

Dielectric constant distribution.

+
+
+ None +
+ mu + + fdfield | None + +
+

Magnetic permeability distribution.

+
+
+ None +
+ dxes + + dx_lists_t | None + +
+

Grid description; see meanas.fdmath.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ fdfield_t + +
+

Change in energy from the time of e1 to the time of h2.

+
+
+ + +
+ +
+ +
+ + +

+ delta_energy_j + + +

+
delta_energy_j(
+    j0: fdfield, e1: fdfield, dxes: dx_lists_t | None = None
+) -> fdfield_t
+
+ +
+ +

Calculate the electric-current work term \(J \cdot E\) on the Yee grid.

+

This is the source contribution that appears beside the flux divergence in +the discrete Poynting identities documented in meanas.fdtd.

+

Note that each value of J contributes twice in a full Yee cycle (once per +half-step energy balance) even though it directly changes E only once.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ j0 + + fdfield + +
+

Electric-current density sampled at the same half-step as the +current work term.

+
+
+ required +
+ e1 + + fdfield + +
+

Electric field sampled at the matching integer timestep.

+
+
+ required +
+ dxes + + dx_lists_t | None + +
+

Grid description; defaults to unit spacing.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ fdfield_t + +
+

Per-cell source-work contribution with the scalar field shape.

+
+
+ + +
+ +
+ +
+ + +

+ dxmul + + +

+
dxmul(
+    ee: fdfield,
+    hh: fdfield,
+    epsilon: fdfield | float | None = None,
+    mu: fdfield | float | None = None,
+    dxes: dx_lists_t | None = None,
+) -> fdfield_t
+
+ +
+ +

Multiply E- and H-like field products by material weights and cell volumes.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ ee + + fdfield + +
+

Three-component electric-field product, such as e0 * e2.

+
+
+ required +
+ hh + + fdfield + +
+

Three-component magnetic-field product, such as h1 * h1.

+
+
+ required +
+ epsilon + + fdfield | float | None + +
+

Electric material weight; defaults to 1.

+
+
+ None +
+ mu + + fdfield | float | None + +
+

Magnetic material weight; defaults to 1.

+
+
+ None +
+ dxes + + dx_lists_t | None + +
+

Grid description; defaults to unit spacing.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ fdfield_t + +
+

Scalar field containing the weighted electric plus magnetic contribution

+
+
+ fdfield_t + +
+

for each Yee cell.

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdtd.phasor + + +

+ +
+ +

Helpers for extracting single- or multi-frequency phasors from FDTD samples.

+

These helpers are intentionally low-level: callers own the accumulator arrays and +the sampling policy. The accumulated quantity is

+
dt * sum(weight * exp(-1j * omega * t_step) * sample_step)
+
+

where t_step = (step + offset_steps) * dt.

+

The usual Yee offsets are:

+
    +
  • accumulate_phasor_e(..., step=l) for E_l
  • +
  • accumulate_phasor_h(..., step=l) for H_{l + 1/2}
  • +
  • accumulate_phasor_j(..., step=l) for J_{l + 1/2}
  • +
+

These helpers do not choose warmup/accumulation windows or pulse-envelope +normalization. They also do not impose a current sign convention. In this +codebase, electric-current injection normally appears as E -= dt * J / epsilon, +which matches the FDFD right-hand side -1j * omega * J.

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ accumulate_phasor + + +

+
accumulate_phasor(
+    accumulator: NDArray[complexfloating],
+    omegas: float
+    | complex
+    | Sequence[float | complex]
+    | NDArray,
+    dt: float,
+    sample: ArrayLike,
+    step: int,
+    *,
+    offset_steps: float = 0.0,
+    weight: ArrayLike = 1.0,
+) -> NDArray[numpy.complexfloating]
+
+ +
+ +

Add one time-domain sample into a phasor accumulator.

+

The added quantity is

+
dt * weight * exp(-1j * omega * t_step) * sample
+
+

where t_step = (step + offset_steps) * dt.

+ + +
+ Note +

This helper already multiplies by dt. If the caller's normalization +factor was derived from a discrete sum that already includes dt, pass +weight / dt here.

+
+ +
+ +
+ +
+ + +

+ accumulate_phasor_e + + +

+
accumulate_phasor_e(
+    accumulator: NDArray[complexfloating],
+    omegas: float
+    | complex
+    | Sequence[float | complex]
+    | NDArray,
+    dt: float,
+    sample: ArrayLike,
+    step: int,
+    *,
+    weight: ArrayLike = 1.0,
+) -> NDArray[numpy.complexfloating]
+
+ +
+ +

Accumulate an E-field sample taken at integer timestep step.

+ + +
+ +
+ +
+ + +

+ accumulate_phasor_h + + +

+
accumulate_phasor_h(
+    accumulator: NDArray[complexfloating],
+    omegas: float
+    | complex
+    | Sequence[float | complex]
+    | NDArray,
+    dt: float,
+    sample: ArrayLike,
+    step: int,
+    *,
+    weight: ArrayLike = 1.0,
+) -> NDArray[numpy.complexfloating]
+
+ +
+ +

Accumulate an H-field sample corresponding to H_{step + 1/2}.

+ + +
+ +
+ +
+ + +

+ accumulate_phasor_j + + +

+
accumulate_phasor_j(
+    accumulator: NDArray[complexfloating],
+    omegas: float
+    | complex
+    | Sequence[float | complex]
+    | NDArray,
+    dt: float,
+    sample: ArrayLike,
+    step: int,
+    *,
+    weight: ArrayLike = 1.0,
+) -> NDArray[numpy.complexfloating]
+
+ +
+ +

Accumulate a current sample corresponding to J_{step + 1/2}.

+ + +
+ +
+ + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/index.html b/api/index.html new file mode 100644 index 0000000..7dfa7f0 --- /dev/null +++ b/api/index.html @@ -0,0 +1,621 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Overview - meanas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + +

API Overview

+

The package is documented directly from its docstrings. The most useful entry +points are:

+
    +
  • meanas: top-level package overview
  • +
  • eigensolvers: generic eigenvalue utilities used by the mode solvers
  • +
  • fdfd: frequency-domain operators, sources, PML, solvers, and far-field transforms
  • +
  • waveguides: straight, cylindrical, and 3D waveguide mode helpers
  • +
  • fdtd: timestepping, CPML, energy/flux helpers, and phasor extraction
  • +
  • fdmath: shared discrete operators, vectorization helpers, and derivation background
  • +
+

The waveguide and FDTD pages are the best places to start if you want the +mathematical derivations rather than just the callable reference.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/meanas/index.html b/api/meanas/index.html new file mode 100644 index 0000000..7f6a325 --- /dev/null +++ b/api/meanas/index.html @@ -0,0 +1,730 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + meanas - meanas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + +

meanas

+ + +
+ + + +

+ meanas + + +

+ +
+ +

Electromagnetic simulation tools

+

See the readme or import meanas; help(meanas) for more info.

+ + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/waveguides/index.html b/api/waveguides/index.html new file mode 100644 index 0000000..64260c7 --- /dev/null +++ b/api/waveguides/index.html @@ -0,0 +1,6832 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + waveguides - meanas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+ +
+ + + + + +

waveguides

+ + +
+ + + +

+ meanas.fdfd.waveguide_2d + + +

+ +
+ +

Operators and helper functions for waveguides with unchanging cross-section.

+

The propagation direction is chosen to be along the z axis, and all fields +are given an implicit z-dependence of the form exp(-1 * wavenumber * z).

+

As the z-dependence is known, all the functions in this file assume a 2D grid + (i.e. dxes = [[[dx_e[0], dx_e[1], ...], [dy_e[0], ...]], [[dx_h[0], ...], [dy_h[0], ...]]]).

+

===============

+

Consider Maxwell's equations in continuous space, in the frequency domain. Assuming +a structure with some (x, y) cross-section extending uniformly into the z dimension, +with a diagonal \(\epsilon\) tensor, we have

+
\[ +\begin{aligned} +\nabla \times \vec{E}(x, y, z) &= -\imath \omega \mu \vec{H} \\ +\nabla \times \vec{H}(x, y, z) &= \imath \omega \epsilon \vec{E} \\ +\vec{E}(x,y,z) &= (\vec{E}_t(x, y) + E_z(x, y)\vec{z}) e^{-\imath \beta z} \\ +\vec{H}(x,y,z) &= (\vec{H}_t(x, y) + H_z(x, y)\vec{z}) e^{-\imath \beta z} \\ +\end{aligned} +\]
+

Expanding the first two equations into vector components, we get

+
\[ +\begin{aligned} +-\imath \omega \mu_{xx} H_x &= \partial_y E_z - \partial_z E_y \\ +-\imath \omega \mu_{yy} H_y &= \partial_z E_x - \partial_x E_z \\ +-\imath \omega \mu_{zz} H_z &= \partial_x E_y - \partial_y E_x \\ +\imath \omega \epsilon_{xx} E_x &= \partial_y H_z - \partial_z H_y \\ +\imath \omega \epsilon_{yy} E_y &= \partial_z H_x - \partial_x H_z \\ +\imath \omega \epsilon_{zz} E_z &= \partial_x H_y - \partial_y H_x \\ +\end{aligned} +\]
+

Substituting in our expressions for \(\vec{E}\), \(\vec{H}\) and discretizing:

+
\[ +\begin{aligned} +-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \imath \beta E_y \\ +-\imath \omega \mu_{yy} H_y &= -\imath \beta E_x - \tilde{\partial}_x E_z \\ +-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\ +\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta H_y \\ +\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \hat{\partial}_x H_z \\ +\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\ +\end{aligned} +\]
+

Rewrite the last three equations as

+
\[ +\begin{aligned} +\imath \beta H_y &= \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\ +\imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\ +\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\ +\end{aligned} +\]
+

Now apply \(\imath \beta \tilde{\partial}_x\) to the last equation, +then substitute in for \(\imath \beta H_x\) and \(\imath \beta H_y\):

+
\[ +\begin{aligned} +\imath \beta \tilde{\partial}_x \imath \omega E_z &= \imath \beta \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y + - \imath \beta \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\ + &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z) + - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\ + &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x) + - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y) \\ +\imath \beta \tilde{\partial}_x E_z &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\ +\end{aligned} +\]
+

With a similar approach (but using \(\imath \beta \tilde{\partial}_y\) instead), we can get

+
\[ +\begin{aligned} +\imath \beta \tilde{\partial}_y E_z &= \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\ +\end{aligned} +\]
+

We can combine this equation for \(\imath \beta \tilde{\partial}_y E_z\) with +the unused \(\imath \omega \mu_{xx} H_x\) and \(\imath \omega \mu_{yy} H_y\) equations to get

+
\[ +\begin{aligned} +-\imath \omega \mu_{xx} \imath \beta H_x &= -\beta^2 E_y + \imath \beta \tilde{\partial}_y E_z \\ +-\imath \omega \mu_{xx} \imath \beta H_x &= -\beta^2 E_y + \tilde{\partial}_y ( + \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + )\\ +\end{aligned} +\]
+

and

+
\[ +\begin{aligned} +-\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \imath \beta \tilde{\partial}_x E_z \\ +-\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \tilde{\partial}_x ( + \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + )\\ +\end{aligned} +\]
+

However, based on our rewritten equation for \(\imath \beta H_x\) and the so-far unused +equation for \(\imath \omega \mu_{zz} H_z\) we can also write

+
\[ +\begin{aligned} +-\imath \omega \mu_{xx} (\imath \beta H_x) &= -\imath \omega \mu_{xx} (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\ + &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + \imath \omega \mu_{xx} \hat{\partial}_x ( + \frac{1}{-\imath \omega \mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x)) \\ + &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\end{aligned} +\]
+

and, similarly,

+
\[ +\begin{aligned} +-\imath \omega \mu_{yy} (\imath \beta H_y) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x + +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\end{aligned} +\]
+

By combining both pairs of expressions, we get

+
\[ +\begin{aligned} +\beta^2 E_x - \tilde{\partial}_x ( + \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + ) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x + +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +-\beta^2 E_y + \tilde{\partial}_y ( + \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) + + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + ) &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\end{aligned} +\]
+

Using these, we can construct the eigenvalue problem

+
\[ +\beta^2 \begin{bmatrix} E_x \\ + E_y \end{bmatrix} = + (\omega^2 \begin{bmatrix} \mu_{yy} \epsilon_{xx} & 0 \\ + 0 & \mu_{xx} \epsilon_{yy} \end{bmatrix} + + \begin{bmatrix} -\mu_{yy} \hat{\partial}_y \\ + \mu_{xx} \hat{\partial}_x \end{bmatrix} \mu_{zz}^{-1} + \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} + + \begin{bmatrix} \tilde{\partial}_x \\ + \tilde{\partial}_y \end{bmatrix} \epsilon_{zz}^{-1} + \begin{bmatrix} \hat{\partial}_x \epsilon_{xx} & \hat{\partial}_y \epsilon_{yy} \end{bmatrix}) + \begin{bmatrix} E_x \\ + E_y \end{bmatrix} +\]
+

In the literature, \(\beta\) is usually used to denote the lossless/real part of the propagation constant, +but in meanas it is allowed to be complex.

+

An equivalent eigenvalue problem can be formed using the \(H_x\) and \(H_y\) fields, if those are more convenient.

+

Note that \(E_z\) was never discretized, so \(\beta\) will need adjustment to account for numerical dispersion +if the result is introduced into a space with a discretized z-axis.

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ operator_e + + +

+
operator_e(
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+) -> sparse.sparray
+
+ +
+ +

Waveguide operator of the form

+
omega**2 * mu * epsilon +
+mu * [[-Dy], [Dx]] / mu * [-Dy, Dx] +
+[[Dx], [Dy]] / epsilon * [Dx, Dy] * epsilon
+
+

for use with a field vector of the form cat([E_x, E_y]).

+

More precisely, the operator is

+
\[ +\omega^2 \begin{bmatrix} \mu_{yy} \epsilon_{xx} & 0 \\ + 0 & \mu_{xx} \epsilon_{yy} \end{bmatrix} + + \begin{bmatrix} -\mu_{yy} \hat{\partial}_y \\ + \mu_{xx} \hat{\partial}_x \end{bmatrix} \mu_{zz}^{-1} + \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} + + \begin{bmatrix} \tilde{\partial}_x \\ + \tilde{\partial}_y \end{bmatrix} \epsilon_{zz}^{-1} + \begin{bmatrix} \hat{\partial}_x \epsilon_{xx} & \hat{\partial}_y \epsilon_{yy} \end{bmatrix} +\]
+

\(\tilde{\partial}_x\) and \(\hat{\partial}_x\) are the forward and backward derivatives along x, +and each \(\epsilon_{xx}\), \(\mu_{yy}\), etc. is a diagonal matrix containing the vectorized material +property distribution.

+

This operator can be used to form an eigenvalue problem of the form +operator_e(...) @ [E_x, E_y] = wavenumber**2 * [E_x, E_y]

+

which can then be solved for the eigenmodes of the system (an exp(-i * wavenumber * z) +z-dependence is assumed for the fields).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

The angular frequency of the system.

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representation of the operator.

+
+
+ + +
+ +
+ +
+ + +

+ operator_h + + +

+
operator_h(
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+) -> sparse.sparray
+
+ +
+ +

Waveguide operator of the form

+
omega**2 * epsilon * mu +
+epsilon * [[-Dy], [Dx]] / epsilon * [-Dy, Dx] +
+[[Dx], [Dy]] / mu * [Dx, Dy] * mu
+
+

for use with a field vector of the form cat([H_x, H_y]).

+

More precisely, the operator is

+
\[ +\omega^2 \begin{bmatrix} \epsilon_{yy} \mu_{xx} & 0 \\ + 0 & \epsilon_{xx} \mu_{yy} \end{bmatrix} + + \begin{bmatrix} -\epsilon_{yy} \tilde{\partial}_y \\ + \epsilon_{xx} \tilde{\partial}_x \end{bmatrix} \epsilon_{zz}^{-1} + \begin{bmatrix} -\hat{\partial}_y & \hat{\partial}_x \end{bmatrix} + + \begin{bmatrix} \hat{\partial}_x \\ + \hat{\partial}_y \end{bmatrix} \mu_{zz}^{-1} + \begin{bmatrix} \tilde{\partial}_x \mu_{xx} & \tilde{\partial}_y \mu_{yy} \end{bmatrix} +\]
+

\(\tilde{\partial}_x\) and \(\hat{\partial}_x\) are the forward and backward derivatives along x, +and each \(\epsilon_{xx}\), \(\mu_{yy}\), etc. is a diagonal matrix containing the vectorized material +property distribution.

+

This operator can be used to form an eigenvalue problem of the form +operator_h(...) @ [H_x, H_y] = wavenumber**2 * [H_x, H_y]

+

which can then be solved for the eigenmodes of the system (an exp(-i * wavenumber * z) +z-dependence is assumed for the fields).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + complex + +
+

The angular frequency of the system.

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representation of the operator.

+
+
+ + +
+ +
+ +
+ + +

+ normalized_fields_e + + +

+
normalized_fields_e(
+    e_xy: vcfdfield2,
+    wavenumber: complex,
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+    prop_phase: float = 0,
+) -> tuple[vcfdslice_t, vcfdslice_t]
+
+ +
+ +

Given a vector e_xy containing the vectorized E_x and E_y fields, + returns normalized, vectorized E and H fields for the system.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ e_xy + + vcfdfield2 + +
+

Vector containing E_x and E_y fields

+
+
+ required +
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z). + It should satisfy operator_e() @ e_xy == wavenumber**2 * e_xy

+
+
+ required +
+ omega + + complex + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ prop_phase + + float + +
+

Phase shift (dz * corrected_wavenumber) over 1 cell in propagation direction. + Default 0 (continuous propagation direction, i.e. dz->0).

+
+
+ 0 +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ vcfdslice_t + +
+

(e, h), where each field is vectorized, normalized,

+
+
+ vcfdslice_t + +
+

and contains all three vector components.

+
+
+ + +
+ Notes +

e_xy is only the transverse electric eigenvector. This helper first +reconstructs the full three-component E and H fields with exy2e(...) +and exy2h(...), then normalizes them to unit forward power using +_normalized_fields(...).

+

The normalization target is

+
\[ +\Re\left[\mathrm{inner\_product}(e, h, \mathrm{conj\_h}=True)\right] = 1, +\]
+

so the returned fields represent a unit-power forward mode under the +discrete Yee-grid Poynting inner product.

+
+ +
+ +
+ +
+ + +

+ normalized_fields_h + + +

+
normalized_fields_h(
+    h_xy: vcfdfield2,
+    wavenumber: complex,
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+    prop_phase: float = 0,
+) -> tuple[vcfdslice_t, vcfdslice_t]
+
+ +
+ +

Given a vector h_xy containing the vectorized H_x and H_y fields, + returns normalized, vectorized E and H fields for the system.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ h_xy + + vcfdfield2 + +
+

Vector containing H_x and H_y fields

+
+
+ required +
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z). + It should satisfy operator_h() @ h_xy == wavenumber**2 * h_xy

+
+
+ required +
+ omega + + complex + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ prop_phase + + float + +
+

Phase shift (dz * corrected_wavenumber) over 1 cell in propagation direction. + Default 0 (continuous propagation direction, i.e. dz->0).

+
+
+ 0 +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ vcfdslice_t + +
+

(e, h), where each field is vectorized, normalized,

+
+
+ vcfdslice_t + +
+

and contains all three vector components.

+
+
+ + +
+ Notes +

This is the H_x/H_y analogue of normalized_fields_e(...). The final +normalized mode should describe the same physical solution, but because +the overall complex phase and sign are chosen heuristically, +normalized_fields_e(...) and normalized_fields_h(...) need not return +identical representatives for nearly symmetric modes.

+
+ +
+ +
+ +
+ + +

+ exy2h + + +

+
exy2h(
+    wavenumber: complex,
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+) -> sparse.sparray
+
+ +
+ +

Operator which transforms the vector e_xy containing the vectorized E_x and E_y fields, + into a vectorized H containing all three H components

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z). + It should satisfy operator_e() @ e_xy == wavenumber**2 * e_xy

+
+
+ required +
+ omega + + complex + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representing the operator.

+
+
+ + +
+ +
+ +
+ + +

+ hxy2e + + +

+
hxy2e(
+    wavenumber: complex,
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+) -> sparse.sparray
+
+ +
+ +

Operator which transforms the vector h_xy containing the vectorized H_x and H_y fields, + into a vectorized E containing all three E components

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z). + It should satisfy operator_h() @ h_xy == wavenumber**2 * h_xy

+
+
+ required +
+ omega + + complex + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representing the operator.

+
+
+ + +
+ +
+ +
+ + +

+ hxy2h + + +

+
hxy2h(
+    wavenumber: complex,
+    dxes: dx_lists2_t,
+    mu: vfdslice | None = None,
+) -> sparse.sparray
+
+ +
+ +

Operator which transforms the vector h_xy containing the vectorized H_x and H_y fields, + into a vectorized H containing all three H components

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z). + It should satisfy operator_h() @ h_xy == wavenumber**2 * h_xy

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representing the operator.

+
+
+ + +
+ +
+ +
+ + +

+ exy2e + + +

+
exy2e(
+    wavenumber: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+) -> sparse.sparray
+
+ +
+ +

Operator which transforms the vector e_xy containing the vectorized E_x and E_y fields, + into a vectorized E containing all three E components

+

From the operator derivation (see module docs), we have

+
\[ +\imath \omega \epsilon_{zz} E_z = \hat{\partial}_x H_y - \hat{\partial}_y H_x \\ +\]
+

as well as the intermediate equations

+
\[ +\begin{aligned} +\imath \beta H_y &= \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\ +\imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\ +\end{aligned} +\]
+

Combining these, we get

+
\[ +\begin{aligned} +E_z &= \frac{1}{- \omega \beta \epsilon_{zz}} (( + \hat{\partial}_y \hat{\partial}_x H_z + -\hat{\partial}_x \hat{\partial}_y H_z) + + \imath \omega (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y)) + &= \frac{1}{\imath \beta \epsilon_{zz}} (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y) +\end{aligned} +\]
+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z) + It should satisfy operator_e() @ e_xy == wavenumber**2 * e_xy

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representing the operator.

+
+
+ + +
+ +
+ +
+ + +

+ e2h + + +

+
e2h(
+    wavenumber: complex,
+    omega: complex,
+    dxes: dx_lists2_t,
+    mu: vfdslice | None = None,
+) -> sparse.sparray
+
+ +
+ +

Returns an operator which, when applied to a vectorized E eigenfield, produces + the vectorized H eigenfield slice.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z)

+
+
+ required +
+ omega + + complex + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representation of the operator.

+
+
+ + +
+ +
+ +
+ + +

+ h2e + + +

+
h2e(
+    wavenumber: complex,
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+) -> sparse.sparray
+
+ +
+ +

Returns an operator which, when applied to a vectorized H eigenfield, produces + the vectorized E eigenfield slice.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z)

+
+
+ required +
+ omega + + complex + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representation of the operator.

+
+
+ + +
+ +
+ +
+ + +

+ curl_e + + +

+
curl_e(
+    wavenumber: complex, dxes: dx_lists2_t
+) -> sparse.sparray
+
+ +
+ +

Discretized curl operator for use with the waveguide E field slice.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z)

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representation of the operator.

+
+
+ + +
+ +
+ +
+ + +

+ curl_h + + +

+
curl_h(
+    wavenumber: complex, dxes: dx_lists2_t
+) -> sparse.sparray
+
+ +
+ +

Discretized curl operator for use with the waveguide H field slice.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z)

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representation of the operator.

+
+
+ + +
+ +
+ +
+ + +

+ h_err + + +

+
h_err(
+    h: vcfdslice,
+    wavenumber: complex,
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+) -> float
+
+ +
+ +

Calculates the relative error in the H field

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ h + + vcfdslice + +
+

Vectorized H field

+
+
+ required +
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z)

+
+
+ required +
+ omega + + complex + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ float + +
+

Relative error norm(A_h @ h) / norm(h).

+
+
+ + +
+ +
+ +
+ + +

+ e_err + + +

+
e_err(
+    e: vcfdslice,
+    wavenumber: complex,
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+) -> float
+
+ +
+ +

Calculates the relative error in the E field

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ e + + vcfdslice + +
+

Vectorized E field

+
+
+ required +
+ wavenumber + + complex + +
+

Wavenumber assuming fields have z-dependence of exp(-i * wavenumber * z)

+
+
+ required +
+ omega + + complex + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ float + +
+

Relative error norm(A_e @ e) / norm(e).

+
+
+ + +
+ +
+ +
+ + +

+ sensitivity + + +

+
sensitivity(
+    e_norm: vcfdslice,
+    h_norm: vcfdslice,
+    wavenumber: complex,
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+) -> vcfdslice_t
+
+ +
+ +

Given a waveguide structure (dxes, epsilon, mu) and mode fields +(e_norm, h_norm, wavenumber, omega), calculates the sensitivity of the wavenumber +\(\beta\) to changes in the dielectric structure \(\epsilon\).

+

The output is a vector of the same size as vec(epsilon), with each element specifying the +sensitivity of wavenumber to changes in the corresponding element in vec(epsilon), i.e.

+
\[sens_{i} = \frac{\partial\beta}{\partial\epsilon_i}\]
+

An adjoint approach is used to calculate the sensitivity; the derivation is provided here:

+

Starting with the eigenvalue equation

+
\[\beta^2 E_{xy} = A_E E_{xy}\]
+

where \(A_E\) is the waveguide operator from operator_e(), and \(E_{xy} = \begin{bmatrix} E_x \\ + E_y \end{bmatrix}\), +we can differentiate with respect to one of the \(\epsilon\) elements (i.e. at one Yee grid point), \(\epsilon_i\):

+
\[ +(2 \beta) \partial_{\epsilon_i}(\beta) E_{xy} + \beta^2 \partial_{\epsilon_i} E_{xy} + = \partial_{\epsilon_i}(A_E) E_{xy} + A_E \partial_{\epsilon_i} E_{xy} +\]
+

We then multiply by \(H_{yx}^\star = \begin{bmatrix}H_y^\star \\ -H_x^\star \end{bmatrix}\) from the left:

+
\[ +(2 \beta) \partial_{\epsilon_i}(\beta) H_{yx}^\star E_{xy} + \beta^2 H_{yx}^\star \partial_{\epsilon_i} E_{xy} + = H_{yx}^\star \partial_{\epsilon_i}(A_E) E_{xy} + H_{yx}^\star A_E \partial_{\epsilon_i} E_{xy} +\]
+

However, \(H_{yx}^\star\) is actually a left-eigenvector of \(A_E\). This can be verified by inspecting +the form of operator_h (\(A_H\)) and comparing its conjugate transpose to operator_e (\(A_E\)). Also, note +\(H_{yx}^\star \cdot E_{xy} = H^\star \times E\) recalls the mode orthogonality relation. See doi:10.5194/ars-9-85-201 +for a similar approach. Therefore,

+
\[ +H_{yx}^\star A_E \partial_{\epsilon_i} E_{xy} = \beta^2 H_{yx}^\star \partial_{\epsilon_i} E_{xy} +\]
+

and we can simplify to

+
\[ +\partial_{\epsilon_i}(\beta) + = \frac{1}{2 \beta} \frac{H_{yx}^\star \partial_{\epsilon_i}(A_E) E_{xy} }{H_{yx}^\star E_{xy}} +\]
+

This expression can be quickly calculated for all \(i\) by writing out the various terms of +\(\partial_{\epsilon_i} A_E\) and recognizing that the vector-matrix-vector products (i.e. scalars) +\(sens_i = \vec{v}_{left} \partial_{\epsilon_i} (\epsilon_{xyz}) \vec{v}_{right}\), indexed by \(i\), can be expressed as +elementwise multiplications \(\vec{sens} = \vec{v}_{left} \star \vec{v}_{right}\)

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ e_norm + + vcfdslice + +
+

Normalized, vectorized E_xyz field for the mode. E.g. as returned by normalized_fields_e.

+
+
+ required +
+ h_norm + + vcfdslice + +
+

Normalized, vectorized H_xyz field for the mode. E.g. as returned by normalized_fields_e.

+
+
+ required +
+ wavenumber + + complex + +
+

Propagation constant for the mode. The z-axis is assumed to be continuous (i.e. without numerical dispersion).

+
+
+ required +
+ omega + + complex + +
+

The angular frequency of the system.

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ vcfdslice_t + +
+

Sparse matrix representation of the operator.

+
+
+ + +
+ +
+ +
+ + +

+ solve_modes + + +

+
solve_modes(
+    mode_numbers: Sequence[int],
+    omega: complex,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+    mode_margin: int = 2,
+) -> tuple[
+    NDArray[numpy.complex128], NDArray[numpy.complex128]
+]
+
+ +
+ +

Given a 2D region, attempts to solve for the eigenmode with the specified mode numbers.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ mode_numbers + + Sequence[int] + +
+

List of 0-indexed mode numbers to solve for

+
+
+ required +
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ epsilon + + vfdslice + +
+

Dielectric constant

+
+
+ required +
+ mu + + vfdslice | None + +
+

Magnetic permeability (default 1 everywhere)

+
+
+ None +
+ mode_margin + + int + +
+

The eigensolver will actually solve for (max(mode_number) + mode_margin) + modes, but only return the target mode. Increasing this value can improve the solver's + ability to find the correct mode. Default 2.

+
+
+ 2 +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
Name TypeDescription
e_xys + NDArray[complex128] + +
+

NDArray of vfdfield_t specifying fields. First dimension is mode number.

+
+
wavenumbers + NDArray[complex128] + +
+

list of wavenumbers

+
+
+ + +
+ +
+ +
+ + +

+ solve_mode + + +

+
solve_mode(
+    mode_number: int, *args: Any, **kwargs: Any
+) -> tuple[vcfdfield2_t, complex]
+
+ +
+ +

Wrapper around solve_modes() that solves for a single mode.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ mode_number + + int + +
+

0-indexed mode number to solve for

+
+
+ required +
+ *args + + Any + +
+

passed to solve_modes()

+
+
+ () +
+ **kwargs + + Any + +
+

passed to solve_modes()

+
+
+ {} +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple[vcfdfield2_t, complex] + +
+

(e_xy, wavenumber)

+
+
+ + +
+ +
+ +
+ + +

+ inner_product + + +

+
inner_product(
+    e1: vcfdfield2,
+    h2: vcfdfield2,
+    dxes: dx_lists2_t,
+    prop_phase: float = 0,
+    conj_h: bool = False,
+    trapezoid: bool = False,
+) -> complex
+
+ +
+ +

Compute the discrete waveguide overlap / Poynting inner product.

+

This is the 2D transverse integral corresponding to the time-averaged +longitudinal Poynting flux,

+
\[ +\frac{1}{2}\int (E_x H_y - E_y H_x) \, dx \, dy +\]
+

with the Yee-grid staggering and optional propagation-phase adjustment used +by the waveguide helpers in this module.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ e1 + + vcfdfield2 + +
+

Vectorized electric field, typically from exy2e(...) or +normalized_fields_e(...).

+
+
+ required +
+ h2 + + vcfdfield2 + +
+

Vectorized magnetic field, typically from hxy2h(...), +exy2h(...), or one of the normalization helpers.

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Two-dimensional Yee-grid spacing lists [dx_e, dx_h].

+
+
+ required +
+ prop_phase + + float + +
+

Phase advance over one propagation cell. This is used to +shift the H field into the same longitudinal reference plane as the +E field.

+
+
+ 0 +
+ conj_h + + bool + +
+

Whether to conjugate h2 before forming the overlap. Use +True for the usual time-averaged power normalization.

+
+
+ False +
+ trapezoid + + bool + +
+

Whether to use trapezoidal quadrature instead of the default +rectangular Yee-cell sum.

+
+
+ False +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ complex + +
+

Complex overlap / longitudinal power integral.

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdfd.waveguide_3d + + +

+ +
+ +

Tools for working with waveguide modes in 3D domains.

+

This module relies heavily on waveguide_2d and mostly just transforms +its parameters into 2D equivalents and expands the results back into 3D.

+

The intended workflow is:

+
    +
  1. Select a single-cell slice normal to the propagation axis.
  2. +
  3. Solve the corresponding 2D mode problem with solve_mode(...).
  4. +
  5. Turn that mode into a one-sided source with compute_source(...).
  6. +
  7. Build an overlap window with compute_overlap_e(...) for port readout.
  8. +
+

polarity is part of the public convention throughout this module:

+
    +
  • +1 means forward propagation toward increasing index along axis
  • +
  • -1 means backward propagation toward decreasing index along axis
  • +
+

That same convention controls which side of the selected slice is used for the +overlap window and how the expanded field is phased.

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ solve_mode + + +

+
solve_mode(
+    mode_number: int,
+    omega: complex,
+    dxes: dx_lists_t,
+    axis: int,
+    polarity: int,
+    slices: Sequence[slice],
+    epsilon: fdfield,
+    mu: fdfield | None = None,
+) -> dict[str, complex | NDArray[complexfloating]]
+
+ +
+ +

Given a 3D grid, selects a slice from the grid and attempts to +solve for an eigenmode propagating through that slice.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ mode_number + + int + +
+

Number of the mode, 0-indexed

+
+
+ required +
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ axis + + int + +
+

Propagation axis (0=x, 1=y, 2=z)

+
+
+ required +
+ polarity + + int + +
+

Propagation direction (+1 for +ve, -1 for -ve)

+
+
+ required +
+ slices + + Sequence[slice] + +
+

epsilon[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] must select exactly one item.

+
+
+ required +
+ epsilon + + fdfield + +
+

Dielectric constant

+
+
+ required +
+ mu + + fdfield | None + +
+

Magnetic permeability (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDescription
+ dict[str, complex | NDArray[complexfloating]] + +
+

Dictionary containing:

+
+
+ dict[str, complex | NDArray[complexfloating]] + +
+
    +
  • E: full-grid electric field for the solved mode
  • +
+
+
+ dict[str, complex | NDArray[complexfloating]] + +
+
    +
  • H: full-grid magnetic field for the solved mode
  • +
+
+
+ dict[str, complex | NDArray[complexfloating]] + +
+
    +
  • wavenumber: propagation constant corrected for the discretized +propagation axis
  • +
+
+
+ dict[str, complex | NDArray[complexfloating]] + +
+
    +
  • wavenumber_2d: propagation constant of the reduced 2D eigenproblem
  • +
+
+
+ + +
+ Notes +

The returned fields are normalized through the waveguide_2d +normalization convention before being expanded back to 3D.

+
+ +
+ +
+ +
+ + +

+ compute_source + + +

+
compute_source(
+    E: cfdfield,
+    wavenumber: complex,
+    omega: complex,
+    dxes: dx_lists_t,
+    axis: int,
+    polarity: int,
+    slices: Sequence[slice],
+    epsilon: fdfield,
+    mu: fdfield | None = None,
+) -> cfdfield_t
+
+ +
+ +

Given an eigenmode obtained by solve_mode, returns the current source distribution +necessary to position a unidirectional source at the slice location.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ E + + cfdfield + +
+

E-field of the mode

+
+
+ required +
+ wavenumber + + complex + +
+

Wavenumber of the mode

+
+
+ required +
+ omega + + complex + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ axis + + int + +
+

Propagation axis (0=x, 1=y, 2=z)

+
+
+ required +
+ polarity + + int + +
+

Propagation direction (+1 for +ve, -1 for -ve)

+
+
+ required +
+ slices + + Sequence[slice] + +
+

epsilon[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] should select only one item.

+
+
+ required +
+ mu + + fdfield | None + +
+

Magnetic permeability (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ cfdfield_t + +
+

J distribution for a one-sided electric-current source.

+
+
+ + +
+ Notes +

The source is built from the expanded mode field and a boundary-source +operator. The resulting current is intended to be injected with the +same sign convention used elsewhere in the package:

+

E -= dt * J / epsilon

+
+ +
+ +
+ +
+ + +

+ compute_overlap_e + + +

+
compute_overlap_e(
+    E: cfdfield_t,
+    wavenumber: complex,
+    dxes: dx_lists_t,
+    axis: int,
+    polarity: int,
+    slices: Sequence[slice],
+    omega: float,
+) -> cfdfield_t
+
+ +
+ +

Build an overlap field for projecting another 3D electric field onto a mode.

+

The returned field is intended for the discrete overlap expression

+
\[ +\sum \mathrm{overlap\_e} \; E_\mathrm{other}^* +\]
+

where the sum is over the full Yee-grid field storage.

+

The construction uses a two-cell window immediately upstream of the selected +slice:

+
    +
  • for polarity=+1, the two cells just before slices[axis].start
  • +
  • for polarity=-1, the two cells just after slices[axis].stop
  • +
+

The window is clipped to the simulation domain if necessary. A clipped but +non-empty window raises RuntimeWarning; an empty window raises +ValueError.

+

The derivation below assumes reflection symmetry and the standard waveguide +overlap relation involving

+
\[ +\int ((E \times H_\mathrm{mode}) + (E_\mathrm{mode} \times H)) \cdot dn. +\]
+

E x H_mode + E_mode x H +-> Ex Hmy - EyHmx + Emx Hy - Emy Hx (Z-prop) +Ex we/B Emx + Ex i/B dy Hmz - Ey (-we/B Emy) - Ey i/B dx Hmz +we/B (Ex Emx + Ey Emy) + i/B (Ex dy Hmz - Ey dx Hmz) +we/B (Ex Emx + Ey Emy) + i/B (Ex dy (dx Emy - dy Emx) - Ey dx (dx Emy - dy Emx)) +we/B (Ex Emx + Ey Emy) + i/B (Ex dy dx Emy - Ex dy dy Emx - Ey dx dx Emy - Ey dx dy Emx)

+

Ex j/wu (-jB Emx - dx Emz) - Ey j/wu (dy Emz + jB Emy) +B/wu (Ex Emx + Ey Emy) - j/wu (Ex dx Emz + Ey dy Emz)

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ E + + cfdfield_t + +
+

E-field of the mode

+
+
+ required +
+ wavenumber + + complex + +
+

Wavenumber of the mode

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ axis + + int + +
+

Propagation axis (0=x, 1=y, 2=z)

+
+
+ required +
+ polarity + + int + +
+

Propagation direction (+1 for +ve, -1 for -ve)

+
+
+ required +
+ slices + + Sequence[slice] + +
+

epsilon[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] should select only one item.

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ cfdfield_t + +
+

overlap_e normalized so that numpy.sum(overlap_e * E.conj()) == 1

+
+
+ cfdfield_t + +
+

over the retained overlap window.

+
+
+ + +
+ +
+ +
+ + +

+ expand_e + + +

+
expand_e(
+    E: cfdfield,
+    wavenumber: complex,
+    dxes: dx_lists_t,
+    axis: int,
+    polarity: int,
+    slices: Sequence[slice],
+) -> cfdfield_t
+
+ +
+ +

Given an eigenmode obtained by solve_mode, expands the E-field from the 2D +slice where the mode was calculated to the entire domain (along the propagation +axis). This assumes the epsilon cross-section remains constant throughout the +entire domain; it is up to the caller to truncate the expansion to any regions +where it is valid.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ E + + cfdfield + +
+

E-field of the mode

+
+
+ required +
+ wavenumber + + complex + +
+

Wavenumber of the mode

+
+
+ required +
+ dxes + + dx_lists_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types

+
+
+ required +
+ axis + + int + +
+

Propagation axis (0=x, 1=y, 2=z)

+
+
+ required +
+ polarity + + int + +
+

Propagation direction (+1 for +ve, -1 for -ve)

+
+
+ required +
+ slices + + Sequence[slice] + +
+

epsilon[tuple(slices)] is used to select the portion of the grid to use + as the waveguide cross-section. slices[axis] should select only one item.

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ cfdfield_t + +
+

E, with the original field expanded along the specified axis.

+
+
+ + +
+ Notes +

This helper assumes that the waveguide cross-section remains constant +along the propagation axis and applies the phase factor

+
\[ +e^{-i \, \mathrm{polarity} \, wavenumber \, \Delta z} +\]
+

to each copied slice.

+
+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ meanas.fdfd.waveguide_cyl + + +

+ +
+ +

Operators and helper functions for cylindrical waveguides with unchanging cross-section.

+

Waveguide operator is derived according to 10.1364/OL.33.001848.

+

As in waveguide_2d, the propagation dependence is separated from the +transverse solve. Here the propagation coordinate is the bend angle \theta, +and the fields are assumed to have the form

+
\[ +\vec{E}(r, y, \theta), \vec{H}(r, y, \theta) \propto e^{-\imath m \theta}, +\]
+

where m is the angular wavenumber returned by solve_mode(s). It is often +convenient to introduce the corresponding linear wavenumber

+
\[ +\beta = \frac{m}{r_{\min}}, +\]
+

so that the cylindrical problem resembles the straight-waveguide problem with +additional metric factors.

+

Those metric factors live on the staggered radial Yee grids. If the left edge of +the computational window is at r = r_{\min}, define the electric-grid and +magnetic-grid radial sample locations by

+
\[ +\begin{aligned} +r_a(n) &= r_{\min} + \sum_{j \le n} \Delta r_{e, j}, \\ +r_b\!\left(n + \tfrac{1}{2}\right) &= r_{\min} + \tfrac{1}{2}\Delta r_{e, n} + + \sum_{j < n} \Delta r_{h, j}, +\end{aligned} +\]
+

and from them the diagonal metric matrices

+
\[ +\begin{aligned} +T_a &= \operatorname{diag}(r_a / r_{\min}), \\ +T_b &= \operatorname{diag}(r_b / r_{\min}). +\end{aligned} +\]
+

With the same forward/backward derivative notation used in waveguide_2d, the +coordinate-transformed discrete curl equations used here are

+
\[ +\begin{aligned} +-\imath \omega \mu_{rr} H_r &= \tilde{\partial}_y E_z + \imath \beta T_a^{-1} E_y, \\ +-\imath \omega \mu_{yy} H_y &= -\imath \beta T_b^{-1} E_r + - T_b^{-1} \tilde{\partial}_r (T_a E_z), \\ +-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_r E_y - \tilde{\partial}_y E_r, \\ +\imath \beta H_y &= -\imath \omega T_b \epsilon_{rr} E_r - T_b \hat{\partial}_y H_z, \\ +\imath \beta H_r &= \imath \omega T_a \epsilon_{yy} E_y + - T_b T_a^{-1} \hat{\partial}_r (T_b H_z), \\ +\imath \omega E_z &= T_a \epsilon_{zz}^{-1} + \left(\hat{\partial}_r H_y - \hat{\partial}_y H_r\right). +\end{aligned} +\]
+

The first three equations are the cylindrical analogue of the straight-guide +relations for H_r, H_y, and H_z. The next two are the metric-weighted +versions of the straight-guide identities for \imath \beta H_y and +\imath \beta H_r, and the last equation plays the same role as the +longitudinal E_z reconstruction in waveguide_2d.

+

Following the same elimination steps as in waveguide_2d, apply +\imath \beta \tilde{\partial}_r and \imath \beta \tilde{\partial}_y to the +equation for E_z, substitute for \imath \beta H_r and \imath \beta H_y, +and then eliminate H_z with

+
\[ +H_z = \frac{1}{-\imath \omega \mu_{zz}} +\left(\tilde{\partial}_r E_y - \tilde{\partial}_y E_r\right). +\]
+

This yields the transverse electric eigenproblem implemented by +cylindrical_operator(...):

+
\[ +\beta^2 +\begin{bmatrix} E_r \\ E_y \end{bmatrix} += +\left( +\omega^2 +\begin{bmatrix} +T_b^2 \mu_{yy} \epsilon_{xx} & 0 \\ +0 & T_a^2 \mu_{xx} \epsilon_{yy} +\end{bmatrix} ++ +\begin{bmatrix} +-T_b \mu_{yy} \hat{\partial}_y \\ + T_a \mu_{xx} \hat{\partial}_x +\end{bmatrix} +T_b \mu_{zz}^{-1} +\begin{bmatrix} +-\tilde{\partial}_y & \tilde{\partial}_x +\end{bmatrix} ++ +\begin{bmatrix} +\tilde{\partial}_x \\ +\tilde{\partial}_y +\end{bmatrix} +T_a \epsilon_{zz}^{-1} +\begin{bmatrix} +\hat{\partial}_x T_b \epsilon_{xx} & +\hat{\partial}_y T_a \epsilon_{yy} +\end{bmatrix} +\right) +\begin{bmatrix} E_r \\ E_y \end{bmatrix}. +\]
+

Since \beta = m / r_{\min}, the solver implemented in this file returns the +angular wavenumber m, while the operator itself is most naturally written in +terms of the linear quantity \beta. The helpers below reconstruct the full +field components from the solved transverse eigenvector and then normalize the +mode to unit forward power with the same discrete longitudinal Poynting inner +product used by waveguide_2d.

+

As in the straight-waveguide case, all functions here assume a 2D grid:

+

dxes = [[[dr_e_0, dr_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]].

+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ cylindrical_operator + + +

+
cylindrical_operator(
+    omega: float,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    rmin: float,
+) -> sparse.sparray
+
+ +
+ +

Cylindrical coordinate waveguide operator of the form

+
\[ + (\omega^2 \begin{bmatrix} T_b T_b \mu_{yy} \epsilon_{xx} & 0 \\ + 0 & T_a T_a \mu_{xx} \epsilon_{yy} \end{bmatrix} + + \begin{bmatrix} -T_b \mu_{yy} \hat{\partial}_y \\ + T_a \mu_{xx} \hat{\partial}_x \end{bmatrix} T_b \mu_{zz}^{-1} + \begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} + + \begin{bmatrix} \tilde{\partial}_x \\ + \tilde{\partial}_y \end{bmatrix} T_a \epsilon_{zz}^{-1} + \begin{bmatrix} \hat{\partial}_x T_b \epsilon_{xx} & \hat{\partial}_y T_a \epsilon_{yy} \end{bmatrix}) + \begin{bmatrix} E_r \\ + E_y \end{bmatrix} +\]
+

for use with a field vector of the form [E_r, E_y].

+

This operator can be used to form an eigenvalue problem of the form + A @ [E_r, E_y] = beta**2 * [E_r, E_y]

+

which can then be solved for the eigenmodes of the system +(an exp(-i * angular_wavenumber * theta) theta-dependence is assumed for +the fields, with beta = angular_wavenumber / rmin).

+

(NOTE: See module docs and 10.1364/OL.33.001848)

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ omega + + float + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ rmin + + float + +
+

Radius at the left edge of the simulation domain (at minimum 'x')

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representation of the operator

+
+
+ + +
+ +
+ +
+ + +

+ solve_modes + + +

+
solve_modes(
+    mode_numbers: Sequence[int],
+    omega: float,
+    dxes: dx_lists2_t,
+    epsilon: vfdslice,
+    rmin: float,
+    mode_margin: int = 2,
+) -> tuple[
+    NDArray[numpy.complex128], NDArray[numpy.complex128]
+]
+
+ +
+ +

Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode + of the bent waveguide with the specified mode number.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ mode_number + + +
+

Number of the mode, 0-indexed

+
+
+ required +
+ omega + + float + +
+

Angular frequency of the simulation

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types. + The first coordinate is assumed to be r, the second is y.

+
+
+ required +
+ epsilon + + vfdslice + +
+

Dielectric constant

+
+
+ required +
+ rmin + + float + +
+

Radius of curvature for the simulation. This should be the minimum value of + r within the simulation domain.

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
Name TypeDescription
e_xys + NDArray[complex128] + +
+

NDArray of vfdfield_t specifying fields. First dimension is mode number.

+
+
angular_wavenumbers + NDArray[complex128] + +
+

list of wavenumbers in 1/rad units.

+
+
+ + +
+ +
+ +
+ + +

+ solve_mode + + +

+
solve_mode(
+    mode_number: int, *args: Any, **kwargs: Any
+) -> tuple[vcfdslice, complex]
+
+ +
+ +

Wrapper around solve_modes() that solves for a single mode.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ mode_number + + int + +
+

0-indexed mode number to solve for

+
+
+ required +
+ *args + + Any + +
+

passed to solve_modes()

+
+
+ () +
+ **kwargs + + Any + +
+

passed to solve_modes()

+
+
+ {} +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple[vcfdslice, complex] + +
+

(e_xy, angular_wavenumber)

+
+
+ + +
+ +
+ +
+ + +

+ linear_wavenumbers + + +

+
linear_wavenumbers(
+    e_xys: list[vcfdfield2_t],
+    angular_wavenumbers: ArrayLike,
+    epsilon: vfdslice,
+    dxes: dx_lists2_t,
+    rmin: float,
+) -> NDArray[numpy.complex128]
+
+ +
+ +

Calculate linear wavenumbers (1/distance) based on angular wavenumbers (1/rad) + and the mode's energy distribution.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ e_xys + + list[vcfdfield2_t] + +
+

Vectorized mode fields with shape (num_modes, 2 * x *y)

+
+
+ required +
+ angular_wavenumbers + + ArrayLike + +
+

Wavenumbers assuming fields have theta-dependence of +exp(-i * angular_wavenumber * theta). They should satisfy +operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid with shape (3, x, y)

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ rmin + + float + +
+

Radius at the left edge of the simulation domain (at minimum 'x')

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ NDArray[complex128] + +
+

NDArray containing the calculated linear (1/distance) wavenumbers

+
+
+ + +
+ +
+ +
+ + +

+ exy2h + + +

+
exy2h(
+    angular_wavenumber: complex,
+    omega: float,
+    dxes: dx_lists2_t,
+    rmin: float,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+) -> sparse.sparray
+
+ +
+ +

Operator which transforms the vector e_xy containing the vectorized E_r and E_y fields, + into a vectorized H containing all three H components

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ angular_wavenumber + + complex + +
+

Wavenumber assuming fields have theta-dependence of +exp(-i * angular_wavenumber * theta). It should satisfy +operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy

+
+
+ required +
+ omega + + float + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ rmin + + float + +
+

Radius at the left edge of the simulation domain (at minimum 'x')

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representing the operator.

+
+
+ + +
+ +
+ +
+ + +

+ exy2e + + +

+
exy2e(
+    angular_wavenumber: complex,
+    omega: float,
+    dxes: dx_lists2_t,
+    rmin: float,
+    epsilon: vfdslice,
+) -> sparse.sparray
+
+ +
+ +

Operator which transforms the vector e_xy containing the vectorized E_r and E_y fields, + into a vectorized E containing all three E components

+

Unlike the straight waveguide case, the H_z components do not cancel and must be calculated +from E_r and E_y in order to then calculate E_z.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ angular_wavenumber + + complex + +
+

Wavenumber assuming fields have theta-dependence of +exp(-i * angular_wavenumber * theta). It should satisfy +operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy

+
+
+ required +
+ omega + + float + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ rmin + + float + +
+

Radius at the left edge of the simulation domain (at minimum 'x')

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representing the operator.

+
+
+ + +
+ +
+ +
+ + +

+ e2h + + +

+
e2h(
+    angular_wavenumber: complex,
+    omega: float,
+    dxes: dx_lists2_t,
+    rmin: float,
+    mu: vfdslice | None = None,
+) -> sparse.sparray
+
+ +
+ +

Returns an operator which, when applied to a vectorized E eigenfield, produces + the vectorized H eigenfield.

+

This operator is created directly from the initial coordinate-transformed equations: +$$ +\begin{aligned} +-\imath \omega \mu_{rr} H_r &= \tilde{\partial}y E_z + \imath \beta T_a^{-1} E_y, \ +-\imath \omega \mu E_r + - T_b^{-1} \tilde{\partial}} H_y &= -\imath \beta T_b^{-1r (T_a E_z), \ +-\imath \omega \mu_y E_r, +\end{aligned} +$$} H_z &= \tilde{\partial}_r E_y - \tilde{\partial

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ angular_wavenumber + + complex + +
+

Wavenumber assuming fields have theta-dependence of +exp(-i * angular_wavenumber * theta). It should satisfy +operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy

+
+
+ required +
+ omega + + float + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ rmin + + float + +
+

Radius at the left edge of the simulation domain (at minimum 'x')

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ sparray + +
+

Sparse matrix representation of the operator.

+
+
+ + +
+ +
+ +
+ + +

+ dxes2T + + +

+
dxes2T(
+    dxes: dx_lists2_t, rmin: float
+) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]
+
+ +
+ +

Construct the cylindrical metric matrices \(T_a\) and \(T_b\).

+

T_a is sampled on the E-grid radial locations, while T_b is sampled on +the staggered H-grid radial locations. These are the diagonal matrices that +convert the straight-waveguide algebra into its cylindrical counterpart.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ rmin + + float + +
+

Radius at the left edge of the simulation domain (at minimum 'x')

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple[NDArray[float64], NDArray[float64]] + +
+

Sparse diagonal matrices (T_a, T_b).

+
+
+ + +
+ +
+ +
+ + +

+ normalized_fields_e + + +

+
normalized_fields_e(
+    e_xy: vcfdfield2,
+    angular_wavenumber: complex,
+    omega: float,
+    dxes: dx_lists2_t,
+    rmin: float,
+    epsilon: vfdslice,
+    mu: vfdslice | None = None,
+    prop_phase: float = 0,
+) -> tuple[vcfdslice_t, vcfdslice_t]
+
+ +
+ +

Given a vector e_xy containing the vectorized E_r and E_y fields, +returns normalized, vectorized E and H fields for the system.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ e_xy + + vcfdfield2 + +
+

Vector containing E_r and E_y fields

+
+
+ required +
+ angular_wavenumber + + complex + +
+

Wavenumber assuming fields have theta-dependence of +exp(-i * angular_wavenumber * theta). It should satisfy +operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy

+
+
+ required +
+ omega + + float + +
+

The angular frequency of the system

+
+
+ required +
+ dxes + + dx_lists2_t + +
+

Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types (2D)

+
+
+ required +
+ rmin + + float + +
+

Radius at the left edge of the simulation domain (at minimum 'x')

+
+
+ required +
+ epsilon + + vfdslice + +
+

Vectorized dielectric constant grid

+
+
+ required +
+ mu + + vfdslice | None + +
+

Vectorized magnetic permeability grid (default 1 everywhere)

+
+
+ None +
+ prop_phase + + float + +
+

Phase shift (dz * corrected_wavenumber) over 1 cell in propagation direction. + Default 0 (continuous propagation direction, i.e. dz->0).

+
+
+ 0 +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ vcfdslice_t + +
+

(e, h), where each field is vectorized, normalized,

+
+
+ vcfdslice_t + +
+

and contains all three vector components.

+
+
+ + +
+ Notes +

The normalization step is delegated to _normalized_fields(...), which +enforces unit forward power under the discrete inner product

+
\[ +\frac{1}{2}\int (E_r H_y^* - E_y H_r^*) \, dr \, dy. +\]
+

The angular wavenumber m is first converted into the full three-component +fields, then the overall complex phase and sign are fixed so the result is +reproducible for symmetric modes.

+
+ +
+ +
+ + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/_mkdocstrings.css b/assets/_mkdocstrings.css new file mode 100644 index 0000000..854048c --- /dev/null +++ b/assets/_mkdocstrings.css @@ -0,0 +1,237 @@ + +/* Avoid breaking parameter names, etc. in table cells. */ +.doc-contents td code { + word-break: normal !important; +} + +/* No line break before first paragraph of descriptions. */ +.doc-md-description, +.doc-md-description>p:first-child { + display: inline; +} + +/* No text transformation from Material for MkDocs for H5 headings. */ +.md-typeset h5 .doc-object-name { + text-transform: none; +} + +/* Max width for docstring sections tables. */ +.doc .md-typeset__table, +.doc .md-typeset__table table { + display: table !important; + width: 100%; +} + +.doc .md-typeset__table tr { + display: table-row; +} + +/* Defaults in Spacy table style. */ +.doc-param-default, +.doc-type_param-default { + float: right; +} + +/* Parameter headings must be inline, not blocks. */ +.doc-heading-parameter, +.doc-heading-type_parameter { + display: inline; +} + +/* Default font size for parameter headings. */ +.md-typeset .doc-heading-parameter { + font-size: inherit; +} + +/* Prefer space on the right, not the left of parameter permalinks. */ +.doc-heading-parameter .headerlink, +.doc-heading-type_parameter .headerlink { + margin-left: 0 !important; + margin-right: 0.2rem; +} + +/* Backward-compatibility: docstring section titles in bold. */ +.doc-section-title { + font-weight: bold; +} + +/* Backlinks crumb separator. */ +.doc-backlink-crumb { + display: inline-flex; + gap: .2rem; + white-space: nowrap; + align-items: center; + vertical-align: middle; +} +.doc-backlink-crumb:not(:first-child)::before { + background-color: var(--md-default-fg-color--lighter); + content: ""; + display: inline; + height: 1rem; + --md-path-icon: url('data:image/svg+xml;charset=utf-8,'); + -webkit-mask-image: var(--md-path-icon); + mask-image: var(--md-path-icon); + width: 1rem; +} +.doc-backlink-crumb.last { + font-weight: bold; +} + +/* Symbols in Navigation and ToC. */ +:root, :host, +[data-md-color-scheme="default"] { + --doc-symbol-parameter-fg-color: #df50af; + --doc-symbol-type_parameter-fg-color: #df50af; + --doc-symbol-attribute-fg-color: #953800; + --doc-symbol-function-fg-color: #8250df; + --doc-symbol-method-fg-color: #8250df; + --doc-symbol-class-fg-color: #0550ae; + --doc-symbol-type_alias-fg-color: #0550ae; + --doc-symbol-module-fg-color: #5cad0f; + + --doc-symbol-parameter-bg-color: #df50af1a; + --doc-symbol-type_parameter-bg-color: #df50af1a; + --doc-symbol-attribute-bg-color: #9538001a; + --doc-symbol-function-bg-color: #8250df1a; + --doc-symbol-method-bg-color: #8250df1a; + --doc-symbol-class-bg-color: #0550ae1a; + --doc-symbol-type_alias-bg-color: #0550ae1a; + --doc-symbol-module-bg-color: #5cad0f1a; +} + +[data-md-color-scheme="slate"] { + --doc-symbol-parameter-fg-color: #ffa8cc; + --doc-symbol-type_parameter-fg-color: #ffa8cc; + --doc-symbol-attribute-fg-color: #ffa657; + --doc-symbol-function-fg-color: #d2a8ff; + --doc-symbol-method-fg-color: #d2a8ff; + --doc-symbol-class-fg-color: #79c0ff; + --doc-symbol-type_alias-fg-color: #79c0ff; + --doc-symbol-module-fg-color: #baff79; + + --doc-symbol-parameter-bg-color: #ffa8cc1a; + --doc-symbol-type_parameter-bg-color: #ffa8cc1a; + --doc-symbol-attribute-bg-color: #ffa6571a; + --doc-symbol-function-bg-color: #d2a8ff1a; + --doc-symbol-method-bg-color: #d2a8ff1a; + --doc-symbol-class-bg-color: #79c0ff1a; + --doc-symbol-type_alias-bg-color: #79c0ff1a; + --doc-symbol-module-bg-color: #baff791a; +} + +code.doc-symbol { + border-radius: .1rem; + font-size: .85em; + padding: 0 .3em; + font-weight: bold; +} + +code.doc-symbol-parameter, +a code.doc-symbol-parameter { + color: var(--doc-symbol-parameter-fg-color); + background-color: var(--doc-symbol-parameter-bg-color); +} + +code.doc-symbol-parameter::after { + content: "param"; +} + +code.doc-symbol-type_parameter, +a code.doc-symbol-type_parameter { + color: var(--doc-symbol-type_parameter-fg-color); + background-color: var(--doc-symbol-type_parameter-bg-color); +} + +code.doc-symbol-type_parameter::after { + content: "type-param"; +} + +code.doc-symbol-attribute, +a code.doc-symbol-attribute { + color: var(--doc-symbol-attribute-fg-color); + background-color: var(--doc-symbol-attribute-bg-color); +} + +code.doc-symbol-attribute::after { + content: "attr"; +} + +code.doc-symbol-function, +a code.doc-symbol-function { + color: var(--doc-symbol-function-fg-color); + background-color: var(--doc-symbol-function-bg-color); +} + +code.doc-symbol-function::after { + content: "func"; +} + +code.doc-symbol-method, +a code.doc-symbol-method { + color: var(--doc-symbol-method-fg-color); + background-color: var(--doc-symbol-method-bg-color); +} + +code.doc-symbol-method::after { + content: "meth"; +} + +code.doc-symbol-class, +a code.doc-symbol-class { + color: var(--doc-symbol-class-fg-color); + background-color: var(--doc-symbol-class-bg-color); +} + +code.doc-symbol-class::after { + content: "class"; +} + + +code.doc-symbol-type_alias, +a code.doc-symbol-type_alias { + color: var(--doc-symbol-type_alias-fg-color); + background-color: var(--doc-symbol-type_alias-bg-color); +} + +code.doc-symbol-type_alias::after { + content: "type"; +} + +code.doc-symbol-module, +a code.doc-symbol-module { + color: var(--doc-symbol-module-fg-color); + background-color: var(--doc-symbol-module-bg-color); +} + +code.doc-symbol-module::after { + content: "mod"; +} + +.doc-signature .autorefs { + color: inherit; + border-bottom: 1px dotted currentcolor; +} + +/* Source code blocks (admonitions). */ +:root { + --md-admonition-icon--mkdocstrings-source: url('data:image/svg+xml;charset=utf-8,') +} +.md-typeset .admonition.mkdocstrings-source, +.md-typeset details.mkdocstrings-source { + border: none; + padding: 0; +} +.md-typeset .admonition.mkdocstrings-source:focus-within, +.md-typeset details.mkdocstrings-source:focus-within { + box-shadow: none; +} +.md-typeset .mkdocstrings-source > .admonition-title, +.md-typeset .mkdocstrings-source > summary { + background-color: inherit; +} +.md-typeset .mkdocstrings-source > .admonition-title::before, +.md-typeset .mkdocstrings-source > summary::before { + background-color: var(--md-default-fg-color); + -webkit-mask-image: var(--md-admonition-icon--mkdocstrings-source); + mask-image: var(--md-admonition-icon--mkdocstrings-source); +} diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf13b9f9d978896599290a74f77d5dbe7d1655c GIT binary patch literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ literal 0 HcmV?d00001 diff --git a/assets/javascripts/bundle.79ae519e.min.js b/assets/javascripts/bundle.79ae519e.min.js new file mode 100644 index 0000000..3df3e5e --- /dev/null +++ b/assets/javascripts/bundle.79ae519e.min.js @@ -0,0 +1,16 @@ +"use strict";(()=>{var Zi=Object.create;var _r=Object.defineProperty;var ea=Object.getOwnPropertyDescriptor;var ta=Object.getOwnPropertyNames,Bt=Object.getOwnPropertySymbols,ra=Object.getPrototypeOf,Ar=Object.prototype.hasOwnProperty,bo=Object.prototype.propertyIsEnumerable;var ho=(e,t,r)=>t in e?_r(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))Ar.call(t,r)&&ho(e,r,t[r]);if(Bt)for(var r of Bt(t))bo.call(t,r)&&ho(e,r,t[r]);return e};var vo=(e,t)=>{var r={};for(var o in e)Ar.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Bt)for(var o of Bt(e))t.indexOf(o)<0&&bo.call(e,o)&&(r[o]=e[o]);return r};var Cr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var oa=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of ta(t))!Ar.call(e,n)&&n!==r&&_r(e,n,{get:()=>t[n],enumerable:!(o=ea(t,n))||o.enumerable});return e};var $t=(e,t,r)=>(r=e!=null?Zi(ra(e)):{},oa(t||!e||!e.__esModule?_r(r,"default",{value:e,enumerable:!0}):r,e));var go=(e,t,r)=>new Promise((o,n)=>{var i=c=>{try{a(r.next(c))}catch(p){n(p)}},s=c=>{try{a(r.throw(c))}catch(p){n(p)}},a=c=>c.done?o(c.value):Promise.resolve(c.value).then(i,s);a((r=r.apply(e,t)).next())});var xo=Cr((kr,yo)=>{(function(e,t){typeof kr=="object"&&typeof yo!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(kr,(function(){"use strict";function e(r){var o=!0,n=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(k){return!!(k&&k!==document&&k.nodeName!=="HTML"&&k.nodeName!=="BODY"&&"classList"in k&&"contains"in k.classList)}function c(k){var ut=k.type,je=k.tagName;return!!(je==="INPUT"&&s[ut]&&!k.readOnly||je==="TEXTAREA"&&!k.readOnly||k.isContentEditable)}function p(k){k.classList.contains("focus-visible")||(k.classList.add("focus-visible"),k.setAttribute("data-focus-visible-added",""))}function l(k){k.hasAttribute("data-focus-visible-added")&&(k.classList.remove("focus-visible"),k.removeAttribute("data-focus-visible-added"))}function f(k){k.metaKey||k.altKey||k.ctrlKey||(a(r.activeElement)&&p(r.activeElement),o=!0)}function u(k){o=!1}function d(k){a(k.target)&&(o||c(k.target))&&p(k.target)}function v(k){a(k.target)&&(k.target.classList.contains("focus-visible")||k.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(k.target))}function S(k){document.visibilityState==="hidden"&&(n&&(o=!0),X())}function X(){document.addEventListener("mousemove",ee),document.addEventListener("mousedown",ee),document.addEventListener("mouseup",ee),document.addEventListener("pointermove",ee),document.addEventListener("pointerdown",ee),document.addEventListener("pointerup",ee),document.addEventListener("touchmove",ee),document.addEventListener("touchstart",ee),document.addEventListener("touchend",ee)}function re(){document.removeEventListener("mousemove",ee),document.removeEventListener("mousedown",ee),document.removeEventListener("mouseup",ee),document.removeEventListener("pointermove",ee),document.removeEventListener("pointerdown",ee),document.removeEventListener("pointerup",ee),document.removeEventListener("touchmove",ee),document.removeEventListener("touchstart",ee),document.removeEventListener("touchend",ee)}function ee(k){k.target.nodeName&&k.target.nodeName.toLowerCase()==="html"||(o=!1,re())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",S,!0),X(),r.addEventListener("focus",d,!0),r.addEventListener("blur",v,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var ro=Cr((jy,Rn)=>{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var qa=/["'&<>]/;Rn.exports=Ka;function Ka(e){var t=""+e,r=qa.exec(t);if(!r)return t;var o,n="",i=0,s=0;for(i=r.index;i{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Nt=="object"&&typeof io=="object"?io.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Nt=="object"?Nt.ClipboardJS=r():t.ClipboardJS=r()})(Nt,function(){return(function(){var e={686:(function(o,n,i){"use strict";i.d(n,{default:function(){return Xi}});var s=i(279),a=i.n(s),c=i(370),p=i.n(c),l=i(817),f=i.n(l);function u(q){try{return document.execCommand(q)}catch(C){return!1}}var d=function(C){var _=f()(C);return u("cut"),_},v=d;function S(q){var C=document.documentElement.getAttribute("dir")==="rtl",_=document.createElement("textarea");_.style.fontSize="12pt",_.style.border="0",_.style.padding="0",_.style.margin="0",_.style.position="absolute",_.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return _.style.top="".concat(D,"px"),_.setAttribute("readonly",""),_.value=q,_}var X=function(C,_){var D=S(C);_.container.appendChild(D);var N=f()(D);return u("copy"),D.remove(),N},re=function(C){var _=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=X(C,_):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=X(C.value,_):(D=f()(C),u("copy")),D},ee=re;function k(q){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?k=function(_){return typeof _}:k=function(_){return _&&typeof Symbol=="function"&&_.constructor===Symbol&&_!==Symbol.prototype?"symbol":typeof _},k(q)}var ut=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},_=C.action,D=_===void 0?"copy":_,N=C.container,G=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(G!==void 0)if(G&&k(G)==="object"&&G.nodeType===1){if(D==="copy"&&G.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(G.hasAttribute("readonly")||G.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return ee(We,{container:N});if(G)return D==="cut"?v(G):ee(G,{container:N})},je=ut;function R(q){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?R=function(_){return typeof _}:R=function(_){return _&&typeof Symbol=="function"&&_.constructor===Symbol&&_!==Symbol.prototype?"symbol":typeof _},R(q)}function se(q,C){if(!(q instanceof C))throw new TypeError("Cannot call a class as a function")}function ce(q,C){for(var _=0;_0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof N.action=="function"?N.action:this.defaultAction,this.target=typeof N.target=="function"?N.target:this.defaultTarget,this.text=typeof N.text=="function"?N.text:this.defaultText,this.container=R(N.container)==="object"?N.container:document.body}},{key:"listenClick",value:function(N){var G=this;this.listener=p()(N,"click",function(We){return G.onClick(We)})}},{key:"onClick",value:function(N){var G=N.delegateTarget||N.currentTarget,We=this.action(G)||"copy",Yt=je({action:We,container:this.container,target:this.target(G),text:this.text(G)});this.emit(Yt?"success":"error",{action:We,text:Yt,trigger:G,clearSelection:function(){G&&G.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(N){return Mr("action",N)}},{key:"defaultTarget",value:function(N){var G=Mr("target",N);if(G)return document.querySelector(G)}},{key:"defaultText",value:function(N){return Mr("text",N)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(N){var G=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return ee(N,G)}},{key:"cut",value:function(N){return v(N)}},{key:"isSupported",value:function(){var N=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],G=typeof N=="string"?[N]:N,We=!!document.queryCommandSupported;return G.forEach(function(Yt){We=We&&!!document.queryCommandSupported(Yt)}),We}}]),_})(a()),Xi=Ji}),828:(function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,c){for(;a&&a.nodeType!==n;){if(typeof a.matches=="function"&&a.matches(c))return a;a=a.parentNode}}o.exports=s}),438:(function(o,n,i){var s=i(828);function a(l,f,u,d,v){var S=p.apply(this,arguments);return l.addEventListener(u,S,v),{destroy:function(){l.removeEventListener(u,S,v)}}}function c(l,f,u,d,v){return typeof l.addEventListener=="function"?a.apply(null,arguments):typeof u=="function"?a.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(S){return a(S,f,u,d,v)}))}function p(l,f,u,d){return function(v){v.delegateTarget=s(v.target,f),v.delegateTarget&&d.call(l,v)}}o.exports=c}),879:(function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}}),370:(function(o,n,i){var s=i(879),a=i(438);function c(u,d,v){if(!u&&!d&&!v)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(v))throw new TypeError("Third argument must be a Function");if(s.node(u))return p(u,d,v);if(s.nodeList(u))return l(u,d,v);if(s.string(u))return f(u,d,v);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function p(u,d,v){return u.addEventListener(d,v),{destroy:function(){u.removeEventListener(d,v)}}}function l(u,d,v){return Array.prototype.forEach.call(u,function(S){S.addEventListener(d,v)}),{destroy:function(){Array.prototype.forEach.call(u,function(S){S.removeEventListener(d,v)})}}}function f(u,d,v){return a(document.body,u,d,v)}o.exports=c}),817:(function(o){function n(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),p=document.createRange();p.selectNodeContents(i),c.removeAllRanges(),c.addRange(p),s=c.toString()}return s}o.exports=n}),279:(function(o){function n(){}n.prototype={on:function(i,s,a){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var c=this;function p(){c.off(i,p),s.apply(a,arguments)}return p._=s,this.on(i,p,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),c=0,p=a.length;for(c;c0&&i[i.length-1])&&(p[0]===6||p[0]===2)){r=0;continue}if(p[0]===3&&(!i||p[1]>i[0]&&p[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function K(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],s;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(a){s={error:a}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(s)throw s.error}}return i}function B(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||c(d,S)})},v&&(n[d]=v(n[d])))}function c(d,v){try{p(o[d](v))}catch(S){u(i[0][3],S)}}function p(d){d.value instanceof dt?Promise.resolve(d.value.v).then(l,f):u(i[0][2],d)}function l(d){c("next",d)}function f(d){c("throw",d)}function u(d,v){d(v),i.shift(),i.length&&c(i[0][0],i[0][1])}}function To(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Oe=="function"?Oe(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(s){return new Promise(function(a,c){s=e[i](s),n(a,c,s.done,s.value)})}}function n(i,s,a,c){Promise.resolve(c).then(function(p){i({value:p,done:a})},s)}}function I(e){return typeof e=="function"}function yt(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Jt=yt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ze(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var qe=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Oe(s),c=a.next();!c.done;c=a.next()){var p=c.value;p.remove(this)}}catch(S){t={error:S}}finally{try{c&&!c.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var l=this.initialTeardown;if(I(l))try{l()}catch(S){i=S instanceof Jt?S.errors:[S]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=Oe(f),d=u.next();!d.done;d=u.next()){var v=d.value;try{So(v)}catch(S){i=i!=null?i:[],S instanceof Jt?i=B(B([],K(i)),K(S.errors)):i.push(S)}}}catch(S){o={error:S}}finally{try{d&&!d.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new Jt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)So(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ze(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ze(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var $r=qe.EMPTY;function Xt(e){return e instanceof qe||e&&"closed"in e&&I(e.remove)&&I(e.add)&&I(e.unsubscribe)}function So(e){I(e)?e():e.unsubscribe()}var De={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var xt={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,s=n.isStopped,a=n.observers;return i||s?$r:(this.currentObservers=null,a.push(r),new qe(function(){o.currentObservers=null,Ze(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,s=o.isStopped;n?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,o){return new Ho(r,o)},t})(F);var Ho=(function(e){ie(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:$r},t})(T);var jr=(function(e){ie(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(T);var Rt={now:function(){return(Rt.delegate||Date).now()},delegate:void 0};var It=(function(e){ie(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=Rt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,s=o._infiniteTimeWindow,a=o._timestampProvider,c=o._windowTime;n||(i.push(r),!s&&i.push(a.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,s=n._buffer,a=s.slice(),c=0;c0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t})(St);var Ro=(function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(Ot);var Dr=new Ro(Po);var Io=(function(e){ie(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=Tt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var s=r.actions;o!=null&&o===r._scheduled&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==o&&(Tt.cancelAnimationFrame(o),r._scheduled=void 0)},t})(St);var Fo=(function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o;r?o=r.id:(o=this._scheduled,this._scheduled=void 0);var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t})(Ot);var ye=new Fo(Io);var y=new F(function(e){return e.complete()});function tr(e){return e&&I(e.schedule)}function Vr(e){return e[e.length-1]}function pt(e){return I(Vr(e))?e.pop():void 0}function Fe(e){return tr(Vr(e))?e.pop():void 0}function rr(e,t){return typeof Vr(e)=="number"?e.pop():t}var Lt=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function or(e){return I(e==null?void 0:e.then)}function nr(e){return I(e[wt])}function ir(e){return Symbol.asyncIterator&&I(e==null?void 0:e[Symbol.asyncIterator])}function ar(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function fa(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var sr=fa();function cr(e){return I(e==null?void 0:e[sr])}function pr(e){return wo(this,arguments,function(){var r,o,n,i;return Gt(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,dt(r.read())];case 3:return o=s.sent(),n=o.value,i=o.done,i?[4,dt(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,dt(n)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function lr(e){return I(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(nr(e))return ua(e);if(Lt(e))return da(e);if(or(e))return ha(e);if(ir(e))return jo(e);if(cr(e))return ba(e);if(lr(e))return va(e)}throw ar(e)}function ua(e){return new F(function(t){var r=e[wt]();if(I(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function da(e){return new F(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?g(function(n,i){return e(n,i,o)}):be,Ee(1),r?Qe(t):tn(function(){return new fr}))}}function Yr(e){return e<=0?function(){return y}:E(function(t,r){var o=[];t.subscribe(w(r,function(n){o.push(n),e=2,!0))}function le(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new T}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,c=a===void 0?!0:a;return function(p){var l,f,u,d=0,v=!1,S=!1,X=function(){f==null||f.unsubscribe(),f=void 0},re=function(){X(),l=u=void 0,v=S=!1},ee=function(){var k=l;re(),k==null||k.unsubscribe()};return E(function(k,ut){d++,!S&&!v&&X();var je=u=u!=null?u:r();ut.add(function(){d--,d===0&&!S&&!v&&(f=Br(ee,c))}),je.subscribe(ut),!l&&d>0&&(l=new bt({next:function(R){return je.next(R)},error:function(R){S=!0,X(),f=Br(re,n,R),je.error(R)},complete:function(){v=!0,X(),f=Br(re,s),je.complete()}}),U(k).subscribe(l))})(p)}}function Br(e,t){for(var r=[],o=2;oe.next(document)),e}function M(e,t=document){return Array.from(t.querySelectorAll(e))}function j(e,t=document){let r=ue(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ue(e,t=document){return t.querySelector(e)||void 0}function Ne(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var Ra=L(h(document.body,"focusin"),h(document.body,"focusout")).pipe(Ae(1),Q(void 0),m(()=>Ne()||document.body),Z(1));function Ye(e){return Ra.pipe(m(t=>e.contains(t)),Y())}function it(e,t){return H(()=>L(h(e,"mouseenter").pipe(m(()=>!0)),h(e,"mouseleave").pipe(m(()=>!1))).pipe(t?jt(r=>He(+!r*t)):be,Q(e.matches(":hover"))))}function sn(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)sn(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)sn(o,n);return o}function br(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function _t(e){let t=x("script",{src:e});return H(()=>(document.head.appendChild(t),L(h(t,"load"),h(t,"error").pipe(b(()=>Nr(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),A(()=>document.head.removeChild(t)),Ee(1))))}var cn=new T,Ia=H(()=>typeof ResizeObserver=="undefined"?_t("https://unpkg.com/resize-observer-polyfill"):$(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>cn.next(t)))),b(e=>L(tt,$(e)).pipe(A(()=>e.disconnect()))),Z(1));function de(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Le(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return Ia.pipe(O(r=>r.observe(t)),b(r=>cn.pipe(g(o=>o.target===t),A(()=>r.unobserve(t)))),m(()=>de(e)),Q(de(e)))}function At(e){return{width:e.scrollWidth,height:e.scrollHeight}}function vr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function pn(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Be(e){return{x:e.offsetLeft,y:e.offsetTop}}function ln(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function mn(e){return L(h(window,"load"),h(window,"resize")).pipe($e(0,ye),m(()=>Be(e)),Q(Be(e)))}function gr(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ge(e){return L(h(e,"scroll"),h(window,"scroll"),h(window,"resize")).pipe($e(0,ye),m(()=>gr(e)),Q(gr(e)))}var fn=new T,Fa=H(()=>$(new IntersectionObserver(e=>{for(let t of e)fn.next(t)},{threshold:0}))).pipe(b(e=>L(tt,$(e)).pipe(A(()=>e.disconnect()))),Z(1));function mt(e){return Fa.pipe(O(t=>t.observe(e)),b(t=>fn.pipe(g(({target:r})=>r===e),A(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function un(e,t=16){return Ge(e).pipe(m(({y:r})=>{let o=de(e),n=At(e);return r>=n.height-o.height-t}),Y())}var yr={drawer:j("[data-md-toggle=drawer]"),search:j("[data-md-toggle=search]")};function dn(e){return yr[e].checked}function at(e,t){yr[e].checked!==t&&yr[e].click()}function Je(e){let t=yr[e];return h(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function ja(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ua(){return L(h(window,"compositionstart").pipe(m(()=>!0)),h(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function hn(){let e=h(window,"keydown").pipe(g(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:dn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),g(({mode:t,type:r})=>{if(t==="global"){let o=Ne();if(typeof o!="undefined")return!ja(o,r)}return!0}),le());return Ua().pipe(b(t=>t?y:e))}function we(){return new URL(location.href)}function st(e,t=!1){if(V("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function bn(){return new T}function vn(){return location.hash.slice(1)}function gn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Zr(e){return L(h(window,"hashchange"),e).pipe(m(vn),Q(vn()),g(t=>t.length>0),Z(1))}function yn(e){return Zr(e).pipe(m(t=>ue(`[id="${t}"]`)),g(t=>typeof t!="undefined"))}function Wt(e){let t=matchMedia(e);return ur(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function xn(){let e=matchMedia("print");return L(h(window,"beforeprint").pipe(m(()=>!0)),h(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function eo(e,t){return e.pipe(b(r=>r?t():y))}function to(e,t){return new F(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let s=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+s*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function ze(e,t){return to(e,t).pipe(b(r=>r.text()),m(r=>JSON.parse(r)),Z(1))}function xr(e,t){let r=new DOMParser;return to(e,t).pipe(b(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),Z(1))}function En(e,t){let r=new DOMParser;return to(e,t).pipe(b(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),Z(1))}function wn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function Tn(){return L(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(m(wn),Q(wn()))}function Sn(){return{width:innerWidth,height:innerHeight}}function On(){return h(window,"resize",{passive:!0}).pipe(m(Sn),Q(Sn()))}function Ln(){return z([Tn(),On()]).pipe(m(([e,t])=>({offset:e,size:t})),Z(1))}function Er(e,{viewport$:t,header$:r}){let o=t.pipe(ne("size")),n=z([o,r]).pipe(m(()=>Be(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:s,size:a},{x:c,y:p}])=>({offset:{x:s.x-c,y:s.y-p+i},size:a})))}function Wa(e){return h(e,"message",t=>t.data)}function Da(e){let t=new T;return t.subscribe(r=>e.postMessage(r)),t}function Mn(e,t=new Worker(e)){let r=Wa(t),o=Da(t),n=new T;n.subscribe(o);let i=o.pipe(oe(),ae(!0));return n.pipe(oe(),Ve(r.pipe(W(i))),le())}var Va=j("#__config"),Ct=JSON.parse(Va.textContent);Ct.base=`${new URL(Ct.base,we())}`;function Te(){return Ct}function V(e){return Ct.features.includes(e)}function Me(e,t){return typeof t!="undefined"?Ct.translations[e].replace("#",t.toString()):Ct.translations[e]}function Ce(e,t=document){return j(`[data-md-component=${e}]`,t)}function me(e,t=document){return M(`[data-md-component=${e}]`,t)}function Na(e){let t=j(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(m(()=>j(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function _n(e){if(!V("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=j(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return H(()=>{let t=new T;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Na(e).pipe(O(r=>t.next(r)),A(()=>t.complete()),m(r=>P({ref:e},r)))})}function za(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function An(e,t){let r=new T;return r.subscribe(({hidden:o})=>{e.hidden=o}),za(e,t).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))}function Dt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function wr(...e){return x("div",{class:"md-tooltip2",role:"dialog"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function Cn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function kn(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Dt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Dt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function Hn(e){return x("button",{class:"md-code__button",title:Me("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function $n(){return x("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function Pn(){return x("nav",{class:"md-code__nav"})}var In=$t(ro());function oo(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(c=>!e.terms[c]).reduce((c,p)=>[...c,x("del",null,(0,In.default)(p))," "],[]).slice(0,-1),i=Te(),s=new URL(e.location,i.base);V("search.highlight")&&s.searchParams.set("h",Object.entries(e.terms).filter(([,c])=>c).reduce((c,[p])=>`${c} ${p}`.trim(),""));let{tags:a}=Te();return x("a",{href:`${s}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&x("nav",{class:"md-tags"},e.tags.map(c=>{let p=a?c in a?`md-tag-icon md-tag--${a[c]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${p}`},c)})),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Me("search.result.term.missing"),": ",...n)))}function Fn(e){let t=e[0].score,r=[...e],o=Te(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),s=r.findIndex(l=>l.scoreoo(l,1)),...c.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,c.length>0&&c.length===1?Me("search.result.more.one"):Me("search.result.more.other",c.length))),...c.map(l=>oo(l,1)))]:[]];return x("li",{class:"md-search-result__item"},p)}function jn(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?br(r):r)))}function no(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function Un(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Qa(e){var o;let t=Te(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Wn(e,t){var o;let r=Te();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Me("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Qa)))}var Ya=0;function Ba(e,t=250){let r=z([Ye(e),it(e,t)]).pipe(m(([n,i])=>n||i),Y()),o=H(()=>pn(e)).pipe(J(Ge),gt(1),Pe(r),m(()=>ln(e)));return r.pipe(Re(n=>n),b(()=>z([r,o])),m(([n,i])=>({active:n,offset:i})),le())}function Vt(e,t,r=250){let{content$:o,viewport$:n}=t,i=`__tooltip2_${Ya++}`;return H(()=>{let s=new T,a=new jr(!1);s.pipe(oe(),ae(!1)).subscribe(a);let c=a.pipe(jt(l=>He(+!l*250,Dr)),Y(),b(l=>l?o:y),O(l=>l.id=i),le());z([s.pipe(m(({active:l})=>l)),c.pipe(b(l=>it(l,250)),Q(!1))]).pipe(m(l=>l.some(f=>f))).subscribe(a);let p=a.pipe(g(l=>l),te(c,n),m(([l,f,{size:u}])=>{let d=e.getBoundingClientRect(),v=d.width/2;if(f.role==="tooltip")return{x:v,y:8+d.height};if(d.y>=u.height/2){let{height:S}=de(f);return{x:v,y:-16-S}}else return{x:v,y:16+d.height}}));return z([c,s,p]).subscribe(([l,{offset:f},u])=>{l.style.setProperty("--md-tooltip-host-x",`${f.x}px`),l.style.setProperty("--md-tooltip-host-y",`${f.y}px`),l.style.setProperty("--md-tooltip-x",`${u.x}px`),l.style.setProperty("--md-tooltip-y",`${u.y}px`),l.classList.toggle("md-tooltip2--top",u.y<0),l.classList.toggle("md-tooltip2--bottom",u.y>=0)}),a.pipe(g(l=>l),te(c,(l,f)=>f),g(l=>l.role==="tooltip")).subscribe(l=>{let f=de(j(":scope > *",l));l.style.setProperty("--md-tooltip-width",`${f.width}px`),l.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(Y(),xe(ye),te(c)).subscribe(([l,f])=>{f.classList.toggle("md-tooltip2--active",l)}),z([a.pipe(g(l=>l)),c]).subscribe(([l,f])=>{f.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),a.pipe(g(l=>!l)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),Ba(e,r).pipe(O(l=>s.next(l)),A(()=>s.complete()),m(l=>P({ref:e},l)))})}function Xe(e,{viewport$:t},r=document.body){return Vt(e,{content$:new F(o=>{let n=e.title,i=Cn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t},0)}function Ga(e,t){let r=H(()=>z([mn(e),Ge(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:s,height:a}=de(e);return{x:o-i.x+s/2,y:n-i.y+a/2}}));return Ye(e).pipe(b(o=>r.pipe(m(n=>({active:o,offset:n})),Ee(+!o||1/0))))}function Dn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return H(()=>{let i=new T,s=i.pipe(oe(),ae(!0));return i.subscribe({next({offset:a}){e.style.setProperty("--md-tooltip-x",`${a.x}px`),e.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),mt(e).pipe(W(s)).subscribe(a=>{e.toggleAttribute("data-md-visible",a)}),L(i.pipe(g(({active:a})=>a)),i.pipe(Ae(250),g(({active:a})=>!a))).subscribe({next({active:a}){a?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe($e(16,ye)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(gt(125,ye),g(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?e.style.setProperty("--md-tooltip-0",`${-a}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),h(n,"click").pipe(W(s),g(a=>!(a.metaKey||a.ctrlKey))).subscribe(a=>{a.stopPropagation(),a.preventDefault()}),h(n,"mousedown").pipe(W(s),te(i)).subscribe(([a,{active:c}])=>{var p;if(a.button!==0||a.metaKey||a.ctrlKey)a.preventDefault();else if(c){a.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(p=Ne())==null||p.blur()}}),r.pipe(W(s),g(a=>a===o),nt(125)).subscribe(()=>e.focus()),Ga(e,t).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))})}function Ja(e){let t=Te();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate&&typeof t.annotate=="object"){let o=e.closest("[class|=language]");if(o)for(let n of Array.from(o.classList)){if(!n.startsWith("language-"))continue;let[,i]=n.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return M(r.join(", "),e)}function Xa(e){let t=[];for(let r of Ja(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let s;for(;s=/(\(\d+\))(!)?/.exec(i.textContent);){let[,a,c]=s;if(typeof c=="undefined"){let p=i.splitText(s.index);i=p.splitText(a.length),t.push(p)}else{i.textContent=a,t.push(i);break}}}}return t}function Vn(e,t){t.append(...Array.from(e.childNodes))}function Tr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,s=new Map;for(let a of Xa(t)){let[,c]=a.textContent.match(/\((\d+)\)/);ue(`:scope > li:nth-child(${c})`,e)&&(s.set(c,kn(c,i)),a.replaceWith(s.get(c)))}return s.size===0?y:H(()=>{let a=new T,c=a.pipe(oe(),ae(!0)),p=[];for(let[l,f]of s)p.push([j(".md-typeset",f),j(`:scope > li:nth-child(${l})`,e)]);return o.pipe(W(c)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of p)l?Vn(f,u):Vn(u,f)}),L(...[...s].map(([,l])=>Dn(l,t,{target$:r}))).pipe(A(()=>a.complete()),le())})}function Nn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Nn(t)}}function zn(e,t){return H(()=>{let r=Nn(e);return typeof r!="undefined"?Tr(r,e,t):y})}var Kn=$t(ao());var Za=0,qn=L(h(window,"keydown").pipe(m(()=>!0)),L(h(window,"keyup"),h(window,"contextmenu")).pipe(m(()=>!1))).pipe(Q(!1),Z(1));function Qn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Qn(t)}}function es(e){return Le(e).pipe(m(({width:t})=>({scrollable:At(e).width>t})),ne("scrollable"))}function Yn(e,t){let{matches:r}=matchMedia("(hover)"),o=H(()=>{let n=new T,i=n.pipe(Yr(1));n.subscribe(({scrollable:d})=>{d&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let s=[],a=e.closest("pre"),c=a.closest("[id]"),p=c?c.id:Za++;a.id=`__code_${p}`;let l=[],f=e.closest(".highlight");if(f instanceof HTMLElement){let d=Qn(f);if(typeof d!="undefined"&&(f.classList.contains("annotate")||V("content.code.annotate"))){let v=Tr(d,e,t);l.push(Le(f).pipe(W(i),m(({width:S,height:X})=>S&&X),Y(),b(S=>S?v:y)))}}let u=M(":scope > span[id]",e);if(u.length&&(e.classList.add("md-code__content"),e.closest(".select")||V("content.code.select")&&!e.closest(".no-select"))){let d=+u[0].id.split("-").pop(),v=$n();s.push(v),V("content.tooltips")&&l.push(Xe(v,{viewport$}));let S=h(v,"click").pipe(Ut(R=>!R,!1),O(()=>v.blur()),le());S.subscribe(R=>{v.classList.toggle("md-code__button--active",R)});let X=fe(u).pipe(J(R=>it(R).pipe(m(se=>[R,se]))));S.pipe(b(R=>R?X:y)).subscribe(([R,se])=>{let ce=ue(".hll.select",R);if(ce&&!se)ce.replaceWith(...Array.from(ce.childNodes));else if(!ce&&se){let he=document.createElement("span");he.className="hll select",he.append(...Array.from(R.childNodes).slice(1)),R.append(he)}});let re=fe(u).pipe(J(R=>h(R,"mousedown").pipe(O(se=>se.preventDefault()),m(()=>R)))),ee=S.pipe(b(R=>R?re:y),te(qn),m(([R,se])=>{var he;let ce=u.indexOf(R)+d;if(se===!1)return[ce,ce];{let Se=M(".hll",e).map(Ue=>u.indexOf(Ue.parentElement)+d);return(he=window.getSelection())==null||he.removeAllRanges(),[Math.min(ce,...Se),Math.max(ce,...Se)]}})),k=Zr(y).pipe(g(R=>R.startsWith(`__codelineno-${p}-`)));k.subscribe(R=>{let[,,se]=R.split("-"),ce=se.split(":").map(Se=>+Se-d+1);ce.length===1&&ce.push(ce[0]);for(let Se of M(".hll:not(.select)",e))Se.replaceWith(...Array.from(Se.childNodes));let he=u.slice(ce[0]-1,ce[1]);for(let Se of he){let Ue=document.createElement("span");Ue.className="hll",Ue.append(...Array.from(Se.childNodes).slice(1)),Se.append(Ue)}}),k.pipe(Ee(1),xe(pe)).subscribe(R=>{if(R.includes(":")){let se=document.getElementById(R.split(":")[0]);se&&setTimeout(()=>{let ce=se,he=-64;for(;ce!==document.body;)he+=ce.offsetTop,ce=ce.offsetParent;window.scrollTo({top:he})},1)}});let je=fe(M('a[href^="#__codelineno"]',f)).pipe(J(R=>h(R,"click").pipe(O(se=>se.preventDefault()),m(()=>R)))).pipe(W(i),te(qn),m(([R,se])=>{let he=+j(`[id="${R.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(se===!1)return[he,he];{let Se=M(".hll",e).map(Ue=>+Ue.parentElement.id.split("-").pop());return[Math.min(he,...Se),Math.max(he,...Se)]}}));L(ee,je).subscribe(R=>{let se=`#__codelineno-${p}-`;R[0]===R[1]?se+=R[0]:se+=`${R[0]}:${R[1]}`,history.replaceState({},"",se),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+se,oldURL:window.location.href}))})}if(Kn.default.isSupported()&&(e.closest(".copy")||V("content.code.copy")&&!e.closest(".no-copy"))){let d=Hn(a.id);s.push(d),V("content.tooltips")&&l.push(Xe(d,{viewport$}))}if(s.length){let d=Pn();d.append(...s),a.insertBefore(d,e)}return es(e).pipe(O(d=>n.next(d)),A(()=>n.complete()),m(d=>P({ref:e},d)),Ve(L(...l).pipe(W(i))))});return V("content.lazy")?mt(e).pipe(g(n=>n),Ee(1),b(()=>o)):o}function ts(e,{target$:t,print$:r}){let o=!0;return L(t.pipe(m(n=>n.closest("details:not([open])")),g(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(g(n=>n||!o),O(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Bn(e,t){return H(()=>{let r=new T;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),ts(e,t).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}var Gn=0;function rs(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],o=e.nextElementSibling;for(;o&&!(o instanceof HTMLHeadingElement);)r.push(o),o=o.nextElementSibling;return r}function os(e,t){for(let r of M("[href], [src]",e))for(let o of["href","src"]){let n=r.getAttribute(o);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){r[o]=new URL(r.getAttribute(o),t).toString();break}}for(let r of M("[name^=__], [for]",e))for(let o of["id","for","name"]){let n=r.getAttribute(o);n&&r.setAttribute(o,`${n}$preview_${Gn}`)}return Gn++,$(e)}function Jn(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(V("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let o=z([Ye(e),it(e)]).pipe(m(([i,s])=>i||s),Y(),g(i=>i));return rt([r,o]).pipe(b(([i])=>{let s=new URL(e.href);return s.search=s.hash="",i.has(`${s}`)?$(s):y}),b(i=>xr(i).pipe(b(s=>os(s,i)))),b(i=>{let s=e.hash?`article [id="${e.hash.slice(1)}"]`:"article h1",a=ue(s,i);return typeof a=="undefined"?y:$(rs(a))})).pipe(b(i=>{let s=new F(a=>{let c=wr(...i);return a.next(c),document.body.append(c),()=>c.remove()});return Vt(e,P({content$:s},t))}))}var Xn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var so,is=0;function as(){return typeof mermaid=="undefined"||mermaid instanceof Element?_t("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):$(void 0)}function Zn(e){return e.classList.remove("mermaid"),so||(so=as().pipe(O(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Xn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),Z(1))),so.subscribe(()=>go(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${is++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),s=r.attachShadow({mode:"closed"});s.innerHTML=n,e.replaceWith(r),i==null||i(s)})),so.pipe(m(()=>({ref:e})))}var ei=x("table");function ti(e){return e.replaceWith(ei),ei.replaceWith(Un(e)),$({ref:e})}function ss(e){let t=e.find(r=>r.checked)||e[0];return L(...e.map(r=>h(r,"change").pipe(m(()=>j(`label[for="${r.id}"]`))))).pipe(Q(j(`label[for="${t.id}"]`)),m(r=>({active:r})))}function ri(e,{viewport$:t,target$:r}){let o=j(".tabbed-labels",e),n=M(":scope > input",e),i=no("prev");e.append(i);let s=no("next");return e.append(s),H(()=>{let a=new T,c=a.pipe(oe(),ae(!0));z([a,Le(e),mt(e)]).pipe(W(c),$e(1,ye)).subscribe({next([{active:p},l]){let f=Be(p),{width:u}=de(p);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let d=gr(o);(f.xd.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([Ge(o),Le(o)]).pipe(W(c)).subscribe(([p,l])=>{let f=At(o);i.hidden=p.x<16,s.hidden=p.x>f.width-l.width-16}),L(h(i,"click").pipe(m(()=>-1)),h(s,"click").pipe(m(()=>1))).pipe(W(c)).subscribe(p=>{let{width:l}=de(o);o.scrollBy({left:l*p,behavior:"smooth"})}),r.pipe(W(c),g(p=>n.includes(p))).subscribe(p=>p.click()),o.classList.add("tabbed-labels--linked");for(let p of n){let l=j(`label[for="${p.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),h(l.firstElementChild,"click").pipe(W(c),g(f=>!(f.metaKey||f.ctrlKey)),O(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return V("content.tabs.link")&&a.pipe(Ie(1),te(t)).subscribe(([{active:p},{offset:l}])=>{let f=p.innerText.trim();if(p.hasAttribute("data-md-switching"))p.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let v of M("[data-tabs]"))for(let S of M(":scope > input",v)){let X=j(`label[for="${S.id}"]`);if(X!==p&&X.innerText.trim()===f){X.setAttribute("data-md-switching",""),S.click();break}}window.scrollTo({top:e.offsetTop-u});let d=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...d])])}}),a.pipe(W(c)).subscribe(()=>{for(let p of M("audio, video",e))p.offsetWidth&&p.autoplay?p.play().catch(()=>{}):p.pause()}),ss(n).pipe(O(p=>a.next(p)),A(()=>a.complete()),m(p=>P({ref:e},p)))}).pipe(et(pe))}function oi(e,t){let{viewport$:r,target$:o,print$:n}=t;return L(...M(".annotate:not(.highlight)",e).map(i=>zn(i,{target$:o,print$:n})),...M("pre:not(.mermaid) > code",e).map(i=>Yn(i,{target$:o,print$:n})),...M("a",e).map(i=>Jn(i,t)),...M("pre.mermaid",e).map(i=>Zn(i)),...M("table:not([class])",e).map(i=>ti(i)),...M("details",e).map(i=>Bn(i,{target$:o,print$:n})),...M("[data-tabs]",e).map(i=>ri(i,{viewport$:r,target$:o})),...M("[title]:not([data-preview])",e).filter(()=>V("content.tooltips")).map(i=>Xe(i,{viewport$:r})),...M(".footnote-ref",e).filter(()=>V("content.footnote.tooltips")).map(i=>Vt(i,{content$:new F(s=>{let a=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(a).cloneNode(!0).children),p=wr(...c);return s.next(p),document.body.append(p),()=>p.remove()}),viewport$:r})))}function cs(e,{alert$:t}){return t.pipe(b(r=>L($(!0),$(!1).pipe(nt(2e3))).pipe(m(o=>({message:r,active:o})))))}function ni(e,t){let r=j(".md-typeset",e);return H(()=>{let o=new T;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),cs(e,t).pipe(O(n=>o.next(n)),A(()=>o.complete()),m(n=>P({ref:e},n)))})}var ps=0;function ls(e,t){document.body.append(e);let{width:r}=de(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=vr(t),n=typeof o!="undefined"?Ge(o):$({x:0,y:0}),i=L(Ye(t),it(t)).pipe(Y());return z([i,n]).pipe(m(([s,a])=>{let{x:c,y:p}=Be(t),l=de(t),f=t.closest("table");return f&&t.parentElement&&(c+=f.offsetLeft+t.parentElement.offsetLeft,p+=f.offsetTop+t.parentElement.offsetTop),{active:s,offset:{x:c-a.x+l.width/2-r/2,y:p-a.y+l.height+8}}}))}function ii(e){let t=e.title;if(!t.length)return y;let r=`__tooltip_${ps++}`,o=Dt(r,"inline"),n=j(".md-typeset",o);return n.innerHTML=t,H(()=>{let i=new T;return i.subscribe({next({offset:s}){o.style.setProperty("--md-tooltip-x",`${s.x}px`),o.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),L(i.pipe(g(({active:s})=>s)),i.pipe(Ae(250),g(({active:s})=>!s))).subscribe({next({active:s}){s?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe($e(16,ye)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(gt(125,ye),g(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?o.style.setProperty("--md-tooltip-0",`${-s}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),ls(o,e).pipe(O(s=>i.next(s)),A(()=>i.complete()),m(s=>P({ref:e},s)))}).pipe(et(pe))}function ms({viewport$:e}){if(!V("header.autohide"))return $(!1);let t=e.pipe(m(({offset:{y:n}})=>n),ot(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),Y()),o=Je("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),Y(),b(n=>n?r:$(!1)),Q(!1))}function ai(e,t){return H(()=>z([Le(e),ms(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),Y((r,o)=>r.height===o.height&&r.hidden===o.hidden),Z(1))}function si(e,{header$:t,main$:r}){return H(()=>{let o=new T,n=o.pipe(oe(),ae(!0));o.pipe(ne("active"),Pe(t)).subscribe(([{active:s},{hidden:a}])=>{e.classList.toggle("md-header--shadow",s&&!a),e.hidden=a});let i=fe(M("[title]",e)).pipe(g(()=>V("content.tooltips")),J(s=>ii(s)));return r.subscribe(o),t.pipe(W(n),m(s=>P({ref:e},s)),Ve(i.pipe(W(n))))})}function fs(e,{viewport$:t,header$:r}){return Er(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=de(e);return{active:n>0&&o>=n}}),ne("active"))}function ci(e,t){return H(()=>{let r=new T;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=ue(".md-content h1");return typeof o=="undefined"?y:fs(o,t).pipe(O(n=>r.next(n)),A(()=>r.complete()),m(n=>P({ref:e},n)))})}function pi(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),Y()),n=o.pipe(b(()=>Le(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),ne("bottom"))));return z([o,n,t]).pipe(m(([i,{top:s,bottom:a},{offset:{y:c},size:{height:p}}])=>(p=Math.max(0,p-Math.max(0,s-c,i)-Math.max(0,p+c-a)),{offset:s-i,height:p,active:s-i<=c})),Y((i,s)=>i.offset===s.offset&&i.height===s.height&&i.active===s.active))}function us(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return $(...e).pipe(J(o=>h(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),Z(1))}function li(e){let t=M("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=Wt("(prefers-color-scheme: light)");return H(()=>{let i=new T;return i.subscribe(s=>{if(document.body.setAttribute("data-md-color-switching",""),s.color.media==="(prefers-color-scheme)"){let a=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(a.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");s.color.scheme=c.getAttribute("data-md-color-scheme"),s.color.primary=c.getAttribute("data-md-color-primary"),s.color.accent=c.getAttribute("data-md-color-accent")}for(let[a,c]of Object.entries(s.color))document.body.setAttribute(`data-md-color-${a}`,c);for(let a=0;as.key==="Enter"),te(i,(s,a)=>a)).subscribe(({index:s})=>{s=(s+1)%t.length,t[s].click(),t[s].focus()}),i.pipe(m(()=>{let s=Ce("header"),a=window.getComputedStyle(s);return o.content=a.colorScheme,a.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(s=>r.content=`#${s}`),i.pipe(xe(pe)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),us(t).pipe(W(n.pipe(Ie(1))),vt(),O(s=>i.next(s)),A(()=>i.complete()),m(s=>P({ref:e},s)))})}function mi(e,{progress$:t}){return H(()=>{let r=new T;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(O(o=>r.next({value:o})),A(()=>r.complete()),m(o=>({ref:e,value:o})))})}function fi(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function ds(e,t){let r=new Map;for(let o of M("url",e)){let n=j("loc",o),i=[fi(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let s of M("[rel=alternate]",o)){let a=s.getAttribute("href");a!=null&&i.push(fi(new URL(a),t))}}return r}function kt(e){return En(new URL("sitemap.xml",e)).pipe(m(t=>ds(t,new URL(e))),ve(()=>$(new Map)),le())}function ui({document$:e}){let t=new Map;e.pipe(b(()=>M("link[rel=alternate]")),m(r=>new URL(r.href)),g(r=>!t.has(r.toString())),J(r=>kt(r).pipe(m(o=>[r,o]),ve(()=>y)))).subscribe(([r,o])=>{t.set(r.toString().replace(/\/$/,""),o)}),h(document.body,"click").pipe(g(r=>!r.metaKey&&!r.ctrlKey),b(r=>{if(r.target instanceof Element){let o=r.target.closest("a");if(o&&!o.target){let n=[...t].find(([f])=>o.href.startsWith(`${f}/`));if(typeof n=="undefined")return y;let[i,s]=n,a=we();if(a.href.startsWith(i))return y;let c=Te(),p=a.href.replace(c.base,"");p=`${i}/${p}`;let l=s.has(p.split("#")[0])?new URL(p,c.base):new URL(i);return r.preventDefault(),$(l)}}return y})).subscribe(r=>st(r,!0))}var co=$t(ao());function hs(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function di({alert$:e}){co.default.isSupported()&&new F(t=>{new co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||hs(j(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(O(t=>{t.trigger.focus()}),m(()=>Me("clipboard.copied"))).subscribe(e)}function hi(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),$(r)):y}function bi(e){let t=new Map;for(let r of M(":scope > *",e.head))t.set(r.outerHTML,r);return t}function vi(e){for(let t of M("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return $(e)}function bs(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...V("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=ue(o),i=ue(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=bi(document);for(let[o,n]of bi(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Ce("container");return Ke(M("script",r)).pipe(b(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new F(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),y}),oe(),ae(document))}function gi({sitemap$:e,location$:t,viewport$:r,progress$:o}){if(location.protocol==="file:")return y;$(document).subscribe(vi);let n=h(document.body,"click").pipe(Pe(e),b(([a,c])=>hi(a,c)),m(({href:a})=>new URL(a)),le()),i=h(window,"popstate").pipe(m(we),le());n.pipe(te(r)).subscribe(([a,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",a)}),L(n,i).subscribe(t);let s=t.pipe(ne("pathname"),b(a=>xr(a,{progress$:o}).pipe(ve(()=>(st(a,!0),y)))),b(vi),b(bs),le());return L(s.pipe(te(t,(a,c)=>c)),s.pipe(b(()=>t),ne("hash")),t.pipe(Y((a,c)=>a.pathname===c.pathname&&a.hash===c.hash),b(()=>n),O(()=>history.back()))).subscribe(a=>{var c,p;history.state!==null||!a.hash?window.scrollTo(0,(p=(c=history.state)==null?void 0:c.y)!=null?p:0):(history.scrollRestoration="auto",gn(a.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(ne("offset"),Ae(100)).subscribe(({offset:a})=>{history.replaceState(a,"")}),V("navigation.instant.prefetch")&&L(h(document.body,"mousemove"),h(document.body,"focusin")).pipe(Pe(e),b(([a,c])=>hi(a,c)),Ae(25),Qr(({href:a})=>a),hr(a=>{let c=document.createElement("link");return c.rel="prefetch",c.href=a.toString(),document.head.appendChild(c),h(c,"load").pipe(m(()=>c),Ee(1))})).subscribe(a=>a.remove()),s}var yi=$t(ro());function xi(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,s)=>`${i}${s}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return s=>(0,yi.default)(s).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function zt(e){return e.type===1}function Sr(e){return e.type===3}function Ei(e,t){let r=Mn(e);return L($(location.protocol!=="file:"),Je("search")).pipe(Re(o=>o),b(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:V("search.suggest")}}})),r}function wi(e){var l;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:o,currentBaseURL:n}=e,i=(l=po(n))==null?void 0:l.pathname;if(i===void 0)return;let s=ys(o.pathname,i);if(s===void 0)return;let a=Es(t.keys());if(!t.has(a))return;let c=po(s,a);if(!c||!t.has(c.href))return;let p=po(s,r);if(p)return p.hash=o.hash,p.search=o.search,p}function po(e,t){try{return new URL(e,t)}catch(r){return}}function ys(e,t){if(e.startsWith(t))return e.slice(t.length)}function xs(e,t){let r=Math.min(e.length,t.length),o;for(o=0;oy)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:s,aliases:a})=>s===i||a.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),b(n=>h(document.body,"click").pipe(g(i=>!i.metaKey&&!i.ctrlKey),te(o),b(([i,s])=>{if(i.target instanceof Element){let a=i.target.closest("a");if(a&&!a.target&&n.has(a.href)){let c=a.href;return!i.target.closest(".md-version")&&n.get(c)===s?y:(i.preventDefault(),$(new URL(c)))}}return y}),b(i=>kt(i).pipe(m(s=>{var a;return(a=wi({selectedVersionSitemap:s,selectedVersionBaseURL:i,currentLocation:we(),currentBaseURL:t.base}))!=null?a:i})))))).subscribe(n=>st(n,!0)),z([r,o]).subscribe(([n,i])=>{j(".md-header__topic").appendChild(Wn(n,i))}),e.pipe(b(()=>o)).subscribe(n=>{var a;let i=new URL(t.base),s=__md_get("__outdated",sessionStorage,i);if(s===null){s=!0;let c=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let p of c)for(let l of n.aliases.concat(n.version))if(new RegExp(p,"i").test(l)){s=!1;break e}__md_set("__outdated",s,sessionStorage,i)}if(s)for(let c of me("outdated"))c.hidden=!1})}function ws(e,{worker$:t}){let{searchParams:r}=we();r.has("q")&&(at("search",!0),e.value=r.get("q"),e.focus(),Je("search").pipe(Re(i=>!i)).subscribe(()=>{let i=we();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=Ye(e),n=L(t.pipe(Re(zt)),h(e,"keyup"),o).pipe(m(()=>e.value),Y());return z([n,o]).pipe(m(([i,s])=>({value:i,focus:s})),Z(1))}function Si(e,{worker$:t}){let r=new T,o=r.pipe(oe(),ae(!0));z([t.pipe(Re(zt)),r],(i,s)=>s).pipe(ne("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(ne("focus")).subscribe(({focus:i})=>{i&&at("search",i)}),h(e.form,"reset").pipe(W(o)).subscribe(()=>e.focus());let n=j("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ws(e,{worker$:t}).pipe(O(i=>r.next(i)),A(()=>r.complete()),m(i=>P({ref:e},i)),Z(1))}function Oi(e,{worker$:t,query$:r}){let o=new T,n=un(e.parentElement).pipe(g(Boolean)),i=e.parentElement,s=j(":scope > :first-child",e),a=j(":scope > :last-child",e);Je("search").subscribe(l=>{a.setAttribute("role",l?"list":"presentation"),a.hidden=!l}),o.pipe(te(r),Gr(t.pipe(Re(zt)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:s.textContent=f.length?Me("search.result.none"):Me("search.result.placeholder");break;case 1:s.textContent=Me("search.result.one");break;default:let u=br(l.length);s.textContent=Me("search.result.other",u)}});let c=o.pipe(O(()=>a.innerHTML=""),b(({items:l})=>L($(...l.slice(0,10)),$(...l.slice(10)).pipe(ot(4),Xr(n),b(([f])=>f)))),m(Fn),le());return c.subscribe(l=>a.appendChild(l)),c.pipe(J(l=>{let f=ue("details",l);return typeof f=="undefined"?y:h(f,"toggle").pipe(W(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(g(Sr),m(({data:l})=>l)).pipe(O(l=>o.next(l)),A(()=>o.complete()),m(l=>P({ref:e},l)))}function Ts(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=we();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function Li(e,t){let r=new T,o=r.pipe(oe(),ae(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(W(o)).subscribe(n=>n.preventDefault()),Ts(e,t).pipe(O(n=>r.next(n)),A(()=>r.complete()),m(n=>P({ref:e},n)))}function Mi(e,{worker$:t,keyboard$:r}){let o=new T,n=Ce("search-query"),i=L(h(n,"keydown"),h(n,"focus")).pipe(xe(pe),m(()=>n.value),Y());return o.pipe(Pe(i),m(([{suggest:a},c])=>{let p=c.split(/([\s-]+)/);if(a!=null&&a.length&&p[p.length-1]){let l=a[a.length-1];l.startsWith(p[p.length-1])&&(p[p.length-1]=l)}else p.length=0;return p})).subscribe(a=>e.innerHTML=a.join("").replace(/\s/g," ")),r.pipe(g(({mode:a})=>a==="search")).subscribe(a=>{a.type==="ArrowRight"&&e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText)}),t.pipe(g(Sr),m(({data:a})=>a)).pipe(O(a=>o.next(a)),A(()=>o.complete()),m(()=>({ref:e})))}function _i(e,{index$:t,keyboard$:r}){let o=Te();try{let n=Ei(o.search,t),i=Ce("search-query",e),s=Ce("search-result",e);h(e,"click").pipe(g(({target:c})=>c instanceof Element&&!!c.closest("a"))).subscribe(()=>at("search",!1)),r.pipe(g(({mode:c})=>c==="search")).subscribe(c=>{let p=Ne();switch(c.type){case"Enter":if(p===i){let l=new Map;for(let f of M(":first-child [href]",s)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,d])=>d-u);f.click()}c.claim()}break;case"Escape":case"Tab":at("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof p=="undefined")i.focus();else{let l=[i,...M(":not(details) > [href], summary, details[open] [href]",s)],f=Math.max(0,(Math.max(0,l.indexOf(p))+l.length+(c.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}c.claim();break;default:i!==Ne()&&i.focus()}}),r.pipe(g(({mode:c})=>c==="global")).subscribe(c=>{switch(c.type){case"f":case"s":case"/":i.focus(),i.select(),c.claim();break}});let a=Si(i,{worker$:n});return L(a,Oi(s,{worker$:n,query$:a})).pipe(Ve(...me("search-share",e).map(c=>Li(c,{query$:a})),...me("search-suggest",e).map(c=>Mi(c,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,tt}}function Ai(e,{index$:t,location$:r}){return z([t,r.pipe(Q(we()),g(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>xi(o.config)(n.searchParams.get("h"))),m(o=>{var s;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let a=i.nextNode();a;a=i.nextNode())if((s=a.parentElement)!=null&&s.offsetHeight){let c=a.textContent,p=o(c);p.length>c.length&&n.set(a,p)}for(let[a,c]of n){let{childNodes:p}=x("span",null,c);a.replaceWith(...Array.from(p))}return{ref:e,nodes:n}}))}function Ss(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:s},{offset:{y:a}}])=>(s=s+Math.min(n,Math.max(0,a-i))-n,{height:s,locked:a>=i+n})),Y((i,s)=>i.height===s.height&&i.locked===s.locked))}function lo(e,o){var n=o,{header$:t}=n,r=vo(n,["header$"]);let i=j(".md-sidebar__scrollwrap",e),{y:s}=Be(i);return H(()=>{let a=new T,c=a.pipe(oe(),ae(!0)),p=a.pipe($e(0,ye));return p.pipe(te(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*s}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),p.pipe(Re()).subscribe(()=>{for(let l of M(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=de(f);f.scrollTo({top:u-d/2})}}}),fe(M("label[tabindex]",e)).pipe(J(l=>h(l,"click").pipe(xe(pe),m(()=>l),W(c)))).subscribe(l=>{let f=j(`[id="${l.htmlFor}"]`);j(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),V("content.tooltips")&&fe(M("abbr[title]",e)).pipe(J(l=>Xe(l,{viewport$})),W(c)).subscribe(),Ss(e,r).pipe(O(l=>a.next(l)),A(()=>a.complete()),m(l=>P({ref:e},l)))})}function Ci(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return rt(ze(`${r}/releases/latest`).pipe(ve(()=>y),m(o=>({version:o.tag_name})),Qe({})),ze(r).pipe(ve(()=>y),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Qe({}))).pipe(m(([o,n])=>P(P({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return ze(r).pipe(m(o=>({repositories:o.public_repos})),Qe({}))}}function ki(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return rt(ze(`${r}/releases/permalink/latest`).pipe(ve(()=>y),m(({tag_name:o})=>({version:o})),Qe({})),ze(r).pipe(ve(()=>y),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Qe({}))).pipe(m(([o,n])=>P(P({},o),n)))}function Hi(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return Ci(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ki(r,o)}return y}var Os;function Ls(e){return Os||(Os=H(()=>{let t=__md_get("__source",sessionStorage);if(t)return $(t);if(me("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return y}return Hi(e.href).pipe(O(o=>__md_set("__source",o,sessionStorage)))}).pipe(ve(()=>y),g(t=>Object.keys(t).length>0),m(t=>({facts:t})),Z(1)))}function $i(e){let t=j(":scope > :last-child",e);return H(()=>{let r=new T;return r.subscribe(({facts:o})=>{t.appendChild(jn(o)),t.classList.add("md-source__repository--active")}),Ls(e).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}function Ms(e,{viewport$:t,header$:r}){return Le(document.body).pipe(b(()=>Er(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),ne("hidden"))}function Pi(e,t){return H(()=>{let r=new T;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(V("navigation.tabs.sticky")?$({hidden:!1}):Ms(e,t)).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}function _s(e,{viewport$:t,header$:r}){let o=new Map,n=M(".md-nav__link",e);for(let a of n){let c=decodeURIComponent(a.hash.substring(1)),p=ue(`[id="${c}"]`);typeof p!="undefined"&&o.set(a,p)}let i=r.pipe(ne("height"),m(({height:a})=>{let c=Ce("main"),p=j(":scope > :first-child",c);return a+.8*(p.offsetTop-c.offsetTop)}),le());return Le(document.body).pipe(ne("height"),b(a=>H(()=>{let c=[];return $([...o].reduce((p,[l,f])=>{for(;c.length&&o.get(c[c.length-1]).tagName>=f.tagName;)c.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return p.set([...c=[...c,l]].reverse(),u)},new Map))}).pipe(m(c=>new Map([...c].sort(([,p],[,l])=>p-l))),Pe(i),b(([c,p])=>t.pipe(Ut(([l,f],{offset:{y:u},size:d})=>{let v=u+d.height>=Math.floor(a.height);for(;f.length;){let[,S]=f[0];if(S-p=u&&!v)f=[l.pop(),...f];else break}return[l,f]},[[],[...c]]),Y((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([a,c])=>({prev:a.map(([p])=>p),next:c.map(([p])=>p)})),Q({prev:[],next:[]}),ot(2,1),m(([a,c])=>a.prev.length{let i=new T,s=i.pipe(oe(),ae(!0));if(i.subscribe(({prev:a,next:c})=>{for(let[p]of c)p.classList.remove("md-nav__link--passed"),p.classList.remove("md-nav__link--active");for(let[p,[l]]of a.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",p===a.length-1)}),V("toc.follow")){let a=L(t.pipe(Ae(1),m(()=>{})),t.pipe(Ae(250),m(()=>"smooth")));i.pipe(g(({prev:c})=>c.length>0),Pe(o.pipe(xe(pe))),te(a)).subscribe(([[{prev:c}],p])=>{let[l]=c[c.length-1];if(l.offsetHeight){let f=vr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=de(f);f.scrollTo({top:u-d/2,behavior:p})}}})}return V("navigation.tracking")&&t.pipe(W(s),ne("offset"),Ae(250),Ie(1),W(n.pipe(Ie(1))),vt({delay:250}),te(i)).subscribe(([,{prev:a}])=>{let c=we(),p=a[a.length-1];if(p&&p.length){let[l]=p,{hash:f}=new URL(l.href);c.hash!==f&&(c.hash=f,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),_s(e,{viewport$:t,header$:r}).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))})}function As(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:s}})=>s),ot(2,1),m(([s,a])=>s>a&&a>0),Y()),i=r.pipe(m(({active:s})=>s));return z([i,n]).pipe(m(([s,a])=>!(s&&a)),Y(),W(o.pipe(Ie(1))),ae(!0),vt({delay:250}),m(s=>({hidden:s})))}function Ii(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new T,s=i.pipe(oe(),ae(!0));return i.subscribe({next({hidden:a}){e.hidden=a,a?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(W(s),ne("height")).subscribe(({height:a})=>{e.style.top=`${a+16}px`}),h(e,"click").subscribe(a=>{a.preventDefault(),window.scrollTo({top:0})}),As(e,{viewport$:t,main$:o,target$:n}).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))}function Fi({document$:e,viewport$:t}){e.pipe(b(()=>M(".md-ellipsis")),J(r=>mt(r).pipe(W(e.pipe(Ie(1))),g(o=>o),m(()=>r),Ee(1))),g(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,V("content.tooltips")?Xe(n,{viewport$:t}).pipe(W(e.pipe(Ie(1))),A(()=>n.removeAttribute("title"))):y})).subscribe(),V("content.tooltips")&&e.pipe(b(()=>M(".md-status")),J(r=>Xe(r,{viewport$:t}))).subscribe()}function ji({document$:e,tablet$:t}){e.pipe(b(()=>M(".md-toggle--indeterminate")),O(r=>{r.indeterminate=!0,r.checked=!1}),J(r=>h(r,"change").pipe(Jr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),te(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function Cs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ui({document$:e}){e.pipe(b(()=>M("[data-md-scrollfix]")),O(t=>t.removeAttribute("data-md-scrollfix")),g(Cs),J(t=>h(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Wi({viewport$:e,tablet$:t}){z([Je("search"),t]).pipe(m(([r,o])=>r&&!o),b(r=>$(r).pipe(nt(r?400:100))),te(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function ks(){return location.protocol==="file:"?_t(`${new URL("search/search_index.js",Or.base)}`).pipe(m(()=>__index),Z(1)):ze(new URL("search/search_index.json",Or.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ct=an(),Kt=bn(),Ht=yn(Kt),mo=hn(),ke=Ln(),Lr=Wt("(min-width: 60em)"),Vi=Wt("(min-width: 76.25em)"),Ni=xn(),Or=Te(),zi=document.forms.namedItem("search")?ks():tt,fo=new T;di({alert$:fo});ui({document$:ct});var uo=new T,qi=kt(Or.base);V("navigation.instant")&&gi({sitemap$:qi,location$:Kt,viewport$:ke,progress$:uo}).subscribe(ct);var Di;((Di=Or.version)==null?void 0:Di.provider)==="mike"&&Ti({document$:ct});L(Kt,Ht).pipe(nt(125)).subscribe(()=>{at("drawer",!1),at("search",!1)});mo.pipe(g(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=ue("link[rel=prev]");typeof t!="undefined"&&st(t);break;case"n":case".":let r=ue("link[rel=next]");typeof r!="undefined"&&st(r);break;case"Enter":let o=Ne();o instanceof HTMLLabelElement&&o.click()}});Fi({viewport$:ke,document$:ct});ji({document$:ct,tablet$:Lr});Ui({document$:ct});Wi({viewport$:ke,tablet$:Lr});var ft=ai(Ce("header"),{viewport$:ke}),qt=ct.pipe(m(()=>Ce("main")),b(e=>pi(e,{viewport$:ke,header$:ft})),Z(1)),Hs=L(...me("consent").map(e=>An(e,{target$:Ht})),...me("dialog").map(e=>ni(e,{alert$:fo})),...me("palette").map(e=>li(e)),...me("progress").map(e=>mi(e,{progress$:uo})),...me("search").map(e=>_i(e,{index$:zi,keyboard$:mo})),...me("source").map(e=>$i(e))),$s=H(()=>L(...me("announce").map(e=>_n(e)),...me("content").map(e=>oi(e,{sitemap$:qi,viewport$:ke,target$:Ht,print$:Ni})),...me("content").map(e=>V("search.highlight")?Ai(e,{index$:zi,location$:Kt}):y),...me("header").map(e=>si(e,{viewport$:ke,header$:ft,main$:qt})),...me("header-title").map(e=>ci(e,{viewport$:ke,header$:ft})),...me("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?eo(Vi,()=>lo(e,{viewport$:ke,header$:ft,main$:qt})):eo(Lr,()=>lo(e,{viewport$:ke,header$:ft,main$:qt}))),...me("tabs").map(e=>Pi(e,{viewport$:ke,header$:ft})),...me("toc").map(e=>Ri(e,{viewport$:ke,header$:ft,main$:qt,target$:Ht})),...me("top").map(e=>Ii(e,{viewport$:ke,header$:ft,main$:qt,target$:Ht})))),Ki=ct.pipe(b(()=>$s),Ve(Hs),Z(1));Ki.subscribe();window.document$=ct;window.location$=Kt;window.target$=Ht;window.keyboard$=mo;window.viewport$=ke;window.tablet$=Lr;window.screen$=Vi;window.print$=Ni;window.alert$=fo;window.progress$=uo;window.component$=Ki;})(); +//# sourceMappingURL=bundle.79ae519e.min.js.map + diff --git a/assets/javascripts/bundle.79ae519e.min.js.map b/assets/javascripts/bundle.79ae519e.min.js.map new file mode 100644 index 0000000..5cf0289 --- /dev/null +++ b/assets/javascripts/bundle.79ae519e.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/escape-html/index.js", "node_modules/clipboard/dist/clipboard.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/tslib/tslib.es6.mjs", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinct.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/exhaustMap.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/link/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/alternate/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/findurl/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*\n * Copyright (c) 2016-2025 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n fetchSitemap,\n setupAlternate,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 60em)\")\nconst screen$ = watchMedia(\"(min-width: 76.25em)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up language selector */\nsetupAlternate({ document$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up sitemap for instant navigation and previews */\nconst sitemap$ = fetchSitemap(config.base)\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ sitemap$, location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { sitemap$, viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/******************************************************************************\nCopyright (c) Microsoft Corporation.\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n***************************************************************************** */\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nexport function __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\nexport var __assign = function() {\n __assign = Object.assign || function __assign(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n }\n return __assign.apply(this, arguments);\n}\n\nexport function __rest(s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\n t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\n t[p[i]] = s[p[i]];\n }\n return t;\n}\n\nexport function __decorate(decorators, target, key, desc) {\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n return c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\nexport function __param(paramIndex, decorator) {\n return function (target, key) { decorator(target, key, paramIndex); }\n}\n\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\n var _, done = false;\n for (var i = decorators.length - 1; i >= 0; i--) {\n var context = {};\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\n if (kind === \"accessor\") {\n if (result === void 0) continue;\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\n if (_ = accept(result.get)) descriptor.get = _;\n if (_ = accept(result.set)) descriptor.set = _;\n if (_ = accept(result.init)) initializers.unshift(_);\n }\n else if (_ = accept(result)) {\n if (kind === \"field\") initializers.unshift(_);\n else descriptor[key] = _;\n }\n }\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\n done = true;\n};\n\nexport function __runInitializers(thisArg, initializers, value) {\n var useValue = arguments.length > 2;\n for (var i = 0; i < initializers.length; i++) {\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\n }\n return useValue ? value : void 0;\n};\n\nexport function __propKey(x) {\n return typeof x === \"symbol\" ? x : \"\".concat(x);\n};\n\nexport function __setFunctionName(f, name, prefix) {\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\n};\n\nexport function __metadata(metadataKey, metadataValue) {\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\n}\n\nexport function __awaiter(thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n}\n\nexport function __generator(thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n}\n\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n});\n\nexport function __exportStar(m, o) {\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\n}\n\nexport function __values(o) {\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n if (m) return m.call(o);\n if (o && typeof o.length === \"number\") return {\n next: function () {\n if (o && i >= o.length) o = void 0;\n return { value: o && o[i++], done: !o };\n }\n };\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n}\n\nexport function __read(o, n) {\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\n if (!m) return o;\n var i = m.call(o), r, ar = [], e;\n try {\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\n }\n catch (error) { e = { error: error }; }\n finally {\n try {\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\n }\n finally { if (e) throw e.error; }\n }\n return ar;\n}\n\n/** @deprecated */\nexport function __spread() {\n for (var ar = [], i = 0; i < arguments.length; i++)\n ar = ar.concat(__read(arguments[i]));\n return ar;\n}\n\n/** @deprecated */\nexport function __spreadArrays() {\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\n r[k] = a[j];\n return r;\n}\n\nexport function __spreadArray(to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n}\n\nexport function __await(v) {\n return this instanceof __await ? (this.v = v, this) : new __await(v);\n}\n\nexport function __asyncGenerator(thisArg, _arguments, generator) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\n function fulfill(value) { resume(\"next\", value); }\n function reject(value) { resume(\"throw\", value); }\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\n}\n\nexport function __asyncDelegator(o) {\n var i, p;\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\n}\n\nexport function __asyncValues(o) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var m = o[Symbol.asyncIterator], i;\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\n}\n\nexport function __makeTemplateObject(cooked, raw) {\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\n return cooked;\n};\n\nvar __setModuleDefault = Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n};\n\nexport function __importStar(mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n __setModuleDefault(result, mod);\n return result;\n}\n\nexport function __importDefault(mod) {\n return (mod && mod.__esModule) ? mod : { default: mod };\n}\n\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n}\n\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n}\n\nexport function __classPrivateFieldIn(state, receiver) {\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\n}\n\nexport function __addDisposableResource(env, value, async) {\n if (value !== null && value !== void 0) {\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\n var dispose, inner;\n if (async) {\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\n dispose = value[Symbol.asyncDispose];\n }\n if (dispose === void 0) {\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\n dispose = value[Symbol.dispose];\n if (async) inner = dispose;\n }\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\n env.stack.push({ value: value, dispose: dispose, async: async });\n }\n else if (async) {\n env.stack.push({ async: true });\n }\n return value;\n}\n\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\n var e = new Error(message);\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\n};\n\nexport function __disposeResources(env) {\n function fail(e) {\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\n env.hasError = true;\n }\n var r, s = 0;\n function next() {\n while (r = env.stack.pop()) {\n try {\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\n if (r.dispose) {\n var result = r.dispose.call(r.value);\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\n }\n else s |= 1;\n }\n catch (e) {\n fail(e);\n }\n }\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\n if (env.hasError) throw env.error;\n }\n return next();\n}\n\nexport default {\n __extends,\n __assign,\n __rest,\n __decorate,\n __param,\n __metadata,\n __awaiter,\n __generator,\n __createBinding,\n __exportStar,\n __values,\n __read,\n __spread,\n __spreadArrays,\n __spreadArray,\n __await,\n __asyncGenerator,\n __asyncDelegator,\n __asyncValues,\n __makeTemplateObject,\n __importStar,\n __importDefault,\n __classPrivateFieldGet,\n __classPrivateFieldSet,\n __classPrivateFieldIn,\n __addDisposableResource,\n __disposeResources,\n};\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n */\nexport class Subscription implements SubscriptionLike {\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param value The `next` value.\n */\n next(value: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param err The `error` exception.\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as ((value: T) => void) | undefined,\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent.\n * @param subscriber The stopped subscriber.\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @param subscribe The function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @param subscribe the subscriber function to be passed to the Observable constructor\n * @return A new observable.\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @param operator the operator defining the operation to take on the observable\n * @return A new observable with the Operator applied.\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param observerOrNext Either an {@link Observer} with some or all callback methods,\n * or the `next` handler that is called for each value emitted from the subscribed Observable.\n * @param error A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param complete A handler for a terminal event resulting from successful completion.\n * @return A subscription reference to the registered handlers.\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next A handler for each value emitted by the observable.\n * @return A promise that either resolves on observable completion or\n * rejects with the handled error.\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @return This instance of the observable.\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n *\n * @return The Observable result of all the operators having been called\n * in the order they were passed in.\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return Observable that this Subject casts to.\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param _bufferSize The size of the buffer to replay on subscription\n * @param _windowTime The amount of time the buffered items will stay buffered\n * @param _timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param state Some contextual data that the `work` function uses when called by the\n * Scheduler.\n * @param delay Time to wait before executing the work, where the time unit is implicit\n * and defined by the Scheduler.\n * @return A subscription in order to be able to unsubscribe the scheduled work.\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param work A function representing a task, or some unit of work to be\n * executed by the Scheduler.\n * @param delay Time to wait before executing the work, where the time unit is\n * implicit and defined by the Scheduler itself.\n * @param state Some contextual data that the `work` function uses when called\n * by the Scheduler.\n * @return A subscription in order to be able to unsubscribe the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && id === scheduler._scheduled && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n let flushId;\n if (action) {\n flushId = action.id;\n } else {\n flushId = this._scheduled;\n this._scheduled = undefined;\n }\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an