snapshot 2019-04-19 23:13:13.303952
This commit is contained in:
		
						commit
						dade740155
					
				
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| *.pyc | ||||
| __pycache__ | ||||
| *.idea | ||||
| build/ | ||||
| dist/ | ||||
| *.egg-info/ | ||||
							
								
								
									
										651
									
								
								LICENSE.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										651
									
								
								LICENSE.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,651 @@ | ||||
| GNU Affero General Public License | ||||
| ================================= | ||||
| 
 | ||||
| _Version 3, 19 November 2007_ | ||||
| _Copyright © 2007 Free Software Foundation, Inc. <<http://fsf.org/>>_ | ||||
| 
 | ||||
| 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. | ||||
| 
 | ||||
|     <one line to give the program's name and a brief idea of what it does.> | ||||
|     Copyright (C) <year>  <name of author> | ||||
| 
 | ||||
|     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 <http://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| 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 | ||||
| <<http://www.gnu.org/licenses/>>. | ||||
							
								
								
									
										2
									
								
								MANIFEST.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								MANIFEST.in
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| include README.md | ||||
| include LICENSE.md | ||||
							
								
								
									
										43
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| # Masque README | ||||
| 
 | ||||
| Masque is a Python module for designing lithography masks. | ||||
| 
 | ||||
| The general idea is to implement something resembling the GDSII file-format, but | ||||
| with some vectorized element types (eg. circles, not just polygons), better support for | ||||
| E-beam doses, and the ability to output to multiple formats. | ||||
| 
 | ||||
| - [Source repository](https://mpxd.net/code/jan/masque) | ||||
| - [PyPi](https://pypi.org/project/masque) | ||||
| 
 | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
| Requirements: | ||||
| * python >= 3.5 (written and tested with 3.6) | ||||
| * numpy | ||||
| * matplotlib (optional, used for visualization functions and text) | ||||
| * python-gdsii (optional, used for gdsii i/o) | ||||
| * svgwrite (optional, used for svg output) | ||||
| * freetype (optional, used for text) | ||||
| 
 | ||||
| 
 | ||||
| Install with pip: | ||||
| ```bash | ||||
| pip3 install masque | ||||
| ``` | ||||
| 
 | ||||
| Alternatively, install from git | ||||
| ```bash | ||||
| pip3 install git+https://mpxd.net/code/jan/masque.git@release | ||||
| ``` | ||||
| 
 | ||||
| ## TODO | ||||
| 
 | ||||
| * Mirroring | ||||
| * Polygon de-embedding | ||||
| 
 | ||||
| ### Maybe | ||||
| 
 | ||||
| * Construct from bitmap | ||||
| * Boolean operations on polygons (using pyclipper) | ||||
| * Output to OASIS (using fatamorgana) | ||||
							
								
								
									
										33
									
								
								examples/ellip_grating.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								examples/ellip_grating.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| # Quick script for testing arcs | ||||
| 
 | ||||
| import numpy | ||||
| 
 | ||||
| import masque | ||||
| import masque.file.gdsii | ||||
| from masque import shapes | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     pat = masque.Pattern(name='ellip_grating') | ||||
|     for rmin in numpy.arange(10, 15, 0.5): | ||||
|         pat.shapes.append(shapes.Arc( | ||||
|             radii=(rmin, rmin), | ||||
|             width=0.1, | ||||
|             angles=(-numpy.pi/4, numpy.pi/4) | ||||
|         )) | ||||
| 
 | ||||
|     pat.scale_by(1000) | ||||
| #    pat.visualize() | ||||
|     pat2 = masque.Pattern(name='p2') | ||||
|     pat2.name = 'ellip_grating' | ||||
| 
 | ||||
|     pat2.subpatterns += [ | ||||
|         masque.SubPattern(pattern=pat, offset=(20e3, 0)), | ||||
|         masque.SubPattern(pattern=pat, offset=(0, 20e3)), | ||||
|         ] | ||||
| 
 | ||||
|     masque.file.gdsii.write_dose2dtype((pat, pat2, pat2.copy(), pat2.copy()), 'out.gds', 1e-9, 1e-3) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
							
								
								
									
										36
									
								
								masque/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								masque/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| """ | ||||
|  masque 2D CAD library | ||||
| 
 | ||||
|  masque is an attempt to make a relatively small library for designing lithography | ||||
|   masks. The general idea is to implement something resembling the GDSII file-format, but | ||||
|   with some vectorized element types (eg. circles, not just polygons), better support for | ||||
|   E-beam doses, and the ability to output to multiple formats. | ||||
| 
 | ||||
|  Pattern is a basic object containing a 2D lithography mask, composed of a list of Shape | ||||
|   objects and a list of SubPattern objects. | ||||
| 
 | ||||
|  SubPattern provides basic support for nesting Pattern objects within each other, by adding | ||||
|   offset, rotation, scaling, and other such properties to a Pattern reference. | ||||
| 
 | ||||
|  Note that the methods for these classes try to avoid copying wherever possible, so unless | ||||
|   otherwise noted, assume that arguments are stored by-reference. | ||||
| 
 | ||||
| 
 | ||||
|  Dependencies: | ||||
|     - numpy | ||||
|     - matplotlib    [Pattern.visualize(...)] | ||||
|     - python-gdsii  [masque.file.gdsii] | ||||
|     - svgwrite      [masque.file.svg] | ||||
| """ | ||||
| 
 | ||||
| from .error import PatternError | ||||
| from .shapes import Shape | ||||
| from .label import Label | ||||
| from .subpattern import SubPattern | ||||
| from .repetition import GridRepetition | ||||
| from .pattern import Pattern | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| version = '0.5' | ||||
							
								
								
									
										9
									
								
								masque/error.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								masque/error.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| class PatternError(Exception): | ||||
|     """ | ||||
|     Simple Exception for Pattern objects and their contents | ||||
|     """ | ||||
|     def __init__(self, value): | ||||
|         self.value = value | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return repr(self.value) | ||||
							
								
								
									
										3
									
								
								masque/file/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								masque/file/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| """ | ||||
| Functions for reading from and writing to various file formats. | ||||
| """ | ||||
							
								
								
									
										558
									
								
								masque/file/gdsii.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										558
									
								
								masque/file/gdsii.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,558 @@ | ||||
| """ | ||||
| GDSII file format readers and writers | ||||
| """ | ||||
| # python-gdsii | ||||
| import gdsii.library | ||||
| import gdsii.structure | ||||
| import gdsii.elements | ||||
| 
 | ||||
| from typing import List, Any, Dict, Tuple | ||||
| import re | ||||
| import numpy | ||||
| import base64 | ||||
| import struct | ||||
| import logging | ||||
| 
 | ||||
| from .utils import mangle_name, make_dose_table | ||||
| from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape | ||||
| from ..shapes import Polygon | ||||
| from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar | ||||
| from ..utils import remove_colinear_vertices | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def write(patterns: Pattern or List[Pattern], | ||||
|           filename: str, | ||||
|           meters_per_unit: float, | ||||
|           logical_units_per_unit: float = 1, | ||||
|           library_name: str = 'masque-gdsii-write'): | ||||
|     """ | ||||
|     Write a Pattern or list of patterns to a GDSII file, by first calling | ||||
|      .polygonize() to change the shapes into polygons, and then writing patterns | ||||
|      as GDSII structures, polygons as boundary elements, and subpatterns as structure | ||||
|      references (sref). | ||||
| 
 | ||||
|      For each shape, | ||||
|         layer is chosen to be equal to shape.layer if it is an int, | ||||
|             or shape.layer[0] if it is a tuple | ||||
|         datatype is chosen to be shape.layer[1] if available, | ||||
|             otherwise 0 | ||||
| 
 | ||||
|     Note that this function modifies the Pattern. | ||||
| 
 | ||||
|     It is often a good idea to run pattern.subpatternize() prior to calling this function, | ||||
|      especially if calling .polygonize() will result in very many vertices. | ||||
| 
 | ||||
|     If you want pattern polygonized with non-default arguments, just call pattern.polygonize() | ||||
|      prior to calling this function. | ||||
| 
 | ||||
|     :param patterns: A Pattern or list of patterns to write to file. Modified by this function. | ||||
|     :param filename: Filename to write to. | ||||
|     :param meters_per_unit: Written into the GDSII file, meters per (database) length unit. | ||||
|         All distances are assumed to be an integer multiple of this unit, and are stored as such. | ||||
|     :param logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a | ||||
|         "logical" unit which is different from the "database" unit, for display purposes. | ||||
|         Default 1. | ||||
|     :param library_name: Library name written into the GDSII file. | ||||
|         Default 'masque-gdsii-write'. | ||||
|     """ | ||||
|     # Create library | ||||
|     lib = gdsii.library.Library(version=600, | ||||
|                                 name=library_name.encode('ASCII'), | ||||
|                                 logical_unit=logical_units_per_unit, | ||||
|                                 physical_unit=meters_per_unit) | ||||
| 
 | ||||
|     if isinstance(patterns, Pattern): | ||||
|         patterns = [patterns] | ||||
| 
 | ||||
|     # Get a dict of id(pattern) -> pattern | ||||
|     patterns_by_id = {id(pattern): pattern for pattern in patterns} | ||||
|     for pattern in patterns: | ||||
|         patterns_by_id.update(pattern.referenced_patterns_by_id()) | ||||
| 
 | ||||
|     _disambiguate_pattern_names(patterns_by_id.values()) | ||||
| 
 | ||||
|     # Now create a structure for each pattern, and add in any Boundary and SREF elements | ||||
|     for pat in patterns_by_id.values(): | ||||
|         structure = gdsii.structure.Structure(name=pat.name) | ||||
|         lib.append(structure) | ||||
| 
 | ||||
|         # Add a Boundary element for each shape | ||||
|         structure += _shapes_to_boundaries(pat.shapes) | ||||
| 
 | ||||
|         structure += _labels_to_texts(pat.labels) | ||||
| 
 | ||||
|         # Add an SREF / AREF for each subpattern entry | ||||
|         structure += _subpatterns_to_refs(pat.subpatterns) | ||||
| 
 | ||||
|     with open(filename, mode='wb') as stream: | ||||
|         lib.save(stream) | ||||
| 
 | ||||
| 
 | ||||
| def write_dose2dtype(patterns: Pattern or List[Pattern], | ||||
|                      filename: str, | ||||
|                      meters_per_unit: float, | ||||
|                      *args, | ||||
|                      **kwargs, | ||||
|                      ) -> List[float]: | ||||
|     """ | ||||
|     Write a Pattern or list of patterns to a GDSII file, by first calling | ||||
|      .polygonize() to change the shapes into polygons, and then writing patterns | ||||
|      as GDSII structures, polygons as boundary elements, and subpatterns as structure | ||||
|      references (sref). | ||||
| 
 | ||||
|      For each shape, | ||||
|         layer is chosen to be equal to shape.layer if it is an int, | ||||
|             or shape.layer[0] if it is a tuple | ||||
|         datatype is chosen arbitrarily, based on calcualted dose for each shape. | ||||
|             Shapes with equal calcualted dose will have the same datatype. | ||||
|             A list of doses is retured, providing a mapping between datatype | ||||
|             (list index) and dose (list entry). | ||||
| 
 | ||||
|     Note that this function modifies the Pattern(s). | ||||
| 
 | ||||
|     It is often a good idea to run pattern.subpatternize() prior to calling this function, | ||||
|      especially if calling .polygonize() will result in very many vertices. | ||||
| 
 | ||||
|     If you want pattern polygonized with non-default arguments, just call pattern.polygonize() | ||||
|      prior to calling this function. | ||||
| 
 | ||||
|     :param patterns: A Pattern or list of patterns to write to file. Modified by this function. | ||||
|     :param filename: Filename to write to. | ||||
|     :param meters_per_unit: Written into the GDSII file, meters per (database) length unit. | ||||
|         All distances are assumed to be an integer multiple of this unit, and are stored as such. | ||||
|     :param args: passed to masque.file.gdsii.write(). | ||||
|     :param kwargs: passed to masque.file.gdsii.write(). | ||||
|     :returns: A list of doses, providing a mapping between datatype (int, list index) | ||||
|                 and dose (float, list entry). | ||||
|     """ | ||||
|     patterns, dose_vals = dose2dtype(patterns) | ||||
|     write(patterns, filename, meters_per_unit, *args, **kwargs) | ||||
|     return dose_vals | ||||
| 
 | ||||
| 
 | ||||
| def dose2dtype(patterns: Pattern or List[Pattern], | ||||
|               ) -> Tuple[List[Pattern], List[float]]: | ||||
|     """ | ||||
|      For each shape in each pattern, set shape.layer to the tuple | ||||
|      (base_layer, datatype), where: | ||||
|         layer is chosen to be equal to the original shape.layer if it is an int, | ||||
|             or shape.layer[0] if it is a tuple | ||||
|         datatype is chosen arbitrarily, based on calcualted dose for each shape. | ||||
|             Shapes with equal calcualted dose will have the same datatype. | ||||
|             A list of doses is retured, providing a mapping between datatype | ||||
|             (list index) and dose (list entry). | ||||
| 
 | ||||
|     Note that this function modifies the input Pattern(s). | ||||
| 
 | ||||
|     :param patterns: A Pattern or list of patterns to write to file. Modified by this function. | ||||
|     :returns: (patterns, dose_list) | ||||
|             patterns: modified input patterns | ||||
|             dose_list: A list of doses, providing a mapping between datatype (int, list index) | ||||
|                        and dose (float, list entry). | ||||
|     """ | ||||
|     if isinstance(patterns, Pattern): | ||||
|         patterns = [patterns] | ||||
| 
 | ||||
|     # Get a dict of id(pattern) -> pattern | ||||
|     patterns_by_id = {id(pattern): pattern for pattern in patterns} | ||||
|     for pattern in patterns: | ||||
|         patterns_by_id.update(pattern.referenced_patterns_by_id()) | ||||
| 
 | ||||
|     # Get a table of (id(pat), written_dose) for each pattern and subpattern | ||||
|     sd_table = make_dose_table(patterns) | ||||
| 
 | ||||
|     # Figure out all the unique doses necessary to write this pattern | ||||
|     #  This means going through each row in sd_table and adding the dose values needed to write | ||||
|     #  that subpattern at that dose level | ||||
|     dose_vals = set() | ||||
|     for pat_id, pat_dose in sd_table: | ||||
|         pat = patterns_by_id[pat_id] | ||||
|         [dose_vals.add(shape.dose * pat_dose) for shape in pat.shapes] | ||||
| 
 | ||||
|     if len(dose_vals) > 256: | ||||
|         raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals))) | ||||
| 
 | ||||
|     # Create a new pattern for each non-1-dose entry in the dose table | ||||
|     #   and update the shapes to reflect their new dose | ||||
|     new_pats = {} # (id, dose) -> new_pattern mapping | ||||
|     for pat_id, pat_dose in sd_table: | ||||
|         if pat_dose == 1: | ||||
|             new_pats[(pat_id, pat_dose)] = patterns_by_id[pat_id] | ||||
|             continue | ||||
| 
 | ||||
|         pat = patterns_by_id[pat_id].deepcopy() | ||||
| 
 | ||||
|         encoded_name = mangle_name(pat, pat_dose).encode('ASCII') | ||||
|         if len(encoded_name) == 0: | ||||
|             raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(pat.name)) | ||||
| 
 | ||||
|         for shape in pat.shapes: | ||||
|             data_type = dose_vals_list.index(shape.dose * pat_dose) | ||||
|             if is_scalar(shape.layer): | ||||
|                 layer = (shape.layer, data_type) | ||||
|             else: | ||||
|                 layer = (shape.layer[0], data_type) | ||||
| 
 | ||||
|         new_pats[(pat_id, pat_dose)] = pat | ||||
| 
 | ||||
|     # Go back through all the dose-specific patterns and fix up their subpattern entries | ||||
|     for (pat_id, pat_dose), pat in new_pats.items(): | ||||
|         for subpat in pat.subpatterns: | ||||
|             dose_mult = subpat.dose * pat_dose | ||||
|             subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)] | ||||
| 
 | ||||
|     return patterns, list(dose_vals) | ||||
| 
 | ||||
| 
 | ||||
| def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]): | ||||
|     """ | ||||
|     Alias for read(filename, use_dtype_as_dose=True) | ||||
|     """ | ||||
|     return read(filename, use_dtype_as_dose=True) | ||||
| 
 | ||||
| 
 | ||||
| def read(filename: str, | ||||
|          use_dtype_as_dose: bool = False, | ||||
|          clean_vertices: bool = True, | ||||
|          ) -> (Dict[str, Pattern], Dict[str, Any]): | ||||
|     """ | ||||
|     Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are | ||||
|      translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs | ||||
|      are translated into SubPattern objects. | ||||
| 
 | ||||
|     Additional library info is returned in a dict, containing: | ||||
|       'name': name of the library | ||||
|       'meters_per_unit': number of meters per database unit (all values are in database units) | ||||
|       'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns) | ||||
|                                 per database unit | ||||
| 
 | ||||
|     :param filename: Filename specifying a GDSII file to read from. | ||||
|     :param use_dtype_as_dose: If false, set each polygon's layer to (gds_layer, gds_datatype). | ||||
|             If true, set the layer to gds_layer and the dose to gds_datatype. | ||||
|             Default False. | ||||
|     :param clean_vertices: If true, remove any redundant vertices when loading polygons. | ||||
|             The cleaning process removes any polygons with zero area or <3 vertices. | ||||
|             Default True. | ||||
|     :return: Tuple: (Dict of pattern_name:Patterns generated from GDSII structures, Dict of GDSII library info) | ||||
|     """ | ||||
| 
 | ||||
|     with open(filename, mode='rb') as stream: | ||||
|         lib = gdsii.library.Library.load(stream) | ||||
| 
 | ||||
|     library_info = {'name': lib.name.decode('ASCII'), | ||||
|                     'meters_per_unit': lib.physical_unit, | ||||
|                     'logical_units_per_unit': lib.logical_unit, | ||||
|                     } | ||||
| 
 | ||||
|     patterns = [] | ||||
|     for structure in lib: | ||||
|         pat = Pattern(name=structure.name.decode('ASCII')) | ||||
|         for element in structure: | ||||
|             # Switch based on element type: | ||||
|             if isinstance(element, gdsii.elements.Boundary): | ||||
|                 if use_dtype_as_dose: | ||||
|                     shape = Polygon(vertices=element.xy[:-1], | ||||
|                                     dose=element.data_type, | ||||
|                                     layer=element.layer) | ||||
|                 else: | ||||
|                     shape = Polygon(vertices=element.xy[:-1], | ||||
|                                     layer=(element.layer, element.data_type)) | ||||
|                 if clean_vertices: | ||||
|                     try: | ||||
|                         shape.clean_vertices() | ||||
|                     except PatternError: | ||||
|                         continue | ||||
| 
 | ||||
|                 pat.shapes.append(shape) | ||||
| 
 | ||||
|             if isinstance(element, gdsii.elements.Path): | ||||
|                 if element.path_type == 0: | ||||
|                     extension = 0.0 | ||||
|                 elif element.path_type in (1, 4): | ||||
|                     raise PatternError('Round-ended and custom paths (types 1 and 4) are not implemented yet') | ||||
|                 elif element.path_type == 2: | ||||
|                     extension = element.width / 2 | ||||
|                 else: | ||||
|                     raise PatternError('Unrecognized path type: {}'.format(element.path_type)) | ||||
| 
 | ||||
|                 if element.width == 0: | ||||
|                     continue | ||||
| 
 | ||||
|                 #TODO add extension | ||||
|                 v = remove_colinear_vertices(numpy.array(element.xy, dtype=float), closed_path=False) | ||||
|                 dv = numpy.diff(v, axis=0) | ||||
|                 dvdir = dv / numpy.sqrt((dv * dv).sum(axis=1))[:, None] | ||||
|                 v[0] -= dvdir[0] * extension | ||||
|                 v[-1] += dvdir[-1] * extension | ||||
|                 perp = dvdir[:, ::-1] * [1, -1] * element.width / 2 | ||||
| 
 | ||||
|                 o0 = [v[0] + perp[0]] | ||||
|                 o1 = [v[0] - perp[0]] | ||||
| 
 | ||||
|                 for i in range(1, dv.shape[0]): | ||||
|                     towards_perp = numpy.dot(perp[i - 1], dv[i]) > 0 # bends towards previous perp | ||||
|                     #straight = numpy.dot(perp[i - 1], dv[i]) == 0   # TODO maybe run without cleaning? | ||||
|                     acute = numpy.dot(dv[i - 1], dv[i]) < 0 | ||||
| 
 | ||||
|                     A = numpy.column_stack((dv[i - 1], -dv[i])) | ||||
|                     ab = numpy.linalg.solve(A, v[i] + perp[i] - v[i - 1] - perp[i - 1]) | ||||
|                     cd = numpy.linalg.solve(A, v[i] - perp[i] - v[i - 1] + perp[i - 1]) | ||||
|                     perpside_intersection = v[i - 1] + ab[0] * dv[i - 1] + perp[i - 1] | ||||
|                     otherside_intersection = v[i - 1] + cd[0] * dv[i - 1] - perp[i - 1] | ||||
|                     if towards_perp: | ||||
|                         o0.append(perpside_intersection) | ||||
|                         if acute: | ||||
|                             # Opposite is 180 > angle > 270 | ||||
|                             o1.append(otherside_intersection) | ||||
|                         else: | ||||
|                             # Opposite is >270 | ||||
|                             pt0 = v[i] - perp[i - 1] + dvdir[i - 1] * element.width / 2 | ||||
|                             pt1 = v[i] - perp[i] - dvdir[i] * element.width / 2 | ||||
|                             o1 += [pt0, pt1] | ||||
|                     else: | ||||
|                         # > 180, opposite is <180 | ||||
|                         o1.append(otherside_intersection) | ||||
|                         print('oi', otherside_intersection) | ||||
|                         if acute: | ||||
|                             # > 270, opposite is <90 | ||||
|                             pt0 = v[i] + perp[i - 1] + dvdir[i - 1] * element.width / 2 | ||||
|                             pt1 = v[i] + perp[i] - dvdir[i] * element.width / 2 | ||||
|                             o0 += [pt0, pt1] | ||||
|                         else: | ||||
|                             # 180 > angle >270 | ||||
|                             o0.append(perpside_intersection) | ||||
|                 o0.append(v[-1] + perp[-1]) | ||||
|                 o1.append(v[-1] - perp[-1]) | ||||
|                 verts = numpy.vstack((o0, o1[::-1])) | ||||
| 
 | ||||
|                 if use_dtype_as_dose: | ||||
|                     shape = Polygon(vertices=verts, | ||||
|                                     dose=element.data_type, | ||||
|                                     layer=element.layer) | ||||
|                 else: | ||||
|                     shape = Polygon(vertices=verts, | ||||
|                                     layer=(element.layer, element.data_type)) | ||||
|                 if clean_vertices: | ||||
|                     try: | ||||
|                         shape.clean_vertices() | ||||
|                     except PatternError as err: | ||||
|                         print('error cleaning! {}'.format(err)) | ||||
|                         continue | ||||
| 
 | ||||
|                 pat.shapes.append(shape) | ||||
| 
 | ||||
|             elif isinstance(element, gdsii.elements.Text): | ||||
|                 label = Label(offset=element.xy, | ||||
|                               layer=(element.layer, element.text_type), | ||||
|                               string=element.string.decode('ASCII')) | ||||
|                 pat.labels.append(label) | ||||
| 
 | ||||
|             elif isinstance(element, gdsii.elements.SRef): | ||||
|                 pat.subpatterns.append(_sref_to_subpat(element)) | ||||
| 
 | ||||
|             elif isinstance(element, gdsii.elements.ARef): | ||||
|                 pat.subpatterns.append(_aref_to_gridrep(element)) | ||||
| 
 | ||||
|         patterns.append(pat) | ||||
| 
 | ||||
|     # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries | ||||
|     #  according to the subpattern.ref_name (which is deleted after use). | ||||
|     patterns_dict = dict(((p.name, p) for p in patterns)) | ||||
|     for p in patterns_dict.values(): | ||||
|         for sp in p.subpatterns: | ||||
|             sp.pattern = patterns_dict[sp.ref_name.decode('ASCII')] | ||||
|             del sp.ref_name | ||||
| 
 | ||||
|     return patterns_dict, library_info | ||||
| 
 | ||||
| 
 | ||||
| def _mlayer2gds(mlayer): | ||||
|     if is_scalar(mlayer): | ||||
|         layer = mlayer | ||||
|         data_type = 0 | ||||
|     else: | ||||
|         layer = mlayer[0] | ||||
|         if len(mlayer) > 1: | ||||
|             data_type = mlayer[1] | ||||
|         else: | ||||
|             data_type = 0 | ||||
|     return layer, data_type | ||||
| 
 | ||||
| 
 | ||||
| def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern: | ||||
|     # Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None | ||||
|     #  and sets the instance attribute .ref_name to the struct_name. | ||||
|     # | ||||
|     # BUG: "Absolute" means not affected by parent elements. | ||||
|     #       That's not currently supported by masque at all, so need to either tag it and | ||||
|     #       undo the parent transformations, or implement it in masque. | ||||
|     subpat = SubPattern(pattern=None, offset=element.xy) | ||||
|     subpat.ref_name = element.struct_name | ||||
|     if element.strans is not None: | ||||
|         if element.mag is not None: | ||||
|             subpat.scale = element.mag | ||||
|             # Bit 13 means absolute scale | ||||
|             if get_bit(element.strans, 15 - 13): | ||||
|                 #subpat.offset *= subpat.scale | ||||
|                 raise PatternError('Absolute scale is not implemented yet!') | ||||
|         if element.angle is not None: | ||||
|             subpat.rotation = element.angle * numpy.pi / 180 | ||||
|             # Bit 14 means absolute rotation | ||||
|             if get_bit(element.strans, 15 - 14): | ||||
|                 #subpat.offset = numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.offset) | ||||
|                 raise PatternError('Absolute rotation is not implemented yet!') | ||||
|         # Bit 0 means mirror x-axis | ||||
|         if get_bit(element.strans, 15 - 0): | ||||
|             subpat.mirror(axis=0) | ||||
|     return subpat | ||||
| 
 | ||||
| 
 | ||||
| def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: | ||||
|     # Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None | ||||
|     #  and sets the instance attribute .ref_name to the struct_name. | ||||
|     # | ||||
|     # BUG: "Absolute" means not affected by parent elements. | ||||
|     #       That's not currently supported by masque at all, so need to either tag it and | ||||
|     #       undo the parent transformations, or implement it in masque.i | ||||
| 
 | ||||
|     rotation = 0 | ||||
|     offset = numpy.array(element.xy[0]) | ||||
|     scale = 1 | ||||
|     mirror_signs = numpy.ones(2) | ||||
| 
 | ||||
|     if element.strans is not None: | ||||
|         if element.mag is not None: | ||||
|             scale = element.mag | ||||
|             # Bit 13 means absolute scale | ||||
|             if get_bit(element.strans, 15 - 13): | ||||
|                 raise PatternError('Absolute scale is not implemented yet!') | ||||
|         if element.angle is not None: | ||||
|             rotation = element.angle * numpy.pi / 180 | ||||
|             # Bit 14 means absolute rotation | ||||
|             if get_bit(element.strans, 15 - 14): | ||||
|                 raise PatternError('Absolute rotation is not implemented yet!') | ||||
|         # Bit 0 means mirror x-axis | ||||
|         if get_bit(element.strans, 15 - 0): | ||||
|             mirror_signs[0] = -1 | ||||
| 
 | ||||
|     counts = [element.cols, element.rows] | ||||
|     vec_a0 = element.xy[1] - offset | ||||
|     vec_b0 = element.xy[2] - offset | ||||
| 
 | ||||
|     a_vector = numpy.dot(rotation_matrix_2d(-rotation), vec_a0 / scale / counts[0]) * mirror_signs | ||||
|     b_vector = numpy.dot(rotation_matrix_2d(-rotation), vec_b0 / scale / counts[1]) * mirror_signs | ||||
| 
 | ||||
| 
 | ||||
|     gridrep = GridRepetition(pattern=None, | ||||
|                             a_vector=a_vector, | ||||
|                             b_vector=b_vector, | ||||
|                             a_count=counts[0], | ||||
|                             b_count=counts[1], | ||||
|                             offset=offset, | ||||
|                             rotation=rotation, | ||||
|                             scale=scale, | ||||
|                             mirrored=(mirror_signs == -1)) | ||||
|     gridrep.ref_name = element.struct_name | ||||
| 
 | ||||
|     return gridrep | ||||
| 
 | ||||
| 
 | ||||
| def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] | ||||
|                         ) -> List[gdsii.elements.ARef or gdsii.elements.SRef]: | ||||
|     #  strans must be set for angle and mag to take effect | ||||
|     refs = [] | ||||
|     for subpat in subpatterns: | ||||
|         encoded_name = subpat.pattern.name | ||||
| 
 | ||||
|         if isinstance(subpat, GridRepetition): | ||||
|             mirror_signs = (-1) ** numpy.array(subpat.mirrored) | ||||
|             xy = numpy.array(subpat.offset) + [ | ||||
|                   [0, 0], | ||||
|                   numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.a_vector * mirror_signs) * subpat.scale * subpat.a_count, | ||||
|                   numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.b_vector * mirror_signs) * subpat.scale * subpat.b_count, | ||||
|                  ] | ||||
|             ref = gdsii.elements.ARef(struct_name=encoded_name, | ||||
|                                        xy=numpy.round(xy).astype(int), | ||||
|                                        cols=numpy.round(subpat.a_count).astype(int), | ||||
|                                        rows=numpy.round(subpat.b_count).astype(int)) | ||||
|         else: | ||||
|             ref = gdsii.elements.SRef(struct_name=encoded_name, | ||||
|                                       xy=numpy.round([subpat.offset]).astype(int)) | ||||
| 
 | ||||
|         ref.strans = 0 | ||||
|         ref.angle = subpat.rotation * 180 / numpy.pi | ||||
|         mirror_x, mirror_y = subpat.mirrored | ||||
|         if mirror_x and mirror_y: | ||||
|             ref.angle += 180 | ||||
|         elif mirror_x: | ||||
|             ref.strans = set_bit(ref.strans, 15 - 0, True) | ||||
|         elif mirror_y: | ||||
|             ref.angle += 180 | ||||
|             ref.strans = set_bit(ref.strans, 15 - 0, True) | ||||
|         ref.angle %= 360 | ||||
|         ref.mag = subpat.scale | ||||
| 
 | ||||
|         refs.append(ref) | ||||
|     return refs | ||||
| 
 | ||||
| 
 | ||||
| def _shapes_to_boundaries(shapes: List[Shape] | ||||
|                          ) -> List[gdsii.elements.Boundary]: | ||||
|     # Add a Boundary element for each shape | ||||
|     boundaries = [] | ||||
|     for shape in shapes: | ||||
|         layer, data_type = _mlayer2gds(shape.layer) | ||||
|         for polygon in shape.to_polygons(): | ||||
|             xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) | ||||
|             xy_closed = numpy.vstack((xy_open, xy_open[0, :])) | ||||
|             boundaries.append(gdsii.elements.Boundary(layer=layer, | ||||
|                                                       data_type=data_type, | ||||
|                                                       xy=xy_closed)) | ||||
|     return boundaries | ||||
| 
 | ||||
| 
 | ||||
| def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]: | ||||
|     texts = [] | ||||
|     for label in labels: | ||||
|         layer, text_type = _mlayer2gds(label.layer) | ||||
|         xy = numpy.round([label.offset]).astype(int) | ||||
|         texts.append(gdsii.elements.Text(layer=layer, | ||||
|                                          text_type=text_type, | ||||
|                                          xy=xy, | ||||
|                                          string=label.string.encode('ASCII'))) | ||||
|     return texts | ||||
| 
 | ||||
| 
 | ||||
| def _disambiguate_pattern_names(patterns): | ||||
|     used_names = [] | ||||
|     for pat in patterns: | ||||
|         sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name) | ||||
| 
 | ||||
|         i = 0 | ||||
|         suffixed_name = sanitized_name | ||||
|         while suffixed_name in used_names or suffixed_name == '': | ||||
|             suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII') | ||||
| 
 | ||||
|             suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A') | ||||
|             i += 1 | ||||
| 
 | ||||
|         if sanitized_name == '': | ||||
|             logger.warning('Empty pattern name saved as "{}"'.format(suffixed_name)) | ||||
|         elif suffixed_name != sanitized_name: | ||||
|             logger.warning('Pattern name "{}" appears multiple times; renaming to "{}"'.format(pat.name, suffixed_name)) | ||||
| 
 | ||||
|         encoded_name = suffixed_name.encode('ASCII') | ||||
|         if len(encoded_name) == 0: | ||||
|             # Should never happen since zero-length names are replaced | ||||
|             raise PatternError('Zero-length name after sanitize+encode, originally "{}"'.format(pat.name)) | ||||
|         if len(encoded_name) > 32: | ||||
|             raise PatternError('Pattern name "{}" length > 32 after encode, originally "{}"'.format(encoded_name, pat.name)) | ||||
| 
 | ||||
|         pat.name = encoded_name | ||||
|         used_names.append(suffixed_name) | ||||
							
								
								
									
										139
									
								
								masque/file/svg.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								masque/file/svg.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,139 @@ | ||||
| """ | ||||
| SVG file format readers and writers | ||||
| """ | ||||
| 
 | ||||
| import svgwrite | ||||
| import numpy | ||||
| 
 | ||||
| from .utils import mangle_name | ||||
| from .. import Pattern | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| def write(pattern: Pattern, | ||||
|           filename: str, | ||||
|           custom_attributes: bool=False): | ||||
|     """ | ||||
|     Write a Pattern to an SVG file, by first calling .polygonize() on it | ||||
|      to change the shapes into polygons, and then writing patterns as SVG | ||||
|      groups (<g>, inside <defs>), polygons as paths (<path>), and subpatterns | ||||
|      as <use> elements. | ||||
| 
 | ||||
|     Note that this function modifies the Pattern. | ||||
| 
 | ||||
|     If custom_attributes is True, non-standard pattern_layer and pattern_dose attributes | ||||
|      are written to the relevant elements. | ||||
| 
 | ||||
|     It is often a good idea to run pattern.subpatternize() on pattern prior to | ||||
|      calling this function, especially if calling .polygonize() will result in very | ||||
|      many vertices. | ||||
| 
 | ||||
|     If you want pattern polygonized with non-default arguments, just call pattern.polygonize() | ||||
|      prior to calling this function. | ||||
| 
 | ||||
|     :param pattern: Pattern to write to file. Modified by this function. | ||||
|     :param filename: Filename to write to. | ||||
|     :param custom_attributes: Whether to write non-standard pattern_layer and | ||||
|             pattern_dose attributes to the SVG elements. | ||||
|     """ | ||||
| 
 | ||||
|     # Polygonize pattern | ||||
|     pattern.polygonize() | ||||
| 
 | ||||
|     [bounds_min, bounds_max] = pattern.get_bounds() | ||||
| 
 | ||||
|     viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2)) | ||||
|     viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox) | ||||
| 
 | ||||
|     # Create file | ||||
|     svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string, | ||||
|                            debug=(not custom_attributes)) | ||||
| 
 | ||||
|     # Get a dict of id(pattern) -> pattern | ||||
|     patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} | ||||
| 
 | ||||
|     # Now create a group for each row in sd_table (ie, each pattern + dose combination) | ||||
|     #  and add in any Boundary and Use elements | ||||
|     for pat in patterns_by_id.values(): | ||||
|         svg_group = svg.g(id=mangle_name(pat), fill='blue', stroke='red') | ||||
| 
 | ||||
|         for shape in pat.shapes: | ||||
|             for polygon in shape.to_polygons(): | ||||
|                 path_spec = poly2path(polygon.vertices + polygon.offset) | ||||
| 
 | ||||
|                 path = svg.path(d=path_spec) | ||||
|                 if custom_attributes: | ||||
|                     path['pattern_layer'] = polygon.layer | ||||
|                     path['pattern_dose'] = polygon.dose | ||||
| 
 | ||||
|                 svg_group.add(path) | ||||
| 
 | ||||
|         for subpat in pat.subpatterns: | ||||
|             transform = 'scale({:g}) rotate({:g}) translate({:g},{:g})'.format( | ||||
|                 subpat.scale, subpat.rotation, subpat.offset[0], subpat.offset[1]) | ||||
|             use = svg.use(href='#' + mangle_name(subpat.pattern), transform=transform) | ||||
|             if custom_attributes: | ||||
|                 use['pattern_dose'] = subpat.dose | ||||
|             svg_group.add(use) | ||||
| 
 | ||||
|         svg.defs.add(svg_group) | ||||
|     svg.add(svg.use(href='#' + mangle_name(pattern))) | ||||
|     svg.save() | ||||
| 
 | ||||
| 
 | ||||
| def write_inverted(pattern: Pattern, filename: str): | ||||
|     """ | ||||
|     Write an inverted Pattern to an SVG file, by first calling .polygonize() and | ||||
|      .flatten() on it to change the shapes into polygons, then drawing a bounding | ||||
|      box and drawing the polygons with reverse vertex order inside it, all within | ||||
|      one <path> element. | ||||
| 
 | ||||
|     Note that this function modifies the Pattern. | ||||
| 
 | ||||
|     If you want pattern polygonized with non-default arguments, just call pattern.polygonize() | ||||
|      prior to calling this function. | ||||
| 
 | ||||
|     :param pattern: Pattern to write to file. Modified by this function. | ||||
|     :param filename: Filename to write to. | ||||
|     """ | ||||
|     # Polygonize and flatten pattern | ||||
|     pattern.polygonize().flatten() | ||||
| 
 | ||||
|     [bounds_min, bounds_max] = pattern.get_bounds() | ||||
| 
 | ||||
|     viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2)) | ||||
|     viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox) | ||||
| 
 | ||||
|     # Create file | ||||
|     svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string) | ||||
| 
 | ||||
|     # Draw bounding box | ||||
|     slab_edge = [[bounds_min[0] - 1, bounds_max[1] + 1], | ||||
|                  [bounds_max[0] + 1, bounds_max[1] + 1], | ||||
|                  [bounds_max[0] + 1, bounds_min[1] - 1], | ||||
|                  [bounds_min[0] - 1, bounds_min[1] - 1]] | ||||
|     path_spec = poly2path(slab_edge) | ||||
| 
 | ||||
|     # Draw polygons with reversed vertex order | ||||
|     for shape in pattern.shapes: | ||||
|         for polygon in shape.to_polygons(): | ||||
|             path_spec += poly2path(polygon.vertices[::-1] + polygon.offset) | ||||
| 
 | ||||
|     svg.add(svg.path(d=path_spec, fill='blue', stroke='red')) | ||||
|     svg.save() | ||||
| 
 | ||||
| 
 | ||||
| def poly2path(vertices: numpy.ndarray) -> str: | ||||
|     """ | ||||
|     Create an SVG path string from an Nx2 list of vertices. | ||||
| 
 | ||||
|     :param vertices: Nx2 array of vertices. | ||||
|     :return: SVG path-string. | ||||
|     """ | ||||
|     commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1]) | ||||
|     for vertex in vertices[1:]: | ||||
|         commands += 'L{:g},{:g}'.format(vertex[0], vertex[1]) | ||||
|     commands += ' Z   ' | ||||
|     return commands | ||||
							
								
								
									
										42
									
								
								masque/file/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								masque/file/utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| """ | ||||
| Helper functions for file reading and writing | ||||
| """ | ||||
| import re | ||||
| from typing import Set, Tuple, List | ||||
| 
 | ||||
| from masque.pattern import Pattern | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: | ||||
|     """ | ||||
|     Create a name using pattern.name, id(pattern), and the dose multiplier. | ||||
| 
 | ||||
|     :param pattern: Pattern whose name we want to mangle. | ||||
|     :param dose_multiplier: Dose multiplier to mangle with. | ||||
|     :return: Mangled name. | ||||
|     """ | ||||
|     expression = re.compile('[^A-Za-z0-9_\?\$]') | ||||
|     full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern)) | ||||
|     sanitized_name = expression.sub('_', full_name) | ||||
|     return sanitized_name | ||||
| 
 | ||||
| 
 | ||||
| def make_dose_table(patterns: List[Pattern], dose_multiplier: float=1.0) -> Set[Tuple[int, float]]: | ||||
|     """ | ||||
|     Create a set containing (id(pat), written_dose) for each pattern (including subpatterns) | ||||
| 
 | ||||
|     :param pattern: Source Patterns. | ||||
|     :param dose_multiplier: Multiplier for all written_dose entries. | ||||
|     :return: {(id(subpat.pattern), written_dose), ...} | ||||
|     """ | ||||
|     dose_table = {(id(pattern), dose_multiplier) for pattern in patterns} | ||||
|     for pattern in patterns: | ||||
|         for subpat in pattern.subpatterns: | ||||
|             subpat_dose_entry = (id(subpat.pattern), subpat.dose * dose_multiplier) | ||||
|             if subpat_dose_entry not in dose_table: | ||||
|                 subpat_dose_table = make_dose_table([subpat.pattern], subpat.dose * dose_multiplier) | ||||
|                 dose_table = dose_table.union(subpat_dose_table) | ||||
|     return dose_table | ||||
							
								
								
									
										129
									
								
								masque/label.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								masque/label.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,129 @@ | ||||
| from typing import List, Tuple | ||||
| import copy | ||||
| import numpy | ||||
| from numpy import pi | ||||
| 
 | ||||
| from . import PatternError | ||||
| from .utils import is_scalar, vector2, rotation_matrix_2d | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| class Label: | ||||
|     """ | ||||
|     A circle, which has a position and radius. | ||||
|     """ | ||||
| 
 | ||||
|     # [x_offset, y_offset] | ||||
|     _offset = numpy.array([0.0, 0.0])   # type: numpy.ndarray | ||||
| 
 | ||||
|     # Layer (integer >= 0) | ||||
|     _layer = 0                          # type: int or Tuple | ||||
| 
 | ||||
|     # Label string | ||||
|     _string = None                      # type: str | ||||
| 
 | ||||
|     # ---- Properties | ||||
|     # offset property | ||||
|     @property | ||||
|     def offset(self) -> numpy.ndarray: | ||||
|         """ | ||||
|         [x, y] offset | ||||
| 
 | ||||
|         :return: [x_offset, y_offset] | ||||
|         """ | ||||
|         return self._offset | ||||
| 
 | ||||
|     @offset.setter | ||||
|     def offset(self, val: vector2): | ||||
|         if not isinstance(val, numpy.ndarray): | ||||
|             val = numpy.array(val, dtype=float) | ||||
| 
 | ||||
|         if val.size != 2: | ||||
|             raise PatternError('Offset must be convertible to size-2 ndarray') | ||||
|         self._offset = val.flatten() | ||||
| 
 | ||||
|     # layer property | ||||
|     @property | ||||
|     def layer(self) -> int or Tuple[int]: | ||||
|         """ | ||||
|         Layer number (int or tuple of ints) | ||||
| 
 | ||||
|         :return: Layer | ||||
|         """ | ||||
|         return self._layer | ||||
| 
 | ||||
|     @layer.setter | ||||
|     def layer(self, val: int or List[int]): | ||||
|         self._layer = val | ||||
| 
 | ||||
|     # string property | ||||
|     @property | ||||
|     def string(self) -> str: | ||||
|         """ | ||||
|         Label string (str) | ||||
| 
 | ||||
|         :return: string | ||||
|         """ | ||||
|         return self._string | ||||
| 
 | ||||
|     @string.setter | ||||
|     def string(self, val: str): | ||||
|         self._string = val | ||||
| 
 | ||||
|     def __init__(self, | ||||
|                  string: str, | ||||
|                  offset: vector2=(0.0, 0.0), | ||||
|                  layer: int=0): | ||||
|         self.string = string | ||||
|         self.offset = numpy.array(offset, dtype=float) | ||||
|         self.layer = layer | ||||
| 
 | ||||
| 
 | ||||
|     # ---- Non-abstract methods | ||||
|     def copy(self) -> 'Label': | ||||
|         """ | ||||
|         Returns a deep copy of the shape. | ||||
| 
 | ||||
|         :return: Deep copy of self | ||||
|         """ | ||||
|         return copy.deepcopy(self) | ||||
| 
 | ||||
|     def translate(self, offset: vector2) -> 'Label': | ||||
|         """ | ||||
|         Translate the shape by the given offset | ||||
| 
 | ||||
|         :param offset: [x_offset, y,offset] | ||||
|         :return: self | ||||
|         """ | ||||
|         self.offset += offset | ||||
|         return self | ||||
| 
 | ||||
|     def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': | ||||
|         """ | ||||
|         Rotate the shape around a point. | ||||
| 
 | ||||
|         :param pivot: Point (x, y) to rotate around | ||||
|         :param rotation: Angle to rotate by (counterclockwise, radians) | ||||
|         :return: self | ||||
|         """ | ||||
|         pivot = numpy.array(pivot, dtype=float) | ||||
|         self.translate(-pivot) | ||||
|         self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) | ||||
|         self.translate(+pivot) | ||||
|         return self | ||||
| 
 | ||||
|     def get_bounds(self) -> numpy.ndarray: | ||||
|         """ | ||||
|         Return the bounds of the label. | ||||
| 
 | ||||
|         Labels are assumed to take up 0 area, i.e. | ||||
|         bounds = [self.offset, | ||||
|                   self.offset] | ||||
| 
 | ||||
|         :return: Bounds [[xmin, xmax], [ymin, ymax]] | ||||
|         """ | ||||
|         return numpy.array([self.offset, self.offset]) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										539
									
								
								masque/pattern.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										539
									
								
								masque/pattern.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,539 @@ | ||||
| """ | ||||
|  Base object for containing a lithography mask. | ||||
| """ | ||||
| 
 | ||||
| from typing import List, Callable, Tuple, Dict, Union | ||||
| import copy | ||||
| import itertools | ||||
| import pickle | ||||
| from collections import defaultdict | ||||
| 
 | ||||
| import numpy | ||||
| # .visualize imports matplotlib and matplotlib.collections | ||||
| 
 | ||||
| from .subpattern import SubPattern | ||||
| from .repetition import GridRepetition | ||||
| from .shapes import Shape, Polygon | ||||
| from .label import Label | ||||
| from .utils import rotation_matrix_2d, vector2 | ||||
| from .error import PatternError | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| class Pattern: | ||||
|     """ | ||||
|     2D layout consisting of some set of shapes and references to other Pattern objects | ||||
|      (via SubPattern). Shapes are assumed to inherit from .shapes.Shape or provide equivalent | ||||
|      functions. | ||||
| 
 | ||||
|     :var shapes: List of all shapes in this Pattern. Elements in this list are assumed to inherit | ||||
|             from Shape or provide equivalent functions. | ||||
|     :var subpatterns: List of all SubPattern objects in this Pattern. Multiple SubPattern objects | ||||
|             may reference the same Pattern object. | ||||
|     :var name: An identifier for this object. Not necessarily unique. | ||||
|     """ | ||||
|     shapes = None               # type: List[Shape] | ||||
|     labels = None               # type: List[Labels] | ||||
|     subpatterns = None          # type: List[SubPattern or GridRepetition] | ||||
|     name = None                 # type: str | ||||
| 
 | ||||
|     def __init__(self, | ||||
|                  shapes: List[Shape]=(), | ||||
|                  labels: List[Label]=(), | ||||
|                  subpatterns: List[SubPattern]=(), | ||||
|                  name: str='', | ||||
|                  ): | ||||
|         """ | ||||
|         Basic init; arguments get assigned to member variables. | ||||
|          Non-list inputs for shapes and subpatterns get converted to lists. | ||||
| 
 | ||||
|         :param shapes: Initial shapes in the Pattern | ||||
|         :param labels: Initial labels in the Pattern | ||||
|         :param subpatterns: Initial subpatterns in the Pattern | ||||
|         :param name: An identifier for the Pattern | ||||
|         """ | ||||
|         if isinstance(shapes, list): | ||||
|             self.shapes = shapes | ||||
|         else: | ||||
|             self.shapes = list(shapes) | ||||
| 
 | ||||
|         if isinstance(labels, list): | ||||
|             self.labels = labels | ||||
|         else: | ||||
|             self.labels = list(labels) | ||||
| 
 | ||||
|         if isinstance(subpatterns, list): | ||||
|             self.subpatterns = subpatterns | ||||
|         else: | ||||
|             self.subpatterns = list(subpatterns) | ||||
| 
 | ||||
|         self.name = name | ||||
| 
 | ||||
|     def append(self, other_pattern: 'Pattern') -> 'Pattern': | ||||
|         """ | ||||
|         Appends all shapes, labels and subpatterns from other_pattern to self's shapes, | ||||
|           labels, and supbatterns. | ||||
| 
 | ||||
|         :param other_pattern: The Pattern to append | ||||
|         :return: self | ||||
|         """ | ||||
|         self.subpatterns += other_pattern.subpatterns | ||||
|         self.shapes += other_pattern.shapes | ||||
|         self.labels += other_pattern.labels | ||||
|         return self | ||||
| 
 | ||||
|     def subset(self, | ||||
|                shapes_func: Callable[[Shape], bool]=None, | ||||
|                labels_func: Callable[[Label], bool]=None, | ||||
|                subpatterns_func: Callable[[SubPattern], bool]=None, | ||||
|                recursive: bool=False, | ||||
|                ) -> 'Pattern': | ||||
|         """ | ||||
|         Returns a Pattern containing only the entities (e.g. shapes) for which the | ||||
|           given entity_func returns True. | ||||
|         Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied. | ||||
| 
 | ||||
|         :param shapes_func: Given a shape, returns a boolean denoting whether the shape is a member | ||||
|              of the subset. Default always returns False. | ||||
|         :param labels_func: Given a label, returns a boolean denoting whether the label is a member | ||||
|              of the subset. Default always returns False. | ||||
|         :param subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member | ||||
|              of the subset. Default always returns False. | ||||
|         :param recursive: If True, also calls .subset() recursively on patterns referenced by this | ||||
|              pattern. | ||||
|         :return: A Pattern containing all the shapes and subpatterns for which the parameter | ||||
|              functions return True | ||||
|         """ | ||||
|         def do_subset(src): | ||||
|             pat = Pattern(name=src.name) | ||||
|             if shapes_func is not None: | ||||
|                 pat.shapes = [s for s in src.shapes if shapes_func(s)] | ||||
|             if labels_func is not None: | ||||
|                 pat.labels = [s for s in src.labels if labels_func(s)] | ||||
|             if subpatterns_func is not None: | ||||
|                 pat.subpatterns = [s for s in src.subpatterns if subpatterns_func(s)] | ||||
|             return pat | ||||
| 
 | ||||
|         if recursive: | ||||
|             pat = self.apply(do_subset) | ||||
|         else: | ||||
|             pat = do_subset(self) | ||||
|         return pat | ||||
| 
 | ||||
|     def apply(self, | ||||
|               func: Callable[['Pattern'], 'Pattern'], | ||||
|               memo: Dict[int, 'Pattern']=None, | ||||
|               ) -> 'Pattern': | ||||
|         """ | ||||
|         Recursively apply func() to this pattern and any pattern it references. | ||||
|         func() is expected to take and return a Pattern. | ||||
|         func() is first applied to the pattern as a whole, then any referenced patterns. | ||||
|         It is only applied to any given pattern once, regardless of how many times it is | ||||
|             referenced. | ||||
| 
 | ||||
|         :param func: Function which accepts a Pattern, and returns a pattern. | ||||
|         :param memo: Dictionary used to avoid re-running on multiply-referenced patterns. | ||||
|             Stores {id(pattern): func(pattern)} for patterns which have already been processed. | ||||
|             Default None (no already-processed patterns). | ||||
|         :return: The result of applying func() to this pattern and all subpatterns. | ||||
|         :raises: PatternError if called on a pattern containing a circular reference. | ||||
|         """ | ||||
|         if memo is None: | ||||
|             memo = {} | ||||
| 
 | ||||
|         pat_id = id(self) | ||||
|         if pat_id not in memo: | ||||
|             memo[pat_id] = None | ||||
|             pat = func(self) | ||||
|             for subpat in pat.subpatterns: | ||||
|                 subpat.pattern = subpat.pattern.apply(func, memo) | ||||
|             memo[pat_id] = pat | ||||
|         elif memo[pat_id] is None: | ||||
|             raise PatternError('.apply() called on pattern with circular reference') | ||||
|         else: | ||||
|             pat = memo[pat_id] | ||||
|         return pat | ||||
| 
 | ||||
|     def polygonize(self, | ||||
|                    poly_num_points: int=None, | ||||
|                    poly_max_arclen: float=None | ||||
|                    ) -> 'Pattern': | ||||
|         """ | ||||
|         Calls .to_polygons(...) on all the shapes in this Pattern and any referenced patterns, | ||||
|          replacing them with the returned polygons. | ||||
|         Arguments are passed directly to shape.to_polygons(...). | ||||
| 
 | ||||
|         :param poly_num_points: Number of points to use for each polygon. Can be overridden by | ||||
|              poly_max_arclen if that results in more points. Optional, defaults to shapes' | ||||
|              internal defaults. | ||||
|         :param poly_max_arclen: Maximum arclength which can be approximated by a single line | ||||
|              segment. Optional, defaults to shapes' internal defaults. | ||||
|         :return: self | ||||
|         """ | ||||
|         old_shapes = self.shapes | ||||
|         self.shapes = list(itertools.chain.from_iterable( | ||||
|                         (shape.to_polygons(poly_num_points, poly_max_arclen) | ||||
|                          for shape in old_shapes))) | ||||
|         for subpat in self.subpatterns: | ||||
|             subpat.pattern.polygonize(poly_num_points, poly_max_arclen) | ||||
|         return self | ||||
| 
 | ||||
|     def manhattanize(self, | ||||
|                      grid_x: numpy.ndarray, | ||||
|                      grid_y: numpy.ndarray | ||||
|                      ) -> 'Pattern': | ||||
|         """ | ||||
|         Calls .polygonize() and .flatten on the pattern, then calls .manhattanize() on all the | ||||
|          resulting shapes, replacing them with the returned Manhattan polygons. | ||||
| 
 | ||||
|         :param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. | ||||
|         :param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. | ||||
|         :return: self | ||||
|         """ | ||||
| 
 | ||||
|         self.polygonize().flatten() | ||||
|         old_shapes = self.shapes | ||||
|         self.shapes = list(itertools.chain.from_iterable( | ||||
|                         (shape.manhattanize(grid_x, grid_y) for shape in old_shapes))) | ||||
|         return self | ||||
| 
 | ||||
|     def subpatternize(self, | ||||
|                       recursive: bool=True, | ||||
|                       norm_value: int=1e6, | ||||
|                       exclude_types: Tuple[Shape]=(Polygon,) | ||||
|                       ) -> 'Pattern': | ||||
|         """ | ||||
|         Iterates through this Pattern and all referenced Patterns. Within each Pattern, it iterates | ||||
|          over all shapes, calling .normalized_form(norm_value) on them to retrieve a scale-, | ||||
|          offset-, dose-, and rotation-independent form. Each shape whose normalized form appears | ||||
|          more than once is removed and re-added using subpattern objects referencing a newly-created | ||||
|          Pattern containing only the normalized form of the shape. | ||||
| 
 | ||||
|         Note that the default norm_value was chosen to give a reasonable precision when converting | ||||
|          to GDSII, which uses integer values for pixel coordinates. | ||||
| 
 | ||||
|         :param recursive: Whether to call recursively on self's subpatterns. Default True. | ||||
|         :param norm_value: Passed to shape.normalized_form(norm_value). Default 1e6 (see function | ||||
|                 note about GDSII) | ||||
|         :param exclude_types: Shape types passed in this argument are always left untouched, for | ||||
|                 speed or convenience. Default: (Shapes.Polygon,) | ||||
|         :return: self | ||||
|         """ | ||||
| 
 | ||||
|         if exclude_types is None: | ||||
|             exclude_types = () | ||||
| 
 | ||||
|         if recursive: | ||||
|             for subpat in self.subpatterns: | ||||
|                 subpat.pattern.subpatternize(recursive=True, | ||||
|                                              norm_value=norm_value, | ||||
|                                              exclude_types=exclude_types) | ||||
| 
 | ||||
|         # Create a dict which uses the label tuple from .normalized_form() as a key, and which | ||||
|         #  stores (function_to_create_normalized_shape, [(index_in_shapes, values), ...]), where | ||||
|         #  values are the (offset, scale, rotation, dose) values as calculated by .normalized_form() | ||||
|         shape_table = defaultdict(lambda: [None, list()]) | ||||
|         for i, shape in enumerate(self.shapes): | ||||
|             if not any((isinstance(shape, t) for t in exclude_types)): | ||||
|                 label, values, func = shape.normalized_form(norm_value) | ||||
|                 shape_table[label][0] = func | ||||
|                 shape_table[label][1].append((i, values)) | ||||
| 
 | ||||
|         # Iterate over the normalized shapes in the table. If any normalized shape occurs more than | ||||
|         #  once, create a Pattern holding a normalized shape object, and add self.subpatterns | ||||
|         #  entries for each occurrence in self. Also, note down that we should delete the | ||||
|         #  self.shapes entries for which we made SubPatterns. | ||||
|         shapes_to_remove = [] | ||||
|         for label in shape_table: | ||||
|             if len(shape_table[label][1]) > 1: | ||||
|                 shape = shape_table[label][0]() | ||||
|                 pat = Pattern(shapes=[shape]) | ||||
| 
 | ||||
|                 for i, values in shape_table[label][1]: | ||||
|                     (offset, scale, rotation, dose) = values | ||||
|                     subpat = SubPattern(pattern=pat, offset=offset, scale=scale, | ||||
|                                         rotation=rotation, dose=dose) | ||||
|                     self.subpatterns.append(subpat) | ||||
|                     shapes_to_remove.append(i) | ||||
| 
 | ||||
|         # Remove any shapes for which we have created subpatterns. | ||||
|         for i in sorted(shapes_to_remove, reverse=True): | ||||
|             del self.shapes[i] | ||||
| 
 | ||||
|         return self | ||||
| 
 | ||||
|     def as_polygons(self) -> List[numpy.ndarray]: | ||||
|         """ | ||||
|         Represents the pattern as a list of polygons. | ||||
| 
 | ||||
|         Deep-copies the pattern, then calls .polygonize() and .flatten() on the copy in order to | ||||
|          generate the list of polygons. | ||||
| 
 | ||||
|         :return: A list of (Ni, 2) numpy.ndarrays specifying vertices of the polygons. Each ndarray | ||||
|             is of the form [[x0, y0], [x1, y1],...]. | ||||
|         """ | ||||
|         pat = copy.deepcopy(self).polygonize().flatten() | ||||
|         return [shape.vertices + shape.offset for shape in pat.shapes] | ||||
| 
 | ||||
|     def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']: | ||||
|         """ | ||||
|         Create a dictionary of {id(pat): pat} for all Pattern objects referenced by this | ||||
|          Pattern (operates recursively on all referenced Patterns as well) | ||||
| 
 | ||||
|         :return: Dictionary of {id(pat): pat} for all referenced Pattern objects | ||||
|         """ | ||||
|         ids = {} | ||||
|         for subpat in self.subpatterns: | ||||
|             if id(subpat.pattern) not in ids: | ||||
|                 ids[id(subpat.pattern)] = subpat.pattern | ||||
|                 ids.update(subpat.pattern.referenced_patterns_by_id()) | ||||
|         return ids | ||||
| 
 | ||||
|     def get_bounds(self) -> Union[numpy.ndarray, None]: | ||||
|         """ | ||||
|         Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the | ||||
|          extent of the Pattern's contents in each dimension. | ||||
|         Returns None if the Pattern is empty. | ||||
| 
 | ||||
|         :return: [[x_min, y_min], [x_max, y_max]] or None | ||||
|         """ | ||||
|         entries = self.shapes + self.subpatterns + self.labels | ||||
|         if not entries: | ||||
|             return None | ||||
| 
 | ||||
|         init_bounds = entries[0].get_bounds() | ||||
|         min_bounds = init_bounds[0, :] | ||||
|         max_bounds = init_bounds[1, :] | ||||
|         for entry in entries[1:]: | ||||
|             bounds = entry.get_bounds() | ||||
|             min_bounds = numpy.minimum(min_bounds, bounds[0, :]) | ||||
|             max_bounds = numpy.maximum(max_bounds, bounds[1, :]) | ||||
|         return numpy.vstack((min_bounds, max_bounds)) | ||||
| 
 | ||||
|     def flatten(self) -> 'Pattern': | ||||
|         """ | ||||
|         Removes all subpatterns and adds equivalent shapes. | ||||
| 
 | ||||
|         :return: self | ||||
|         """ | ||||
|         subpatterns = copy.deepcopy(self.subpatterns) | ||||
|         self.subpatterns = [] | ||||
|         for subpat in subpatterns: | ||||
|             subpat.pattern.flatten() | ||||
|             p = subpat.as_pattern() | ||||
|             self.shapes += p.shapes | ||||
|             self.labels += p.labels | ||||
|         return self | ||||
| 
 | ||||
|     def translate_elements(self, offset: vector2) -> 'Pattern': | ||||
|         """ | ||||
|         Translates all shapes, label, and subpatterns by the given offset. | ||||
| 
 | ||||
|         :param offset: Offset to translate by | ||||
|         :return: self | ||||
|         """ | ||||
|         for entry in self.shapes + self.subpatterns + self.labels: | ||||
|             entry.translate(offset) | ||||
|         return self | ||||
| 
 | ||||
|     def scale_elements(self, scale: float) -> 'Pattern': | ||||
|         """" | ||||
|         Scales all shapes and subpatterns by the given value. | ||||
| 
 | ||||
|         :param scale: value to scale by | ||||
|         :return: self | ||||
|         """ | ||||
|         for entry in self.shapes + self.subpatterns: | ||||
|             entry.scale(scale) | ||||
|         return self | ||||
| 
 | ||||
|     def scale_by(self, c: float) -> 'Pattern': | ||||
|         """ | ||||
|         Scale this Pattern by the given value | ||||
|          (all shapes and subpatterns and their offsets are scaled) | ||||
| 
 | ||||
|         :param c: value to scale by | ||||
|         :return: self | ||||
|         """ | ||||
|         for entry in self.shapes + self.subpatterns: | ||||
|             entry.offset *= c | ||||
|             entry.scale_by(c) | ||||
|         return self | ||||
| 
 | ||||
|     def rotate_around(self, pivot: vector2, rotation: float) -> 'Pattern': | ||||
|         """ | ||||
|         Rotate the Pattern around the a location. | ||||
| 
 | ||||
|         :param pivot: Location to rotate around | ||||
|         :param rotation: Angle to rotate by (counter-clockwise, radians) | ||||
|         :return: self | ||||
|         """ | ||||
|         pivot = numpy.array(pivot) | ||||
|         self.translate_elements(-pivot) | ||||
|         self.rotate_elements(rotation) | ||||
|         self.rotate_element_centers(rotation) | ||||
|         self.translate_elements(+pivot) | ||||
|         return self | ||||
| 
 | ||||
|     def rotate_element_centers(self, rotation: float) -> 'Pattern': | ||||
|         """ | ||||
|         Rotate the offsets of all shapes, labels, and subpatterns around (0, 0) | ||||
| 
 | ||||
|         :param rotation: Angle to rotate by (counter-clockwise, radians) | ||||
|         :return: self | ||||
|         """ | ||||
|         for entry in self.shapes + self.subpatterns + self.labels: | ||||
|             entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset) | ||||
|         return self | ||||
| 
 | ||||
|     def rotate_elements(self, rotation: float) -> 'Pattern': | ||||
|         """ | ||||
|         Rotate each shape and subpattern around its center (offset) | ||||
| 
 | ||||
|         :param rotation: Angle to rotate by (counter-clockwise, radians) | ||||
|         :return: self | ||||
|         """ | ||||
|         for entry in self.shapes + self.subpatterns: | ||||
|             entry.rotate(rotation) | ||||
|         return self | ||||
| 
 | ||||
|     def mirror_element_centers(self, axis: int) -> 'Pattern': | ||||
|         """ | ||||
|         Mirror the offsets of all shapes, labels, and subpatterns across an axis | ||||
| 
 | ||||
|         :param axis: Axis to mirror across | ||||
|         :return: self | ||||
|         """ | ||||
|         for entry in self.shapes + self.subpatterns + self.labels: | ||||
|             entry.offset[axis - 1] *= -1 | ||||
|         return self | ||||
| 
 | ||||
|     def mirror_elements(self, axis: int) -> 'Pattern': | ||||
|         """ | ||||
|         Mirror each shape and subpattern across an axis, relative to its | ||||
|           center (offset) | ||||
| 
 | ||||
|         :param axis: Axis to mirror across | ||||
|         :return: self | ||||
|         """ | ||||
|         for entry in self.shapes + self.subpatterns: | ||||
|             entry.mirror(axis) | ||||
|         return self | ||||
| 
 | ||||
|     def mirror(self, axis: int) -> 'Pattern': | ||||
|         """ | ||||
|         Mirror the Pattern across an axis | ||||
| 
 | ||||
|         :param axis: Axis to mirror across | ||||
|         :return: self | ||||
|         """ | ||||
|         self.mirror_elements(axis) | ||||
|         self.mirror_element_centers(axis) | ||||
|         return self | ||||
| 
 | ||||
|     def scale_element_doses(self, factor: float) -> 'Pattern': | ||||
|         """ | ||||
|         Multiply all shape and subpattern doses by a factor | ||||
| 
 | ||||
|         :param factor: Factor to multiply doses by | ||||
|         :return: self | ||||
|         """ | ||||
|         for entry in self.shapes + self.subpatterns: | ||||
|             entry.dose *= factor | ||||
|         return self | ||||
| 
 | ||||
|     def copy(self) -> 'Pattern': | ||||
|         """ | ||||
|         Return a copy of the Pattern, deep-copying shapes and copying subpattern entries, but not | ||||
|          deep-copying any referenced patterns. | ||||
| 
 | ||||
|         See also: Pattern.deepcopy() | ||||
| 
 | ||||
|         :return: A copy of the current Pattern. | ||||
|         """ | ||||
|         cp = copy.copy(self) | ||||
|         cp.shapes = copy.deepcopy(cp.shapes) | ||||
|         cp.labels = copy.deepcopy(cp.labels) | ||||
|         cp.subpatterns = [copy.copy(subpat) for subpat in cp.subpatterns] | ||||
|         return cp | ||||
| 
 | ||||
|     def deepcopy(self) -> 'Pattern': | ||||
|         """ | ||||
|         Convenience method for copy.deepcopy(pattern) | ||||
| 
 | ||||
|         :return: A deep copy of the current Pattern. | ||||
|         """ | ||||
|         return copy.deepcopy(self) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def load(filename: str) -> 'Pattern': | ||||
|         """ | ||||
|         Load a Pattern from a file | ||||
| 
 | ||||
|         :param filename: Filename to load from | ||||
|         :return: Loaded Pattern | ||||
|         """ | ||||
|         with open(filename, 'rb') as f: | ||||
|             tmp_dict = pickle.load(f) | ||||
| 
 | ||||
|         pattern = Pattern() | ||||
|         pattern.__dict__.update(tmp_dict) | ||||
|         return pattern | ||||
| 
 | ||||
|     def save(self, filename: str) -> 'Pattern': | ||||
|         """ | ||||
|         Save the Pattern to a file | ||||
| 
 | ||||
|         :param filename: Filename to save to | ||||
|         :return: self | ||||
|         """ | ||||
|         with open(filename, 'wb') as f: | ||||
|             pickle.dump(self.__dict__, f, protocol=2) | ||||
|         return self | ||||
| 
 | ||||
|     def visualize(self, | ||||
|                   offset: vector2=(0., 0.), | ||||
|                   line_color: str='k', | ||||
|                   fill_color: str='none', | ||||
|                   overdraw: bool=False): | ||||
|         """ | ||||
|         Draw a picture of the Pattern and wait for the user to inspect it | ||||
| 
 | ||||
|         Imports matplotlib. | ||||
| 
 | ||||
|         :param offset: Coordinates to offset by before drawing | ||||
|         :param line_color: Outlines are drawn with this color (passed to matplotlib PolyCollection) | ||||
|         :param fill_color: Interiors are drawn with this color (passed to matplotlib PolyCollection) | ||||
|         :param overdraw: Whether to create a new figure or draw on a pre-existing one | ||||
|         """ | ||||
|         # TODO: add text labels to visualize() | ||||
|         from matplotlib import pyplot | ||||
|         import matplotlib.collections | ||||
| 
 | ||||
|         offset = numpy.array(offset, dtype=float) | ||||
| 
 | ||||
|         if not overdraw: | ||||
|             figure = pyplot.figure() | ||||
|             pyplot.axis('equal') | ||||
|         else: | ||||
|             figure = pyplot.gcf() | ||||
| 
 | ||||
|         axes = figure.gca() | ||||
| 
 | ||||
|         polygons = [] | ||||
|         for shape in self.shapes: | ||||
|             polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()] | ||||
| 
 | ||||
|         mpl_poly_collection = matplotlib.collections.PolyCollection(polygons, | ||||
|                                                                     facecolors=fill_color, | ||||
|                                                                     edgecolors=line_color) | ||||
|         axes.add_collection(mpl_poly_collection) | ||||
|         pyplot.axis('equal') | ||||
| 
 | ||||
|         for subpat in self.subpatterns: | ||||
|             subpat.as_pattern().visualize(offset=offset, overdraw=True, | ||||
|                                           line_color=line_color, fill_color=fill_color) | ||||
| 
 | ||||
|         if not overdraw: | ||||
|             pyplot.show() | ||||
							
								
								
									
										291
									
								
								masque/repetition.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								masque/repetition.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,291 @@ | ||||
| """ | ||||
|     Repetitions provides support for efficiently nesting multiple identical | ||||
|      instances of a Pattern in the same parent Pattern. | ||||
| """ | ||||
| 
 | ||||
| from typing import Union, List | ||||
| import copy | ||||
| 
 | ||||
| import numpy | ||||
| from numpy import pi | ||||
| 
 | ||||
| from .error import PatternError | ||||
| from .utils import is_scalar, rotation_matrix_2d, vector2 | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| # TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied | ||||
| 
 | ||||
| class GridRepetition: | ||||
|     """ | ||||
|     GridRepetition provides support for efficiently embedding multiple copies of a Pattern | ||||
|      into another Pattern at regularly-spaced offsets. | ||||
|     """ | ||||
| 
 | ||||
|     pattern = None          # type: Pattern | ||||
| 
 | ||||
|     _offset = (0.0, 0.0)    # type: numpy.ndarray | ||||
|     _rotation = 0.0         # type: float | ||||
|     _dose = 1.0             # type: float | ||||
|     _scale = 1.0            # type: float | ||||
|     _mirrored = None        # type: List[bool] | ||||
| 
 | ||||
|     _a_vector = None        # type: numpy.ndarray | ||||
|     _b_vector = None        # type: numpy.ndarray | ||||
|     a_count = None          # type: int | ||||
|     b_count = 1             # type: int | ||||
| 
 | ||||
|     def __init__(self, | ||||
|                  pattern: 'Pattern', | ||||
|                  a_vector: numpy.ndarray, | ||||
|                  a_count: int, | ||||
|                  b_vector: numpy.ndarray = None, | ||||
|                  b_count: int = 1, | ||||
|                  offset: vector2 = (0.0, 0.0), | ||||
|                  rotation: float = 0.0, | ||||
|                  mirrored: List[bool] = None, | ||||
|                  dose: float = 1.0, | ||||
|                  scale: float = 1.0): | ||||
|         """ | ||||
|         :param a_vector: First lattice vector, of the form [x, y]. | ||||
|             Specifies center-to-center spacing between adjacent elements. | ||||
|         :param a_count: Number of elements in the a_vector direction. | ||||
|         :param b_vector: Second lattice vector, of the form [x, y]. | ||||
|             Specifies center-to-center spacing between adjacent elements. | ||||
|             Can be omitted when specifying a 1D array. | ||||
|         :param b_count: Number of elements in the b_vector direction. | ||||
|             Should be omitted if b_vector was omitted. | ||||
|         :raises: InvalidDataError if b_* inputs conflict with each other | ||||
|             or a_count < 1. | ||||
|         """ | ||||
|         if b_vector is None: | ||||
|             if b_count > 1: | ||||
|                 raise PatternError('Repetition has b_count > 1 but no b_vector') | ||||
|             else: | ||||
|                 b_vector = numpy.array([0.0, 0.0]) | ||||
| 
 | ||||
|         if a_count < 1: | ||||
|             raise InvalidDataError('Repetition has too-small a_count: ' | ||||
|                                    '{}'.format(a_count)) | ||||
|         if b_count < 1: | ||||
|             raise InvalidDataError('Repetition has too-small b_count: ' | ||||
|                                    '{}'.format(b_count)) | ||||
|         self.a_vector = a_vector | ||||
|         self.b_vector = b_vector | ||||
|         self.a_count = a_count | ||||
|         self.b_count = b_count | ||||
| 
 | ||||
|         self.pattern = pattern | ||||
|         self.offset = offset | ||||
|         self.rotation = rotation | ||||
|         self.dose = dose | ||||
|         self.scale = scale | ||||
|         if mirrored is None: | ||||
|             mirrored = [False, False] | ||||
|         self.mirrored = mirrored | ||||
| 
 | ||||
|     # offset property | ||||
|     @property | ||||
|     def offset(self) -> numpy.ndarray: | ||||
|         return self._offset | ||||
| 
 | ||||
|     @offset.setter | ||||
|     def offset(self, val: vector2): | ||||
|         if not isinstance(val, numpy.ndarray): | ||||
|             val = numpy.array(val, dtype=float) | ||||
| 
 | ||||
|         if val.size != 2: | ||||
|             raise PatternError('Offset must be convertible to size-2 ndarray') | ||||
|         self._offset = val.flatten().astype(float) | ||||
| 
 | ||||
|     # dose property | ||||
|     @property | ||||
|     def dose(self) -> float: | ||||
|         return self._dose | ||||
| 
 | ||||
|     @dose.setter | ||||
|     def dose(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Dose must be a scalar') | ||||
|         if not val >= 0: | ||||
|             raise PatternError('Dose must be non-negative') | ||||
|         self._dose = val | ||||
| 
 | ||||
|     # scale property | ||||
|     @property | ||||
|     def scale(self) -> float: | ||||
|         return self._scale | ||||
| 
 | ||||
|     @scale.setter | ||||
|     def scale(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Scale must be a scalar') | ||||
|         if not val > 0: | ||||
|             raise PatternError('Scale must be positive') | ||||
|         self._scale = val | ||||
| 
 | ||||
|     # Rotation property [ccw] | ||||
|     @property | ||||
|     def rotation(self) -> float: | ||||
|         return self._rotation | ||||
| 
 | ||||
|     @rotation.setter | ||||
|     def rotation(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Rotation must be a scalar') | ||||
|         self._rotation = val % (2 * pi) | ||||
| 
 | ||||
|     # Mirrored property | ||||
|     @property | ||||
|     def mirrored(self) -> List[bool]: | ||||
|         return self._mirrored | ||||
| 
 | ||||
|     @mirrored.setter | ||||
|     def mirrored(self, val: List[bool]): | ||||
|         if is_scalar(val): | ||||
|             raise PatternError('Mirrored must be a 2-element list of booleans') | ||||
|         self._mirrored = val | ||||
| 
 | ||||
|     # a_vector property | ||||
|     @property | ||||
|     def a_vector(self) -> numpy.ndarray: | ||||
|         return self._a_vector | ||||
| 
 | ||||
|     @a_vector.setter | ||||
|     def a_vector(self, val: vector2): | ||||
|         if not isinstance(val, numpy.ndarray): | ||||
|             val = numpy.array(val, dtype=float) | ||||
| 
 | ||||
|         if val.size != 2: | ||||
|             raise PatternError('a_vector must be convertible to size-2 ndarray') | ||||
|         self._a_vector = val.flatten() | ||||
| 
 | ||||
|     # b_vector property | ||||
|     @property | ||||
|     def b_vector(self) -> numpy.ndarray: | ||||
|         return self._b_vector | ||||
| 
 | ||||
|     @b_vector.setter | ||||
|     def b_vector(self, val: vector2): | ||||
|         if not isinstance(val, numpy.ndarray): | ||||
|             val = numpy.array(val, dtype=float) | ||||
| 
 | ||||
|         if val.size != 2: | ||||
|             raise PatternError('b_vector must be convertible to size-2 ndarray') | ||||
|         self._b_vector = val.flatten() | ||||
| 
 | ||||
| 
 | ||||
|     def as_pattern(self) -> 'Pattern': | ||||
|         """ | ||||
|         Returns a copy of self.pattern which has been scaled, rotated, etc. according to this | ||||
|          SubPattern's properties. | ||||
|         :return: Copy of self.pattern that has been altered to reflect the SubPattern's properties. | ||||
|         """ | ||||
|         #xy = numpy.array(element.xy) | ||||
|         #origin = xy[0] | ||||
|         #col_spacing = (xy[1] - origin) / element.cols | ||||
|         #row_spacing = (xy[2] - origin) / element.rows | ||||
| 
 | ||||
|         patterns = [] | ||||
| 
 | ||||
|         for a in range(self.a_count): | ||||
|             for b in range(self.b_count): | ||||
|                 offset = a * self.a_vector + b * self.b_vector | ||||
|                 newPat = self.pattern.deepcopy() | ||||
|                 newPat.translate_elements(offset) | ||||
|                 patterns.append(newPat) | ||||
| 
 | ||||
|         combined = patterns[0] | ||||
|         for p in patterns[1:]: | ||||
|             combined.append(p) | ||||
| 
 | ||||
|         combined.scale_by(self.scale) | ||||
|         [combined.mirror(ax) for ax, do in enumerate(self.mirrored) if do] | ||||
|         combined.rotate_around((0.0, 0.0), self.rotation) | ||||
|         combined.translate_elements(self.offset) | ||||
|         combined.scale_element_doses(self.dose) | ||||
| 
 | ||||
|         return combined | ||||
| 
 | ||||
|     def translate(self, offset: vector2) -> 'GridRepetition': | ||||
|         """ | ||||
|         Translate by the given offset | ||||
| 
 | ||||
|         :param offset: Translate by this offset | ||||
|         :return: self | ||||
|         """ | ||||
|         self.offset += offset | ||||
|         return self | ||||
| 
 | ||||
|     def rotate_around(self, pivot: vector2, rotation: float) -> 'GridRepetition': | ||||
|         """ | ||||
|         Rotate around a point | ||||
| 
 | ||||
|         :param pivot: Point to rotate around | ||||
|         :param rotation: Angle to rotate by (counterclockwise, radians) | ||||
|         :return: self | ||||
|         """ | ||||
|         pivot = numpy.array(pivot, dtype=float) | ||||
|         self.translate(-pivot) | ||||
|         self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) | ||||
|         self.rotate(rotation) | ||||
|         self.translate(+pivot) | ||||
|         return self | ||||
| 
 | ||||
|     def rotate(self, rotation: float) -> 'GridRepetition': | ||||
|         """ | ||||
|         Rotate around (0, 0) | ||||
| 
 | ||||
|         :param rotation: Angle to rotate by (counterclockwise, radians) | ||||
|         :return: self | ||||
|         """ | ||||
|         self.rotation += rotation | ||||
|         return self | ||||
| 
 | ||||
|     def mirror(self, axis: int) -> 'GridRepetition': | ||||
|         """ | ||||
|         Mirror the subpattern across an axis. | ||||
| 
 | ||||
|         :param axis: Axis to mirror across. | ||||
|         :return: self | ||||
|         """ | ||||
|         self.mirrored[axis] = not self.mirrored[axis] | ||||
|         return self | ||||
| 
 | ||||
|     def get_bounds(self) -> numpy.ndarray or None: | ||||
|         """ | ||||
|         Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the | ||||
|          extent of the SubPattern in each dimension. | ||||
|         Returns None if the contained Pattern is empty. | ||||
| 
 | ||||
|         :return: [[x_min, y_min], [x_max, y_max]] or None | ||||
|         """ | ||||
|         return self.as_pattern().get_bounds() | ||||
| 
 | ||||
|     def scale_by(self, c: float) -> 'GridRepetition': | ||||
|         """ | ||||
|         Scale the subpattern by a factor | ||||
| 
 | ||||
|         :param c: scaling factor | ||||
|         """ | ||||
|         self.scale *= c | ||||
|         return self | ||||
| 
 | ||||
|     def copy(self) -> 'GridRepetition': | ||||
|         """ | ||||
|         Return a shallow copy of the repetition. | ||||
| 
 | ||||
|         :return: copy.copy(self) | ||||
|         """ | ||||
|         return copy.copy(self) | ||||
| 
 | ||||
|     def deepcopy(self) -> 'SubPattern': | ||||
|         """ | ||||
|         Return a deep copy of the repetition. | ||||
| 
 | ||||
|         :return: copy.copy(self) | ||||
|         """ | ||||
|         return copy.deepcopy(self) | ||||
| 
 | ||||
							
								
								
									
										12
									
								
								masque/shapes/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								masque/shapes/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| """ | ||||
| Shapes for use with the Pattern class, as well as the Shape abstract class from | ||||
|  which they are derived. | ||||
| """ | ||||
| 
 | ||||
| from .shape import Shape, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | ||||
| 
 | ||||
| from .polygon import Polygon | ||||
| from .circle import Circle | ||||
| from .ellipse import Ellipse | ||||
| from .arc import Arc | ||||
| from .text import Text | ||||
							
								
								
									
										358
									
								
								masque/shapes/arc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										358
									
								
								masque/shapes/arc.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,358 @@ | ||||
| from typing import List | ||||
| import math | ||||
| import numpy | ||||
| from numpy import pi | ||||
| 
 | ||||
| from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | ||||
| from .. import PatternError | ||||
| from ..utils import is_scalar, vector2 | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| class Arc(Shape): | ||||
|     """ | ||||
|     An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its | ||||
|      center. It has a position, two radii, a start and stop angle, a rotation, and a width. | ||||
| 
 | ||||
|     The radii define an ellipse; the ring is formed with radii +/- width/2. | ||||
|     The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. | ||||
|     The start and stop angle are measured counterclockwise from the first (x) radius. | ||||
|     """ | ||||
| 
 | ||||
|     _radii = None           # type: numpy.ndarray | ||||
|     _angles = None          # type: numpy.ndarray | ||||
|     _width = 1.0            # type: float | ||||
|     _rotation = 0.0         # type: float | ||||
| 
 | ||||
|     # Defaults for to_polygons | ||||
|     poly_num_points = DEFAULT_POLY_NUM_POINTS   # type: int | ||||
|     poly_max_arclen = None                      # type: float | ||||
| 
 | ||||
|     # radius properties | ||||
|     @property | ||||
|     def radii(self) -> numpy.ndarray: | ||||
|         """ | ||||
|         Return the radii [rx, ry] | ||||
| 
 | ||||
|         :return: [rx, ry] | ||||
|         """ | ||||
|         return self._radii | ||||
| 
 | ||||
|     @radii.setter | ||||
|     def radii(self, val: vector2): | ||||
|         val = numpy.array(val, dtype=float).flatten() | ||||
|         if not val.size == 2: | ||||
|             raise PatternError('Radii must have length 2') | ||||
|         if not val.min() >= 0: | ||||
|             raise PatternError('Radii must be non-negative') | ||||
|         self._radii = val | ||||
| 
 | ||||
|     @property | ||||
|     def radius_x(self) -> float: | ||||
|         return self._radii[0] | ||||
| 
 | ||||
|     @radius_x.setter | ||||
|     def radius_x(self, val: float): | ||||
|         if not val >= 0: | ||||
|             raise PatternError('Radius must be non-negative') | ||||
|         self._radii[0] = val | ||||
| 
 | ||||
|     @property | ||||
|     def radius_y(self) -> float: | ||||
|         return self._radii[1] | ||||
| 
 | ||||
|     @radius_y.setter | ||||
|     def radius_y(self, val: float): | ||||
|         if not val >= 0: | ||||
|             raise PatternError('Radius must be non-negative') | ||||
|         self._radii[1] = val | ||||
| 
 | ||||
|     # arc start/stop angle properties | ||||
|     @property | ||||
|     def angles(self) -> vector2: | ||||
|         """ | ||||
|         Return the start and stop angles [a_start, a_stop]. | ||||
|         Angles are measured from x-axis after rotation | ||||
| 
 | ||||
|         :return: [a_start, a_stop] | ||||
|         """ | ||||
|         return self._angles | ||||
| 
 | ||||
|     @angles.setter | ||||
|     def angles(self, val: vector2): | ||||
|         val = numpy.array(val, dtype=float).flatten() | ||||
|         if not val.size == 2: | ||||
|             raise PatternError('Angles must have length 2') | ||||
|         self._angles = val | ||||
| 
 | ||||
|     @property | ||||
|     def start_angle(self) -> float: | ||||
|         return self.angles[0] | ||||
| 
 | ||||
|     @start_angle.setter | ||||
|     def start_angle(self, val: float): | ||||
|         self.angles = (val, self.angles[1]) | ||||
| 
 | ||||
|     @property | ||||
|     def stop_angle(self) -> float: | ||||
|         return self.angles[1] | ||||
| 
 | ||||
|     @stop_angle.setter | ||||
|     def stop_angle(self, val: float): | ||||
|         self.angles = (self.angles[0], val) | ||||
| 
 | ||||
|     # Rotation property | ||||
|     @property | ||||
|     def rotation(self) -> float: | ||||
|         """ | ||||
|         Rotation of radius_x from x_axis, counterclockwise, in radians. Stored mod 2*pi | ||||
| 
 | ||||
|         :return: rotation counterclockwise in radians | ||||
|         """ | ||||
|         return self._rotation | ||||
| 
 | ||||
|     @rotation.setter | ||||
|     def rotation(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Rotation must be a scalar') | ||||
|         self._rotation = val % (2 * pi) | ||||
| 
 | ||||
|     # Width | ||||
|     @property | ||||
|     def width(self) -> float: | ||||
|         """ | ||||
|         Width of the arc (difference between inner and outer radii) | ||||
| 
 | ||||
|         :return: width | ||||
|         """ | ||||
|         return self._width | ||||
| 
 | ||||
|     @width.setter | ||||
|     def width(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Width must be a scalar') | ||||
|         if not val > 0: | ||||
|             raise PatternError('Width must be positive') | ||||
|         self._width = val | ||||
| 
 | ||||
|     def __init__(self, | ||||
|                  radii: vector2, | ||||
|                  angles: vector2, | ||||
|                  width: float, | ||||
|                  rotation: float=0, | ||||
|                  poly_num_points: int=DEFAULT_POLY_NUM_POINTS, | ||||
|                  poly_max_arclen: float=None, | ||||
|                  offset: vector2=(0.0, 0.0), | ||||
|                  layer: int=0, | ||||
|                  dose: float=1.0): | ||||
|         self.offset = offset | ||||
|         self.layer = layer | ||||
|         self.dose = dose | ||||
|         self.radii = radii | ||||
|         self.angles = angles | ||||
|         self.width = width | ||||
|         self.rotation = rotation | ||||
|         self.poly_num_points = poly_num_points | ||||
|         self.poly_max_arclen = poly_max_arclen | ||||
| 
 | ||||
|     def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None) -> List[Polygon]: | ||||
|         if poly_num_points is None: | ||||
|             poly_num_points = self.poly_num_points | ||||
|         if poly_max_arclen is None: | ||||
|             poly_max_arclen = self.poly_max_arclen | ||||
| 
 | ||||
|         if (poly_num_points is None) and (poly_max_arclen is None): | ||||
|             raise PatternError('Max number of points and arclength left unspecified' + | ||||
|                                ' (default was also overridden)') | ||||
| 
 | ||||
|         r0, r1 = self.radii | ||||
| 
 | ||||
|         # Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation) | ||||
|         a_ranges = self._angles_to_parameters() | ||||
| 
 | ||||
|         # Approximate perimeter | ||||
|         # Ramanujan, S., "Modular Equations and Approximations to ," | ||||
|         #  Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372 | ||||
|         a0, a1 = a_ranges[1]    # use outer arc | ||||
|         h = ((r1 - r0) / (r1 + r0)) ** 2 | ||||
|         ellipse_perimeter = pi * (r1 + r0) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) | ||||
|         perimeter = abs(a0 - a1) / (2 * pi) * ellipse_perimeter         # TODO: make this more accurate | ||||
| 
 | ||||
|         n = [] | ||||
|         if poly_num_points is not None: | ||||
|             n += [poly_num_points] | ||||
|         if poly_max_arclen is not None: | ||||
|             n += [perimeter / poly_max_arclen] | ||||
|         thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], max(n), endpoint=True) | ||||
|         thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], max(n), endpoint=True) | ||||
| 
 | ||||
|         sin_th_i, cos_th_i = (numpy.sin(thetas_inner), numpy.cos(thetas_inner)) | ||||
|         sin_th_o, cos_th_o = (numpy.sin(thetas_outer), numpy.cos(thetas_outer)) | ||||
|         wh = self.width / 2.0 | ||||
| 
 | ||||
|         xs1 = (r0 + wh) * cos_th_o | ||||
|         ys1 = (r1 + wh) * sin_th_o | ||||
|         xs2 = (r0 - wh) * cos_th_i | ||||
|         ys2 = (r1 - wh) * sin_th_i | ||||
| 
 | ||||
|         xs = numpy.hstack((xs1, xs2)) | ||||
|         ys = numpy.hstack((ys1, ys2)) | ||||
|         xys = numpy.vstack((xs, ys)).T | ||||
| 
 | ||||
|         poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset) | ||||
|         poly.rotate(self.rotation) | ||||
|         return [poly] | ||||
| 
 | ||||
|     def get_bounds(self) -> numpy.ndarray: | ||||
|         ''' | ||||
|         Equation for rotated ellipse is | ||||
|             x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi) | ||||
|             y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot) | ||||
|           where t is our parameter. | ||||
| 
 | ||||
|         Differentiating and solving for 0 slope wrt. t, we find | ||||
|             tan(t) = -+ b/a cot(phi) | ||||
|           where -+ is for x, y cases, so that's where the extrema are. | ||||
| 
 | ||||
|         If the extrema are innaccessible due to arc constraints, check the arc endpoints instead. | ||||
|         ''' | ||||
|         a_ranges = self._angles_to_parameters() | ||||
| 
 | ||||
|         mins = [] | ||||
|         maxs = [] | ||||
|         for a, sgn in zip(a_ranges, (-1, +1)): | ||||
|             wh = sgn * self.width/2 | ||||
|             rx = self.radius_x + wh | ||||
|             ry = self.radius_y + wh | ||||
| 
 | ||||
|             a0, a1 = a | ||||
|             a0_offset = a0 - (a0 % (2 * pi)) | ||||
| 
 | ||||
|             sin_r = numpy.sin(self.rotation) | ||||
|             cos_r = numpy.cos(self.rotation) | ||||
|             sin_a = numpy.sin(a) | ||||
|             cos_a = numpy.cos(a) | ||||
| 
 | ||||
|             # Cutoff angles | ||||
|             xpt = (-self.rotation) % (2 * pi) + a0_offset | ||||
|             ypt = (pi/2 - self.rotation) % (2 * pi) + a0_offset | ||||
|             xnt = (xpt - pi) % (2 * pi) + a0_offset | ||||
|             ynt = (ypt - pi) % (2 * pi) + a0_offset | ||||
| 
 | ||||
|             # Points along coordinate axes | ||||
|             rx2_inv = 1 / (rx * rx) | ||||
|             ry2_inv = 1 / (ry * ry) | ||||
|             xr = numpy.abs(cos_r * cos_r * rx2_inv + sin_r * sin_r * ry2_inv) ** -0.5 | ||||
|             yr = numpy.abs(-sin_r * -sin_r * rx2_inv + cos_r * cos_r * ry2_inv) ** -0.5 | ||||
| 
 | ||||
|             # Arc endpoints | ||||
|             xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a) | ||||
|             yn, yp = sorted(rx * sin_r * cos_a + ry * cos_r * sin_a) | ||||
| 
 | ||||
|             # If our arc subtends a coordinate axis, use the extremum along that axis | ||||
|             if a0 < xpt < a1 or a0 < xpt + 2 * pi < a1: | ||||
|                 xp = xr | ||||
| 
 | ||||
|             if a0 < xnt < a1 or a0 < xnt + 2 * pi < a1: | ||||
|                 xn = -xr | ||||
| 
 | ||||
|             if a0 < ypt < a1 or a0 < ypt + 2 * pi < a1: | ||||
|                 yp = yr | ||||
| 
 | ||||
|             if a0 < ynt < a1 or a0 < ynt + 2 * pi < a1: | ||||
|                 yn = -yr | ||||
| 
 | ||||
|             mins.append([xn, yn]) | ||||
|             maxs.append([xp, yp]) | ||||
|         return numpy.vstack((numpy.min(mins, axis=0) + self.offset, | ||||
|                              numpy.max(maxs, axis=0) + self.offset)) | ||||
| 
 | ||||
|     def rotate(self, theta: float) -> 'Arc': | ||||
|         self.rotation += theta | ||||
|         return self | ||||
| 
 | ||||
|     def mirror(self, axis: int) -> 'Arc': | ||||
|         self.offset[axis - 1] *= -1 | ||||
|         self.rotation *= -1 | ||||
|         self.angles *= -1 | ||||
|         return self | ||||
| 
 | ||||
|     def scale_by(self, c: float) -> 'Arc': | ||||
|         self.radii *= c | ||||
|         self.width *= c | ||||
|         return self | ||||
| 
 | ||||
|     def normalized_form(self, norm_value: float) -> normalized_shape_tuple: | ||||
|         if self.radius_x < self.radius_y: | ||||
|             radii = self.radii / self.radius_x | ||||
|             scale = self.radius_x | ||||
|             rotation = self.rotation | ||||
|             angles = self.angles | ||||
|         else:  # rotate by 90 degrees and swap radii | ||||
|             radii = self.radii[::-1] / self.radius_y | ||||
|             scale = self.radius_y | ||||
|             rotation = self.rotation + pi / 2 | ||||
|             angles = self.angles - pi / 2 | ||||
| 
 | ||||
|         delta_angle = angles[1] - angles[0] | ||||
|         start_angle = angles[0] % (2 * pi) | ||||
|         if start_angle >= pi: | ||||
|             start_angle -= pi | ||||
|             rotation += pi | ||||
| 
 | ||||
|         angles = (start_angle, start_angle + delta_angle) | ||||
|         rotation %= 2 * pi | ||||
|         width = self.width | ||||
| 
 | ||||
|         return (type(self), radii, angles, width, self.layer), \ | ||||
|                (self.offset, scale/norm_value, rotation, self.dose), \ | ||||
|                lambda: Arc(radii=radii*norm_value, angles=angles, width=width, layer=self.layer) | ||||
| 
 | ||||
|     def get_cap_edges(self) -> numpy.ndarray: | ||||
|         ''' | ||||
|         :returns: [[[x0, y0], [x1, y1]],   array of 4 points, specifying the two cuts which | ||||
|                    [[x2, y2], [x3, y3]]],    would create this arc from its corresponding ellipse. | ||||
|         ''' | ||||
|         a_ranges = self._angles_to_parameters() | ||||
| 
 | ||||
|         mins = [] | ||||
|         maxs = [] | ||||
|         for a, sgn in zip(a_ranges, (-1, +1)): | ||||
|             wh = sgn * self.width/2 | ||||
|             rx = self.radius_x + wh | ||||
|             ry = self.radius_y + wh | ||||
| 
 | ||||
|             sin_r = numpy.sin(self.rotation) | ||||
|             cos_r = numpy.cos(self.rotation) | ||||
|             sin_a = numpy.sin(a) | ||||
|             cos_a = numpy.cos(a) | ||||
| 
 | ||||
|             # arc endpoints | ||||
|             xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a) | ||||
|             yn, yp = sorted(rx * sin_r * cos_a + ry * cos_r * sin_a) | ||||
| 
 | ||||
|             mins.append([xn, yn]) | ||||
|             maxs.append([xp, yp]) | ||||
|         return  numpy.array([mins, maxs]) + self.offset | ||||
| 
 | ||||
|     def _angles_to_parameters(self) -> numpy.ndarray: | ||||
|         ''' | ||||
|         :return: "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form | ||||
|                    [[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]] | ||||
|         ''' | ||||
|         a = [] | ||||
|         for sgn in (-1, +1): | ||||
|             wh = sgn * self.width/2 | ||||
|             rx = self.radius_x + wh | ||||
|             ry = self.radius_y + wh | ||||
| 
 | ||||
|             # create paremeter 'a' for parametrized ellipse | ||||
|             a0, a1 = (numpy.arctan2(rx*numpy.sin(a), ry*numpy.cos(a)) for a in self.angles) | ||||
|             sign = numpy.sign(self.angles[1] - self.angles[0]) | ||||
|             if sign != numpy.sign(a1 - a0): | ||||
|                 a1 += sign * 2 * pi | ||||
| 
 | ||||
|             a.append((a0, a1)) | ||||
|         return numpy.array(a) | ||||
							
								
								
									
										99
									
								
								masque/shapes/circle.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								masque/shapes/circle.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | ||||
| from typing import List | ||||
| import numpy | ||||
| from numpy import pi | ||||
| 
 | ||||
| from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | ||||
| from .. import PatternError | ||||
| from ..utils import is_scalar, vector2 | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| class Circle(Shape): | ||||
|     """ | ||||
|     A circle, which has a position and radius. | ||||
|     """ | ||||
| 
 | ||||
|     _radius = None                              # type: float | ||||
| 
 | ||||
|     # Defaults for to_polygons | ||||
|     poly_num_points = DEFAULT_POLY_NUM_POINTS   # type: int | ||||
|     poly_max_arclen = None                      # type: float | ||||
| 
 | ||||
|     # radius property | ||||
|     @property | ||||
|     def radius(self) -> float: | ||||
|         """ | ||||
|         Circle's radius (float, >= 0) | ||||
| 
 | ||||
|         :return: radius | ||||
|         """ | ||||
|         return self._radius | ||||
| 
 | ||||
|     @radius.setter | ||||
|     def radius(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Radius must be a scalar') | ||||
|         if not val >= 0: | ||||
|             raise PatternError('Radius must be non-negative') | ||||
|         self._radius = val | ||||
| 
 | ||||
|     def __init__(self, | ||||
|                  radius: float, | ||||
|                  poly_num_points: int=DEFAULT_POLY_NUM_POINTS, | ||||
|                  poly_max_arclen: float=None, | ||||
|                  offset: vector2=(0.0, 0.0), | ||||
|                  layer: int=0, | ||||
|                  dose: float=1.0): | ||||
|         self.offset = numpy.array(offset, dtype=float) | ||||
|         self.layer = layer | ||||
|         self.dose = dose | ||||
|         self.radius = radius | ||||
|         self.poly_num_points = poly_num_points | ||||
|         self.poly_max_arclen = poly_max_arclen | ||||
| 
 | ||||
|     def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None) -> List[Polygon]: | ||||
|         if poly_num_points is None: | ||||
|             poly_num_points = self.poly_num_points | ||||
|         if poly_max_arclen is None: | ||||
|             poly_max_arclen = self.poly_max_arclen | ||||
| 
 | ||||
|         if (poly_num_points is None) and (poly_max_arclen is None): | ||||
|             raise PatternError('Number of points and arclength left ' | ||||
|                                'unspecified (default was also overridden)') | ||||
| 
 | ||||
|         n = [] | ||||
|         if poly_num_points is not None: | ||||
|             n += [poly_num_points] | ||||
|         if poly_max_arclen is not None: | ||||
|             n += [2 * pi * self.radius / poly_max_arclen] | ||||
|         thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False) | ||||
|         xs = numpy.cos(thetas) * self.radius | ||||
|         ys = numpy.sin(thetas) * self.radius | ||||
|         xys = numpy.vstack((xs, ys)).T | ||||
| 
 | ||||
|         return [Polygon(xys, offset=self.offset, dose=self.dose, layer=self.layer)] | ||||
| 
 | ||||
|     def get_bounds(self) -> numpy.ndarray: | ||||
|         return numpy.vstack((self.offset - self.radius, | ||||
|                              self.offset + self.radius)) | ||||
| 
 | ||||
|     def rotate(self, theta: float) -> 'Circle': | ||||
|         return self | ||||
| 
 | ||||
|     def mirror(self, axis: int) -> 'Circle': | ||||
|         self.offset *= -1 | ||||
|         return self | ||||
| 
 | ||||
|     def scale_by(self, c: float) -> 'Circle': | ||||
|         self.radius *= c | ||||
|         return self | ||||
| 
 | ||||
|     def normalized_form(self, norm_value) -> normalized_shape_tuple: | ||||
|         rotation = 0.0 | ||||
|         magnitude = self.radius / norm_value | ||||
|         return (type(self), self.layer), \ | ||||
|                (self.offset, magnitude, rotation, self.dose), \ | ||||
|                lambda: Circle(radius=norm_value, layer=self.layer) | ||||
| 
 | ||||
							
								
								
									
										166
									
								
								masque/shapes/ellipse.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								masque/shapes/ellipse.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,166 @@ | ||||
| from typing import List | ||||
| import math | ||||
| import numpy | ||||
| from numpy import pi | ||||
| 
 | ||||
| from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | ||||
| from .. import PatternError | ||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2 | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| class Ellipse(Shape): | ||||
|     """ | ||||
|     An ellipse, which has a position, two radii, and a rotation. | ||||
|     The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. | ||||
|     """ | ||||
| 
 | ||||
|     _radii = None       # type: numpy.ndarray | ||||
|     _rotation = 0.0     # type: float | ||||
| 
 | ||||
|     # Defaults for to_polygons | ||||
|     poly_num_points = DEFAULT_POLY_NUM_POINTS   # type: int | ||||
|     poly_max_arclen = None                      # type: float | ||||
| 
 | ||||
|     # radius properties | ||||
|     @property | ||||
|     def radii(self) -> numpy.ndarray: | ||||
|         """ | ||||
|         Return the radii [rx, ry] | ||||
| 
 | ||||
|         :return: [rx, ry] | ||||
|         """ | ||||
|         return self._radii | ||||
| 
 | ||||
|     @radii.setter | ||||
|     def radii(self, val: vector2): | ||||
|         val = numpy.array(val).flatten() | ||||
|         if not val.size == 2: | ||||
|             raise PatternError('Radii must have length 2') | ||||
|         if not val.min() >= 0: | ||||
|             raise PatternError('Radii must be non-negative') | ||||
|         self._radii = val | ||||
| 
 | ||||
|     @property | ||||
|     def radius_x(self) -> float: | ||||
|         return self.radii[0] | ||||
| 
 | ||||
|     @radius_x.setter | ||||
|     def radius_x(self, val: float): | ||||
|         if not val >= 0: | ||||
|             raise PatternError('Radius must be non-negative') | ||||
|         self.radii[0] = val | ||||
| 
 | ||||
|     @property | ||||
|     def radius_y(self) -> float: | ||||
|         return self.radii[1] | ||||
| 
 | ||||
|     @radius_y.setter | ||||
|     def radius_y(self, val: float): | ||||
|         if not val >= 0: | ||||
|             raise PatternError('Radius must be non-negative') | ||||
|         self.radii[1] = val | ||||
| 
 | ||||
|     # Rotation property | ||||
|     @property | ||||
|     def rotation(self) -> float: | ||||
|         """ | ||||
|         Rotation of rx from the x axis. Uses the interval [0, pi) in radians (counterclockwise | ||||
|          is positive) | ||||
| 
 | ||||
|         :return: counterclockwise rotation in radians | ||||
|         """ | ||||
|         return self._rotation | ||||
| 
 | ||||
|     @rotation.setter | ||||
|     def rotation(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Rotation must be a scalar') | ||||
|         self._rotation = val % pi | ||||
| 
 | ||||
|     def __init__(self, | ||||
|                  radii: vector2, | ||||
|                  rotation: float=0, | ||||
|                  poly_num_points: int=DEFAULT_POLY_NUM_POINTS, | ||||
|                  poly_max_arclen: float=None, | ||||
|                  offset: vector2=(0.0, 0.0), | ||||
|                  layer: int=0, | ||||
|                  dose: float=1.0): | ||||
|         self.offset = offset | ||||
|         self.layer = layer | ||||
|         self.dose = dose | ||||
|         self.radii = radii | ||||
|         self.rotation = rotation | ||||
|         self.poly_num_points = poly_num_points | ||||
|         self.poly_max_arclen = poly_max_arclen | ||||
| 
 | ||||
|     def to_polygons(self, | ||||
|                     poly_num_points: int=None, | ||||
|                     poly_max_arclen: float=None | ||||
|                     ) -> List[Polygon]: | ||||
|         if poly_num_points is None: | ||||
|             poly_num_points = self.poly_num_points | ||||
|         if poly_max_arclen is None: | ||||
|             poly_max_arclen = self.poly_max_arclen | ||||
| 
 | ||||
|         if (poly_num_points is None) and (poly_max_arclen is None): | ||||
|             raise PatternError('Number of points and arclength left unspecified' | ||||
|                                ' (default was also overridden)') | ||||
| 
 | ||||
|         r0, r1 = self.radii | ||||
| 
 | ||||
|         # Approximate perimeter | ||||
|         # Ramanujan, S., "Modular Equations and Approximations to ," | ||||
|         #  Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372 | ||||
|         h = ((r1 - r0) / (r1 + r0)) ** 2 | ||||
|         perimeter = pi * (r1 + r0) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) | ||||
| 
 | ||||
|         n = [] | ||||
|         if poly_num_points is not None: | ||||
|             n += [poly_num_points] | ||||
|         if poly_max_arclen is not None: | ||||
|             n += [perimeter / poly_max_arclen] | ||||
|         thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False) | ||||
| 
 | ||||
|         sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas)) | ||||
|         xs = r0 * cos_th | ||||
|         ys = r1 * sin_th | ||||
|         xys = numpy.vstack((xs, ys)).T | ||||
| 
 | ||||
|         poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset) | ||||
|         poly.rotate(self.rotation) | ||||
|         return [poly] | ||||
| 
 | ||||
|     def get_bounds(self) -> numpy.ndarray: | ||||
|         rot_radii = numpy.dot(rotation_matrix_2d(self.rotation), self.radii) | ||||
|         return numpy.vstack((self.offset - rot_radii[0], | ||||
|                              self.offset + rot_radii[1])) | ||||
| 
 | ||||
|     def rotate(self, theta: float) -> 'Ellipse': | ||||
|         self.rotation += theta | ||||
|         return self | ||||
| 
 | ||||
|     def mirror(self, axis: int) -> 'Ellipse': | ||||
|         self.offset[axis - 1] *= -1 | ||||
|         self.rotation *= -1 | ||||
|         return self | ||||
| 
 | ||||
|     def scale_by(self, c: float) -> 'Ellipse': | ||||
|         self.radii *= c | ||||
|         return self | ||||
| 
 | ||||
|     def normalized_form(self, norm_value: float) -> normalized_shape_tuple: | ||||
|         if self.radius_x < self.radius_y: | ||||
|             radii = self.radii / self.radius_x | ||||
|             scale = self.radius_x | ||||
|             angle = self.rotation | ||||
|         else: | ||||
|             radii = self.radii[::-1] / self.radius_y | ||||
|             scale = self.radius_y | ||||
|             angle = (self.rotation + pi / 2) % pi | ||||
|         return (type(self), radii, self.layer), \ | ||||
|                (self.offset, scale/norm_value, angle, self.dose), \ | ||||
|                lambda: Ellipse(radii=radii*norm_value, layer=self.layer) | ||||
| 
 | ||||
							
								
								
									
										281
									
								
								masque/shapes/polygon.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								masque/shapes/polygon.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,281 @@ | ||||
| from typing import List | ||||
| import copy | ||||
| import numpy | ||||
| from numpy import pi | ||||
| 
 | ||||
| from . import Shape, normalized_shape_tuple | ||||
| from .. import PatternError | ||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2 | ||||
| from ..utils import remove_colinear_vertices, remove_duplicate_vertices | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| class Polygon(Shape): | ||||
|     """ | ||||
|     A polygon, consisting of a bunch of vertices (Nx2 ndarray) along with an offset. | ||||
| 
 | ||||
|     A normalized_form(...) is available, but can be quite slow with lots of vertices. | ||||
|     """ | ||||
|     _vertices = None        # type: numpy.ndarray | ||||
| 
 | ||||
|     # vertices property | ||||
|     @property | ||||
|     def vertices(self) -> numpy.ndarray: | ||||
|         """ | ||||
|         Vertices of the polygon (Nx2 ndarray: [[x0, y0], [x1, y1], ...] | ||||
| 
 | ||||
|         :return: vertices | ||||
|         """ | ||||
|         return self._vertices | ||||
| 
 | ||||
|     @vertices.setter | ||||
|     def vertices(self, val: numpy.ndarray): | ||||
|         val = numpy.array(val, dtype=float) | ||||
|         if len(val.shape) < 2 or val.shape[1] != 2: | ||||
|             raise PatternError('Vertices must be an Nx2 array') | ||||
|         if val.shape[0] < 3: | ||||
|             raise PatternError('Must have at least 3 vertices (Nx2, N>3)') | ||||
|         self._vertices = val | ||||
| 
 | ||||
|     # xs property | ||||
|     @property | ||||
|     def xs(self) -> numpy.ndarray: | ||||
|         """ | ||||
|         All x vertices in a 1D ndarray | ||||
|         """ | ||||
|         return self.vertices[:, 0] | ||||
| 
 | ||||
|     @xs.setter | ||||
|     def xs(self, val: numpy.ndarray): | ||||
|         val = numpy.array(val, dtype=float).flatten() | ||||
|         if val.size != self.vertices.shape[0]: | ||||
|             raise PatternError('Wrong number of vertices') | ||||
|         self.vertices[:, 0] = val | ||||
| 
 | ||||
|     # ys property | ||||
|     @property | ||||
|     def ys(self) -> numpy.ndarray: | ||||
|         """ | ||||
|         All y vertices in a 1D ndarray | ||||
|         """ | ||||
|         return self.vertices[:, 1] | ||||
| 
 | ||||
|     @ys.setter | ||||
|     def ys(self, val: numpy.ndarray): | ||||
|         val = numpy.array(val, dtype=float).flatten() | ||||
|         if val.size != self.vertices.shape[0]: | ||||
|             raise PatternError('Wrong number of vertices') | ||||
|         self.vertices[:, 1] = val | ||||
| 
 | ||||
|     def __init__(self, | ||||
|                  vertices: numpy.ndarray, | ||||
|                  offset: vector2=(0.0, 0.0), | ||||
|                  layer: int=0, | ||||
|                  dose: float=1.0): | ||||
|         self.offset = offset | ||||
|         self.layer = layer | ||||
|         self.dose = dose | ||||
|         self.vertices = vertices | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def square(side_length: float, | ||||
|                rotation: float=0.0, | ||||
|                offset: vector2=(0.0, 0.0), | ||||
|                layer: int=0, | ||||
|                dose: float=1.0 | ||||
|                ) -> 'Polygon': | ||||
|         """ | ||||
|         Draw a square given side_length, centered on the origin. | ||||
| 
 | ||||
|         :param side_length: Length of one side | ||||
|         :param rotation: Rotation counterclockwise, in radians | ||||
|         :param offset: Offset, default (0, 0) | ||||
|         :param layer: Layer, default 0 | ||||
|         :param dose: Dose, default 1.0 | ||||
|         :return: A Polygon object containing the requested square | ||||
|         """ | ||||
|         norm_square = numpy.array([[-1, -1], | ||||
|                                    [-1, +1], | ||||
|                                    [+1, +1], | ||||
|                                    [+1, -1]], dtype=float) | ||||
|         vertices = 0.5 * side_length * norm_square | ||||
|         poly = Polygon(vertices, offset, layer, dose) | ||||
|         poly.rotate(rotation) | ||||
|         return poly | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def rectangle(lx: float, | ||||
|                   ly: float, | ||||
|                   rotation: float=0, | ||||
|                   offset: vector2=(0.0, 0.0), | ||||
|                   layer: int=0, | ||||
|                   dose: float=1.0 | ||||
|                   ) -> 'Polygon': | ||||
|         """ | ||||
|         Draw a rectangle with side lengths lx and ly, centered on the origin. | ||||
| 
 | ||||
|         :param lx: Length along x (before rotation) | ||||
|         :param ly: Length along y (before rotation) | ||||
|         :param rotation: Rotation counterclockwise, in radians | ||||
|         :param offset: Offset, default (0, 0) | ||||
|         :param layer: Layer, default 0 | ||||
|         :param dose: Dose, default 1.0 | ||||
|         :return: A Polygon object containing the requested rectangle | ||||
|         """ | ||||
|         vertices = 0.5 * numpy.array([[-lx, -ly], | ||||
|                                       [-lx, +ly], | ||||
|                                       [+lx, +ly], | ||||
|                                       [+lx, -ly]], dtype=float) | ||||
|         poly = Polygon(vertices, offset, layer, dose) | ||||
|         poly.rotate(rotation) | ||||
|         return poly | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def rect(xmin: float = None, | ||||
|              xctr: float = None, | ||||
|              xmax: float = None, | ||||
|              lx: float = None, | ||||
|              ymin: float = None, | ||||
|              yctr: float = None, | ||||
|              ymax: float = None, | ||||
|              ly: float = None, | ||||
|              layer: int = 0, | ||||
|              dose: float = 1.0 | ||||
|              ) -> 'Polygon': | ||||
|         """ | ||||
|         Draw a rectangle by specifying side/center positions. | ||||
| 
 | ||||
|         Must provide 2 of (xmin, xctr, xmax, lx), | ||||
|         and 2 of (ymin, yctr, ymax, ly). | ||||
| 
 | ||||
|         :param xmin: Minimum x coordinate | ||||
|         :param xctr: Center x coordinate | ||||
|         :param xmax: Maximum x coordinate | ||||
|         :param lx: Length along x direction | ||||
|         :param ymin: Minimum y coordinate | ||||
|         :param yctr: Center y coordinate | ||||
|         :param ymax: Maximum y coordinate | ||||
|         :param ly: Length along y direction | ||||
|         :param layer: Layer, default 0 | ||||
|         :param dose: Dose, default 1.0 | ||||
|         :return: A Polygon object containing the requested rectangle | ||||
|         """ | ||||
|         if lx is None: | ||||
|             if xctr is None: | ||||
|                 xctr = 0.5 * (xmax + xmin) | ||||
|                 lx = xmax - xmin | ||||
|             elif xmax is None: | ||||
|                 lx = 2 * (xctr - xmin) | ||||
|             elif xmin is None: | ||||
|                 lx = 2 * (xmax - xctr) | ||||
|             else: | ||||
|                 raise PatternError('Two of xmin, xctr, xmax, lx must be None!') | ||||
|         else: | ||||
|             if xctr is not None: | ||||
|                 pass | ||||
|             elif xmax is None: | ||||
|                 xctr = xmin + 0.5 * lx | ||||
|             elif xmin is None: | ||||
|                 xctr = xmax - 0.5 * lx | ||||
|             else: | ||||
|                 raise PatternError('Two of xmin, xctr, xmax, lx must be None!') | ||||
| 
 | ||||
|         if ly is None: | ||||
|             if yctr is None: | ||||
|                 yctr = 0.5 * (ymax + ymin) | ||||
|                 ly = ymax - ymin | ||||
|             elif ymax is None: | ||||
|                 ly = 2 * (yctr - ymin) | ||||
|             elif ymin is None: | ||||
|                 ly = 2 * (ymax - yctr) | ||||
|             else: | ||||
|                 raise PatternError('Two of ymin, yctr, ymax, ly must be None!') | ||||
|         else: | ||||
|             if yctr is not None: | ||||
|                 pass | ||||
|             elif ymax is None: | ||||
|                 yctr = ymin + 0.5 * ly | ||||
|             elif ymin is None: | ||||
|                 yctr = ymax - 0.5 * ly | ||||
|             else: | ||||
|                 raise PatternError('Two of ymin, yctr, ymax, ly must be None!') | ||||
| 
 | ||||
|         poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), | ||||
|                                  layer=layer, dose=dose) | ||||
|         return poly | ||||
| 
 | ||||
| 
 | ||||
|     def to_polygons(self, | ||||
|                     _poly_num_points: int=None, | ||||
|                     _poly_max_arclen: float=None, | ||||
|                     ) -> List['Polygon']: | ||||
|         return [copy.deepcopy(self)] | ||||
| 
 | ||||
|     def get_bounds(self) -> numpy.ndarray: | ||||
|         return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0), | ||||
|                              self.offset + numpy.max(self.vertices, axis=0))) | ||||
| 
 | ||||
|     def rotate(self, theta: float) -> 'Polygon': | ||||
|         self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T | ||||
|         return self | ||||
| 
 | ||||
|     def mirror(self, axis: int) -> 'Polygon': | ||||
|         self.vertices[:, axis - 1] *= -1 | ||||
|         return self | ||||
| 
 | ||||
|     def scale_by(self, c: float) -> 'Polygon': | ||||
|         self.vertices *= c | ||||
|         return self | ||||
| 
 | ||||
|     def normalized_form(self, norm_value: float) -> normalized_shape_tuple: | ||||
|         # Note: this function is going to be pretty slow for many-vertexed polygons, relative to | ||||
|         #   other shapes | ||||
|         offset = self.vertices.mean(axis=0) + self.offset | ||||
|         zeroed_vertices = self.vertices - offset | ||||
| 
 | ||||
|         scale = zeroed_vertices.std() | ||||
|         normed_vertices = zeroed_vertices / scale | ||||
| 
 | ||||
|         _, _, vertex_axis = numpy.linalg.svd(zeroed_vertices) | ||||
|         rotation = numpy.arctan2(vertex_axis[0][1], vertex_axis[0][0]) % (2 * pi) | ||||
|         rotated_vertices = numpy.vstack([numpy.dot(rotation_matrix_2d(-rotation), v) | ||||
|                                         for v in normed_vertices]) | ||||
| 
 | ||||
|         # Reorder the vertices so that the one with lowest x, then y, comes first. | ||||
|         x_min = rotated_vertices[:, 0].argmin() | ||||
|         if not is_scalar(x_min): | ||||
|             y_min = rotated_vertices[x_min, 1].argmin() | ||||
|             x_min = x_min[y_min] | ||||
|         reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0) | ||||
| 
 | ||||
|         return (type(self), reordered_vertices.data.tobytes(), self.layer), \ | ||||
|                (offset, scale/norm_value, rotation, self.dose), \ | ||||
|                lambda: Polygon(reordered_vertices*norm_value, layer=self.layer) | ||||
| 
 | ||||
|     def clean_vertices(self) -> 'Polygon': | ||||
|         """ | ||||
|         Removes duplicate, co-linear and otherwise redundant vertices. | ||||
| 
 | ||||
|         :returns: self | ||||
|         """ | ||||
|         self.remove_colinear_vertices() | ||||
|         return self | ||||
| 
 | ||||
|     def remove_duplicate_vertices(self) -> 'Polygon': | ||||
|         ''' | ||||
|         Removes all consecutive duplicate (repeated) vertices. | ||||
| 
 | ||||
|         :returns: self | ||||
|         ''' | ||||
|         self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True) | ||||
|         return self | ||||
| 
 | ||||
|     def remove_colinear_vertices(self) -> 'Polygon': | ||||
|         ''' | ||||
|         Removes consecutive co-linear vertices. | ||||
| 
 | ||||
|         :returns: self | ||||
|         ''' | ||||
|         self.vertices = remove_colinear_vertices(self.vertices, closed_path=True) | ||||
|         return self | ||||
							
								
								
									
										386
									
								
								masque/shapes/shape.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										386
									
								
								masque/shapes/shape.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,386 @@ | ||||
| from typing import List, Tuple, Callable | ||||
| from abc import ABCMeta, abstractmethod | ||||
| import copy | ||||
| import numpy | ||||
| 
 | ||||
| from .. import PatternError | ||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2 | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| # Type definitions | ||||
| normalized_shape_tuple = Tuple[Tuple, | ||||
|                                Tuple[numpy.ndarray, float, float, float], | ||||
|                                Callable[[], 'Shape']] | ||||
| 
 | ||||
| # ## Module-wide defaults | ||||
| # Default number of points per polygon for shapes | ||||
| DEFAULT_POLY_NUM_POINTS = 24 | ||||
| 
 | ||||
| 
 | ||||
| class Shape(metaclass=ABCMeta): | ||||
|     """ | ||||
|     Abstract class specifying functions common to all shapes. | ||||
|     """ | ||||
| 
 | ||||
|     # [x_offset, y_offset] | ||||
|     _offset = numpy.array([0.0, 0.0])   # type: numpy.ndarray | ||||
| 
 | ||||
|     # Layer (integer >= 0 or tuple) | ||||
|     _layer = 0                          # type: int or Tuple | ||||
| 
 | ||||
|     # Dose | ||||
|     _dose = 1.0                         # type: float | ||||
| 
 | ||||
|     # --- Abstract methods | ||||
|     @abstractmethod | ||||
|     def to_polygons(self, num_vertices: int, max_arclen: float) -> List['Polygon']: | ||||
|         """ | ||||
|         Returns a list of polygons which approximate the shape. | ||||
| 
 | ||||
|         :param num_vertices: Number of points to use for each polygon. Can be overridden by | ||||
|                      max_arclen if that results in more points. Optional, defaults to shapes' | ||||
|                       internal defaults. | ||||
|         :param max_arclen: Maximum arclength which can be approximated by a single line | ||||
|                      segment. Optional, defaults to shapes' internal defaults. | ||||
|         :return: List of polygons equivalent to the shape | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def get_bounds(self) -> numpy.ndarray: | ||||
|         """ | ||||
|         Returns [[x_min, y_min], [x_max, y_max]] which specify a minimal bounding box for the shape. | ||||
| 
 | ||||
|         :return: [[x_min, y_min], [x_max, y_max]] | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def rotate(self, theta: float) -> 'Shape': | ||||
|         """ | ||||
|         Rotate the shape around its center (0, 0), ignoring its offset. | ||||
| 
 | ||||
|         :param theta: Angle to rotate by (counterclockwise, radians) | ||||
|         :return: self | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def mirror(self, axis: int) -> 'Shape': | ||||
|         """ | ||||
|         Mirror the shape across an axis. | ||||
| 
 | ||||
|         :param axis: Axis to mirror across. | ||||
|         :return: self | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def scale_by(self, c: float) -> 'Shape': | ||||
|         """ | ||||
|         Scale the shape's size (eg. radius, for a circle) by a constant factor. | ||||
| 
 | ||||
|         :param c: Factor to scale by | ||||
|         :return: self | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def normalized_form(self, norm_value: int) -> normalized_shape_tuple: | ||||
|         """ | ||||
|         Writes the shape in a standardized notation, with offset, scale, rotation, and dose | ||||
|          information separated out from the remaining values. | ||||
| 
 | ||||
|         :param norm_value: This value is used to normalize lengths intrinsic to teh shape; | ||||
|                 eg. for a circle, the returned magnitude value will be (radius / norm_value), and | ||||
|                 the returned callable will create a Circle(radius=norm_value, ...). This is useful | ||||
|                 when you find it important for quantities to remain in a certain range, eg. for | ||||
|                 GDSII where vertex locations are stored as integers. | ||||
|         :return: The returned information takes the form of a 3-element tuple, | ||||
|                 (intrinsic, extrinsic, constructor). These are further broken down as: | ||||
|                 extrinsic: ([x_offset, y_offset], scale, rotation, dose) | ||||
|                 intrinsic: A tuple of basic types containing all information about the instance that | ||||
|                             is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self). | ||||
|                 constructor: A callable (no arguments) which returns an instance of type(self) with | ||||
|                             internal state equivalent to 'intrinsic'. | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     # ---- Non-abstract properties | ||||
|     # offset property | ||||
|     @property | ||||
|     def offset(self) -> numpy.ndarray: | ||||
|         """ | ||||
|         [x, y] offset | ||||
| 
 | ||||
|         :return: [x_offset, y_offset] | ||||
|         """ | ||||
|         return self._offset | ||||
| 
 | ||||
|     @offset.setter | ||||
|     def offset(self, val: vector2): | ||||
|         if not isinstance(val, numpy.ndarray): | ||||
|             val = numpy.array(val, dtype=float) | ||||
| 
 | ||||
|         if val.size != 2: | ||||
|             raise PatternError('Offset must be convertible to size-2 ndarray') | ||||
|         self._offset = val.flatten() | ||||
| 
 | ||||
|     # layer property | ||||
|     @property | ||||
|     def layer(self) -> int or Tuple[int]: | ||||
|         """ | ||||
|         Layer number (int or tuple of ints) | ||||
| 
 | ||||
|         :return: Layer | ||||
|         """ | ||||
|         return self._layer | ||||
| 
 | ||||
|     @layer.setter | ||||
|     def layer(self, val: int or List[int]): | ||||
|         self._layer = val | ||||
| 
 | ||||
|     # dose property | ||||
|     @property | ||||
|     def dose(self) -> float: | ||||
|         """ | ||||
|         Dose (float >= 0) | ||||
| 
 | ||||
|         :return: Dose value | ||||
|         """ | ||||
|         return self._dose | ||||
| 
 | ||||
|     @dose.setter | ||||
|     def dose(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Dose must be a scalar') | ||||
|         if not val >= 0: | ||||
|             raise PatternError('Dose must be non-negative') | ||||
|         self._dose = val | ||||
| 
 | ||||
|     # ---- Non-abstract methods | ||||
|     def copy(self) -> 'Shape': | ||||
|         """ | ||||
|         Returns a deep copy of the shape. | ||||
| 
 | ||||
|         :return: Deep copy of self | ||||
|         """ | ||||
|         return copy.deepcopy(self) | ||||
| 
 | ||||
|     def translate(self, offset: vector2) -> 'Shape': | ||||
|         """ | ||||
|         Translate the shape by the given offset | ||||
| 
 | ||||
|         :param offset: [x_offset, y,offset] | ||||
|         :return: self | ||||
|         """ | ||||
|         self.offset += offset | ||||
|         return self | ||||
| 
 | ||||
|     def rotate_around(self, pivot: vector2, rotation: float) -> 'Shape': | ||||
|         """ | ||||
|         Rotate the shape around a point. | ||||
| 
 | ||||
|         :param pivot: Point (x, y) to rotate around | ||||
|         :param rotation: Angle to rotate by (counterclockwise, radians) | ||||
|         :return: self | ||||
|         """ | ||||
|         pivot = numpy.array(pivot, dtype=float) | ||||
|         self.translate(-pivot) | ||||
|         self.rotate(rotation) | ||||
|         self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) | ||||
|         self.translate(+pivot) | ||||
|         return self | ||||
| 
 | ||||
|     def manhattanize_fast(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']: | ||||
|         """ | ||||
|         Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape. | ||||
| 
 | ||||
|         This function works by | ||||
|             1) Converting the shape to polygons using .to_polygons() | ||||
|             2) Approximating each edge with an equivalent Manhattan edge | ||||
|         This process results in a reasonable Manhattan representation of the shape, but is | ||||
|           imprecise near non-Manhattan or off-grid corners. | ||||
| 
 | ||||
|         :param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. | ||||
|         :param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. | ||||
|         :return: List of Polygon objects with grid-aligned edges. | ||||
|         """ | ||||
|         from . import Polygon | ||||
| 
 | ||||
|         grid_x = numpy.unique(grid_x) | ||||
|         grid_y = numpy.unique(grid_y) | ||||
| 
 | ||||
|         polygon_contours = [] | ||||
|         for polygon in self.to_polygons(): | ||||
|             mins, maxs = polygon.get_bounds() | ||||
| 
 | ||||
|             vertex_lists = [] | ||||
|             p_verts = polygon.vertices + polygon.offset | ||||
|             for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0)): | ||||
|                 dv = v_next - v | ||||
| 
 | ||||
|                 # Find x-index bounds for the line      # TODO: fix this and err_xmin/xmax for grids smaller than the line / shape | ||||
|                 gxi_range = numpy.digitize([v[0], v_next[0]], grid_x) | ||||
|                 gxi_min = numpy.min(gxi_range - 1).clip(0, len(grid_x) - 1) | ||||
|                 gxi_max = numpy.max(gxi_range).clip(0, len(grid_x)) | ||||
| 
 | ||||
|                 err_xmin = (min(v[0], v_next[0]) - grid_x[gxi_min]) / (grid_x[gxi_min + 1] - grid_x[gxi_min]) | ||||
|                 err_xmax = (max(v[0], v_next[0]) - grid_x[gxi_max - 1]) / (grid_x[gxi_max] - grid_x[gxi_max - 1]) | ||||
| 
 | ||||
|                 if err_xmin >= 0.5: | ||||
|                     gxi_min += 1 | ||||
|                 if err_xmax >= 0.5: | ||||
|                     gxi_max += 1 | ||||
| 
 | ||||
| 
 | ||||
|                 if abs(dv[0]) < 1e-20: | ||||
|                     # Vertical line, don't calculate slope | ||||
|                     xi = [gxi_min, gxi_max - 1] | ||||
|                     ys = numpy.array([v[1], v_next[1]]) | ||||
|                     yi = numpy.digitize(ys, grid_y).clip(1, len(grid_y) - 1) | ||||
|                     err_y = (ys - grid_y[yi]) / (grid_y[yi] - grid_y[yi - 1]) | ||||
|                     yi[err_y < 0.5] -= 1 | ||||
| 
 | ||||
|                     segment = numpy.column_stack((grid_x[xi], grid_y[yi])) | ||||
|                     vertex_lists.append(segment) | ||||
|                     continue | ||||
| 
 | ||||
|                 m = dv[1]/dv[0] | ||||
|                 def get_grid_inds(xes): | ||||
|                     ys = m * (xes - v[0]) + v[1] | ||||
| 
 | ||||
|                     # (inds - 1) is the index of the y-grid line below the edge's intersection with the x-grid | ||||
|                     inds = numpy.digitize(ys, grid_y).clip(1, len(grid_y) - 1) | ||||
| 
 | ||||
|                     # err is what fraction of the cell upwards we have to go to reach our y | ||||
|                     #   (can be negative at bottom edge due to clip above) | ||||
|                     err = (ys - grid_y[inds - 1]) / (grid_y[inds] - grid_y[inds - 1]) | ||||
| 
 | ||||
|                     # now set inds to the index of the nearest y-grid line | ||||
|                     inds[err < 0.5] -= 1 | ||||
|                     return inds | ||||
| 
 | ||||
|                 # Find the y indices on all x gridlines | ||||
|                 xs = grid_x[gxi_min:gxi_max] | ||||
|                 inds = get_grid_inds(xs) | ||||
| 
 | ||||
|                 # Find y-intersections for x-midpoints | ||||
|                 xs2 = (xs[:-1] + xs[1:]) / 2 | ||||
|                 inds2 = get_grid_inds(xs2) | ||||
| 
 | ||||
|                 xinds = numpy.round(numpy.arange(gxi_min, gxi_max - 0.99, 1/3)).astype(int) | ||||
| 
 | ||||
|                 # interleave the results | ||||
|                 yinds = xinds.copy() | ||||
|                 yinds[0::3] = inds | ||||
|                 yinds[1::3] = inds2 | ||||
|                 yinds[2::3] = inds2 | ||||
| 
 | ||||
|                 vlist = numpy.column_stack((grid_x[xinds], grid_y[yinds])) | ||||
|                 if dv[0] < 0: | ||||
|                     vlist = vlist[::-1] | ||||
| 
 | ||||
|                 vertex_lists.append(vlist) | ||||
|             polygon_contours.append(numpy.vstack(vertex_lists)) | ||||
| 
 | ||||
|         manhattan_polygons = [] | ||||
|         for contour in polygon_contours: | ||||
|             manhattan_polygons.append(Polygon( | ||||
|                 vertices=contour, | ||||
|                 layer=self.layer, | ||||
|                 dose=self.dose)) | ||||
| 
 | ||||
|         return manhattan_polygons | ||||
| 
 | ||||
| 
 | ||||
|     def manhattanize(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']: | ||||
|         """ | ||||
|         Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape. | ||||
| 
 | ||||
|         This function works by | ||||
|             1) Converting the shape to polygons using .to_polygons() | ||||
|             2) Accurately rasterizing each polygon on a grid, | ||||
|                 where the edges of each grid cell correspond to the allowed coordinates | ||||
|             3) Thresholding the (anti-aliased) rasterized image | ||||
|             4) Finding the contours which outline the filled areas in the thresholded image | ||||
|         This process results in a fairly accurate Manhattan representation of the shape. Possible | ||||
|           caveats include: | ||||
|             a) If high accuracy is important, perform any polygonization and clipping operations | ||||
|                 prior to calling this function. This allows you to specify any arguments you may | ||||
|                 need for .to_polygons(), and also avoids calling .manhattanize() multiple times for | ||||
|                 the same grid location (which causes inaccuracies in the final representation). | ||||
|             b) If the shape is very large or the grid very fine, memory requirements can be reduced | ||||
|                 by breaking the shape apart into multiple, smaller shapes. | ||||
|             c) Inaccuracies in edge shape can result from Manhattanization of edges which are | ||||
|                 equidistant from allowed edge location. | ||||
| 
 | ||||
|         Implementation notes: | ||||
|             i) Rasterization is performed using float_raster, giving a high-precision anti-aliased | ||||
|                 rasterized image. | ||||
|             ii) To find the exact polygon edges, the thresholded rasterized image is supersampled | ||||
|                   prior to calling skimage.measure.find_contours(), which uses marching squares | ||||
|                   to find the contours. This is done because find_contours() performs interpolation, | ||||
|                   which has to be undone in order to regain the axis-aligned contours. A targetted | ||||
|                   rewrite of find_contours() for this specific application, or use of a different | ||||
|                   boundary tracing method could remove this requirement, but for now this seems to | ||||
|                   be the most performant approach. | ||||
| 
 | ||||
|         :param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. | ||||
|         :param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. | ||||
|         :return: List of Polygon objects with grid-aligned edges. | ||||
|         """ | ||||
|         from . import Polygon | ||||
|         import skimage.measure | ||||
|         import float_raster | ||||
| 
 | ||||
|         grid_x = numpy.unique(grid_x) | ||||
|         grid_y = numpy.unique(grid_y) | ||||
| 
 | ||||
|         polygon_contours = [] | ||||
|         for polygon in self.to_polygons(): | ||||
|             # Get rid of unused gridlines (anything not within 2 lines of the polygon bounds) | ||||
|             mins, maxs = polygon.get_bounds() | ||||
|             keep_x = numpy.logical_and(grid_x > mins[0], grid_x < maxs[0]) | ||||
|             keep_y = numpy.logical_and(grid_y > mins[1], grid_y < maxs[1]) | ||||
|             for k in (keep_x, keep_y): | ||||
|                 for s in (1, 2): | ||||
|                     k[s:] += k[:-s] | ||||
|                     k[:-s] += k[s:] | ||||
|                 k = k > 0 | ||||
| 
 | ||||
|             gx = grid_x[keep_x] | ||||
|             gy = grid_y[keep_y] | ||||
| 
 | ||||
|             if len(gx) == 0 or len(gy) == 0: | ||||
|                 continue | ||||
| 
 | ||||
|             offset = (numpy.where(keep_x)[0][0], | ||||
|                       numpy.where(keep_y)[0][0]) | ||||
| 
 | ||||
|             rastered = float_raster.raster((polygon.vertices + polygon.offset).T, gx, gy) | ||||
|             binary_rastered = (numpy.abs(rastered) >= 0.5) | ||||
|             supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1) | ||||
| 
 | ||||
|             contours = skimage.measure.find_contours(supersampled, 0.5) | ||||
|             polygon_contours.append((offset, contours)) | ||||
| 
 | ||||
|         manhattan_polygons = [] | ||||
|         for offset_i, contours in polygon_contours: | ||||
|             for contour in contours: | ||||
|                 # /2 deals with supersampling | ||||
|                 # +.5 deals with the fact that our 0-edge becomes -.5 in the super-sampled contour output | ||||
|                 snapped_contour = numpy.round((contour + .5) / 2).astype(int) | ||||
|                 vertices = numpy.hstack((grid_x[snapped_contour[:, None, 0] + offset_i[0]], | ||||
|                                          grid_y[snapped_contour[:, None, 1] + offset_i[1]])) | ||||
| 
 | ||||
|                 manhattan_polygons.append(Polygon( | ||||
|                     vertices=vertices, | ||||
|                     layer=self.layer, | ||||
|                     dose=self.dose)) | ||||
| 
 | ||||
|         return manhattan_polygons | ||||
| 
 | ||||
							
								
								
									
										224
									
								
								masque/shapes/text.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								masque/shapes/text.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,224 @@ | ||||
| from typing import List, Tuple | ||||
| import numpy | ||||
| from numpy import pi, inf | ||||
| 
 | ||||
| from . import Shape, Polygon, normalized_shape_tuple | ||||
| from .. import PatternError | ||||
| from ..utils import is_scalar, vector2, get_bit | ||||
| 
 | ||||
| # Loaded on use: | ||||
| # from freetype import Face | ||||
| # from matplotlib.path import Path | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| class Text(Shape): | ||||
|     _string = '' | ||||
|     _height = 1.0 | ||||
|     _rotation = 0.0 | ||||
|     _mirrored = None | ||||
|     font_path = '' | ||||
| 
 | ||||
|     # vertices property | ||||
|     @property | ||||
|     def string(self) -> str: | ||||
|         return self._string | ||||
| 
 | ||||
|     @string.setter | ||||
|     def string(self, val: str): | ||||
|         self._string = val | ||||
| 
 | ||||
|     # Rotation property | ||||
|     @property | ||||
|     def rotation(self) -> float: | ||||
|         return self._rotation | ||||
| 
 | ||||
|     @rotation.setter | ||||
|     def rotation(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Rotation must be a scalar') | ||||
|         self._rotation = val % (2 * pi) | ||||
| 
 | ||||
|     # Height property | ||||
|     @property | ||||
|     def height(self) -> float: | ||||
|         return self._height | ||||
| 
 | ||||
|     @height.setter | ||||
|     def height(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Height must be a scalar') | ||||
|         self._height = val | ||||
| 
 | ||||
|     # Mirrored property | ||||
|     @property | ||||
|     def mirrored(self) -> List[bool]: | ||||
|         return self._mirrored | ||||
| 
 | ||||
|     @mirrored.setter | ||||
|     def mirrored(self, val: List[bool]): | ||||
|         if is_scalar(val): | ||||
|             raise PatternError('Mirrored must be a 2-element list of booleans') | ||||
|         self._mirrored = val | ||||
| 
 | ||||
|     def __init__(self, | ||||
|                  string: str, | ||||
|                  height: float, | ||||
|                  font_path: str, | ||||
|                  mirrored: List[bool]=None, | ||||
|                  rotation: float=0.0, | ||||
|                  offset: vector2=(0.0, 0.0), | ||||
|                  layer: int=0, | ||||
|                  dose: float=1.0): | ||||
|         self.offset = offset | ||||
|         self.layer = layer | ||||
|         self.dose = dose | ||||
|         self.string = string | ||||
|         self.height = height | ||||
|         self.rotation = rotation | ||||
|         self.font_path = font_path | ||||
|         if mirrored is None: | ||||
|             mirrored = [False, False] | ||||
|         self.mirrored = mirrored | ||||
| 
 | ||||
|     def to_polygons(self, | ||||
|                     _poly_num_points: int=None, | ||||
|                     _poly_max_arclen: float=None | ||||
|                     ) -> List[Polygon]: | ||||
|         all_polygons = [] | ||||
|         total_advance = 0 | ||||
|         for char in self.string: | ||||
|             raw_polys, advance = get_char_as_polygons(self.font_path, char) | ||||
| 
 | ||||
|             # Move these polygons to the right of the previous letter | ||||
|             for xys in raw_polys: | ||||
|                 poly = Polygon(xys, dose=self.dose, layer=self.layer) | ||||
|                 [poly.mirror(ax) for ax, do in enumerate(self.mirrored) if do] | ||||
|                 poly.scale_by(self.height) | ||||
|                 poly.offset = self.offset + [total_advance, 0] | ||||
|                 poly.rotate_around(self.offset, self.rotation) | ||||
|                 all_polygons += [poly] | ||||
| 
 | ||||
|             # Update the list of all polygons and how far to advance | ||||
|             total_advance += advance * self.height | ||||
| 
 | ||||
|         return all_polygons | ||||
| 
 | ||||
|     def rotate(self, theta: float) -> 'Text': | ||||
|         self.rotation += theta | ||||
|         return self | ||||
| 
 | ||||
|     def mirror(self, axis: int) -> 'Text': | ||||
|         self.mirrored[axis] = not self.mirrored[axis] | ||||
|         return self | ||||
| 
 | ||||
|     def scale_by(self, c: float) -> 'Text': | ||||
|         self.height *= c | ||||
|         return self | ||||
| 
 | ||||
|     def normalized_form(self, norm_value: float) -> normalized_shape_tuple: | ||||
|         return (type(self), self.string, self.font_path, self.mirrored, self.layer), \ | ||||
|                (self.offset, self.height / norm_value, self.rotation, self.dose), \ | ||||
|                lambda: Text(string=self.string, | ||||
|                             height=self.height * norm_value, | ||||
|                             font_path=self.font_path, | ||||
|                             mirrored=self.mirrored, | ||||
|                             layer=self.layer) | ||||
| 
 | ||||
|     def get_bounds(self) -> numpy.ndarray: | ||||
|         # rotation makes this a huge pain when using slot.advance and glyph.bbox(), so | ||||
|         #  just convert to polygons instead | ||||
|         bounds = [[+inf, +inf], [-inf, -inf]] | ||||
|         polys = self.to_polygons() | ||||
|         for poly in polys: | ||||
|             poly_bounds = poly.get_bounds() | ||||
|             bounds[0, :] = numpy.minimum(bounds[0, :], poly_bounds[0, :]) | ||||
|             bounds[1, :] = numpy.maximum(bounds[1, :], poly_bounds[1, :]) | ||||
| 
 | ||||
|         return bounds | ||||
| 
 | ||||
| 
 | ||||
| def get_char_as_polygons(font_path: str, | ||||
|                          char: str, | ||||
|                          resolution: float=48*64, | ||||
|                          ) -> Tuple[List[List[List[float]]], float]: | ||||
|     from freetype import Face | ||||
|     from matplotlib.path import Path | ||||
| 
 | ||||
|     """ | ||||
|     Get a list of polygons representing a single character. | ||||
| 
 | ||||
|     The output is normalized so that the font size is 1 unit. | ||||
| 
 | ||||
|     :param font_path: File path specifying a font loadable by freetype | ||||
|     :param char: Character to convert to polygons | ||||
|     :param resolution: Internal resolution setting (used for freetype | ||||
|             Face.set_font_size(resolution)). Modify at your own peril! | ||||
|     :return: List of polygons [[[x0, y0], [x1, y1], ...], ...] and 'advance' distance (distance | ||||
|             from the start of this glyph to the start of the next one) | ||||
|     """ | ||||
|     if len(char) != 1: | ||||
|         raise Exception('get_char_as_polygons called with non-char') | ||||
| 
 | ||||
|     face = Face(font_path) | ||||
|     face.set_char_size(resolution) | ||||
|     face.load_char(char) | ||||
|     slot = face.glyph | ||||
|     outline = slot.outline | ||||
| 
 | ||||
|     start = 0 | ||||
|     all_verts, all_codes = [], [] | ||||
|     for end in outline.contours: | ||||
|         points = outline.points[start:end + 1] | ||||
|         points.append(points[0]) | ||||
| 
 | ||||
|         tags = outline.tags[start:end + 1] | ||||
|         tags.append(tags[0]) | ||||
| 
 | ||||
|         segments = [] | ||||
|         for j, point in enumerate(points): | ||||
|             # If we already have a segment, add this point to it | ||||
|             if j > 0: | ||||
|                 segments[-1].append(point) | ||||
| 
 | ||||
|             # If not bezier control point, start next segment | ||||
|             if get_bit(tags[j], 0) and j < (len(points) - 1): | ||||
|                 segments.append([point]) | ||||
| 
 | ||||
|         verts = [points[0]] | ||||
|         codes = [Path.MOVETO] | ||||
|         for segment in segments: | ||||
|             if len(segment) == 2: | ||||
|                 verts.extend(segment[1:]) | ||||
|                 codes.extend([Path.LINETO]) | ||||
|             elif len(segment) == 3: | ||||
|                 verts.extend(segment[1:]) | ||||
|                 codes.extend([Path.CURVE3, Path.CURVE3]) | ||||
|             else: | ||||
|                 verts.append(segment[1]) | ||||
|                 codes.append(Path.CURVE3) | ||||
|                 for i in range(1, len(segment) - 2): | ||||
|                     a, b = segment[i], segment[i + 1] | ||||
|                     c = ((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0) | ||||
|                     verts.extend([c, b]) | ||||
|                     codes.extend([Path.CURVE3, Path.CURVE3]) | ||||
|                 verts.append(segment[-1]) | ||||
|                 codes.append(Path.CURVE3) | ||||
|         all_verts.extend(verts) | ||||
|         all_codes.extend(codes) | ||||
|         start = end + 1 | ||||
| 
 | ||||
|     all_verts = numpy.array(all_verts) / resolution | ||||
| 
 | ||||
|     advance = slot.advance.x / resolution | ||||
| 
 | ||||
|     if len(all_verts) == 0: | ||||
|         polygons = [] | ||||
|     else: | ||||
|         path = Path(all_verts, all_codes) | ||||
|         path.should_simplify = False | ||||
|         polygons = path.to_polygons() | ||||
| 
 | ||||
|     return polygons, advance | ||||
							
								
								
									
										202
									
								
								masque/subpattern.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								masque/subpattern.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,202 @@ | ||||
| """ | ||||
|  SubPattern provides basic support for nesting Pattern objects within each other, by adding | ||||
|   offset, rotation, scaling, and other such properties to the reference. | ||||
| """ | ||||
| 
 | ||||
| from typing import Union, List | ||||
| import copy | ||||
| 
 | ||||
| import numpy | ||||
| from numpy import pi | ||||
| 
 | ||||
| from .error import PatternError | ||||
| from .utils import is_scalar, rotation_matrix_2d, vector2 | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
| 
 | ||||
| 
 | ||||
| class SubPattern: | ||||
|     """ | ||||
|     SubPattern provides basic support for nesting Pattern objects within each other, by adding | ||||
|      offset, rotation, scaling, and associated methods. | ||||
|     """ | ||||
| 
 | ||||
|     pattern = None          # type: Pattern | ||||
|     _offset = (0.0, 0.0)    # type: numpy.ndarray | ||||
|     _rotation = 0.0         # type: float | ||||
|     _dose = 1.0             # type: float | ||||
|     _scale = 1.0            # type: float | ||||
|     _mirrored = None        # type: List[bool] | ||||
| 
 | ||||
|     def __init__(self, | ||||
|                  pattern: 'Pattern', | ||||
|                  offset: vector2=(0.0, 0.0), | ||||
|                  rotation: float=0.0, | ||||
|                  mirrored: List[bool]=None, | ||||
|                  dose: float=1.0, | ||||
|                  scale: float=1.0): | ||||
|         self.pattern = pattern | ||||
|         self.offset = offset | ||||
|         self.rotation = rotation | ||||
|         self.dose = dose | ||||
|         self.scale = scale | ||||
|         if mirrored is None: | ||||
|             mirrored = [False, False] | ||||
|         self.mirrored = mirrored | ||||
| 
 | ||||
|     # offset property | ||||
|     @property | ||||
|     def offset(self) -> numpy.ndarray: | ||||
|         return self._offset | ||||
| 
 | ||||
|     @offset.setter | ||||
|     def offset(self, val: vector2): | ||||
|         if not isinstance(val, numpy.ndarray): | ||||
|             val = numpy.array(val, dtype=float) | ||||
| 
 | ||||
|         if val.size != 2: | ||||
|             raise PatternError('Offset must be convertible to size-2 ndarray') | ||||
|         self._offset = val.flatten().astype(float) | ||||
| 
 | ||||
|     # dose property | ||||
|     @property | ||||
|     def dose(self) -> float: | ||||
|         return self._dose | ||||
| 
 | ||||
|     @dose.setter | ||||
|     def dose(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Dose must be a scalar') | ||||
|         if not val >= 0: | ||||
|             raise PatternError('Dose must be non-negative') | ||||
|         self._dose = val | ||||
| 
 | ||||
|     # scale property | ||||
|     @property | ||||
|     def scale(self) -> float: | ||||
|         return self._scale | ||||
| 
 | ||||
|     @scale.setter | ||||
|     def scale(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Scale must be a scalar') | ||||
|         if not val > 0: | ||||
|             raise PatternError('Scale must be positive') | ||||
|         self._scale = val | ||||
| 
 | ||||
|     # Rotation property [ccw] | ||||
|     @property | ||||
|     def rotation(self) -> float: | ||||
|         return self._rotation | ||||
| 
 | ||||
|     @rotation.setter | ||||
|     def rotation(self, val: float): | ||||
|         if not is_scalar(val): | ||||
|             raise PatternError('Rotation must be a scalar') | ||||
|         self._rotation = val % (2 * pi) | ||||
| 
 | ||||
|     # Mirrored property | ||||
|     @property | ||||
|     def mirrored(self) -> List[bool]: | ||||
|         return self._mirrored | ||||
| 
 | ||||
|     @mirrored.setter | ||||
|     def mirrored(self, val: List[bool]): | ||||
|         if is_scalar(val): | ||||
|             raise PatternError('Mirrored must be a 2-element list of booleans') | ||||
|         self._mirrored = val | ||||
| 
 | ||||
|     def as_pattern(self) -> 'Pattern': | ||||
|         """ | ||||
|         Returns a copy of self.pattern which has been scaled, rotated, etc. according to this | ||||
|          SubPattern's properties. | ||||
|         :return: Copy of self.pattern that has been altered to reflect the SubPattern's properties. | ||||
|         """ | ||||
|         pattern = self.pattern.deepcopy() | ||||
|         pattern.scale_by(self.scale) | ||||
|         [pattern.mirror(ax) for ax, do in enumerate(self.mirrored) if do] | ||||
|         pattern.rotate_around((0.0, 0.0), self.rotation) | ||||
|         pattern.translate_elements(self.offset) | ||||
|         pattern.scale_element_doses(self.dose) | ||||
|         return pattern | ||||
| 
 | ||||
|     def translate(self, offset: vector2) -> 'SubPattern': | ||||
|         """ | ||||
|         Translate by the given offset | ||||
| 
 | ||||
|         :param offset: Translate by this offset | ||||
|         :return: self | ||||
|         """ | ||||
|         self.offset += offset | ||||
|         return self | ||||
| 
 | ||||
|     def rotate_around(self, pivot: vector2, rotation: float) -> 'SubPattern': | ||||
|         """ | ||||
|         Rotate around a point | ||||
| 
 | ||||
|         :param pivot: Point to rotate around | ||||
|         :param rotation: Angle to rotate by (counterclockwise, radians) | ||||
|         :return: self | ||||
|         """ | ||||
|         pivot = numpy.array(pivot, dtype=float) | ||||
|         self.translate(-pivot) | ||||
|         self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) | ||||
|         self.rotate(rotation) | ||||
|         self.translate(+pivot) | ||||
|         return self | ||||
| 
 | ||||
|     def rotate(self, rotation: float) -> 'SubPattern': | ||||
|         """ | ||||
|         Rotate around (0, 0) | ||||
| 
 | ||||
|         :param rotation: Angle to rotate by (counterclockwise, radians) | ||||
|         :return: self | ||||
|         """ | ||||
|         self.rotation += rotation | ||||
|         return self | ||||
| 
 | ||||
|     def mirror(self, axis: int) -> 'SubPattern': | ||||
|         """ | ||||
|         Mirror the subpattern across an axis. | ||||
| 
 | ||||
|         :param axis: Axis to mirror across. | ||||
|         :return: self | ||||
|         """ | ||||
|         self.mirrored[axis] = not self.mirrored[axis] | ||||
|         return self | ||||
| 
 | ||||
|     def get_bounds(self) -> numpy.ndarray or None: | ||||
|         """ | ||||
|         Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the | ||||
|          extent of the SubPattern in each dimension. | ||||
|         Returns None if the contained Pattern is empty. | ||||
| 
 | ||||
|         :return: [[x_min, y_min], [x_max, y_max]] or None | ||||
|         """ | ||||
|         return self.as_pattern().get_bounds() | ||||
| 
 | ||||
|     def scale_by(self, c: float) -> 'SubPattern': | ||||
|         """ | ||||
|         Scale the subpattern by a factor | ||||
| 
 | ||||
|         :param c: scaling factor | ||||
|         """ | ||||
|         self.scale *= c | ||||
|         return self | ||||
| 
 | ||||
|     def copy(self) -> 'SubPattern': | ||||
|         """ | ||||
|         Return a shallow copy of the subpattern. | ||||
| 
 | ||||
|         :return: copy.copy(self) | ||||
|         """ | ||||
|         return copy.copy(self) | ||||
| 
 | ||||
|     def deepcopy(self) -> 'SubPattern': | ||||
|         """ | ||||
|         Return a deep copy of the subpattern. | ||||
| 
 | ||||
|         :return: copy.copy(self) | ||||
|         """ | ||||
|         return copy.deepcopy(self) | ||||
							
								
								
									
										89
									
								
								masque/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								masque/utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | ||||
| """ | ||||
| Various helper functions | ||||
| """ | ||||
| 
 | ||||
| from typing import Any, Union, Tuple | ||||
| 
 | ||||
| import numpy | ||||
| 
 | ||||
| # Type definitions | ||||
| vector2 = Union[numpy.ndarray, Tuple[float, float]] | ||||
| 
 | ||||
| 
 | ||||
| def is_scalar(var: Any) -> bool: | ||||
|     """ | ||||
|     Alias for 'not hasattr(var, "__len__")' | ||||
| 
 | ||||
|     :param var: Checks if var has a length. | ||||
|     """ | ||||
|     return not hasattr(var, "__len__") | ||||
| 
 | ||||
| 
 | ||||
| def get_bit(bit_string: Any, bit_id: int) -> bool: | ||||
|     """ | ||||
|     Returns true iff bit number 'bit_id' from the right of 'bit_string' is 1 | ||||
| 
 | ||||
|     :param bit_string: Bit string to test | ||||
|     :param bit_id: Bit number, 0-indexed from the right (lsb) | ||||
|     :return: value of the requested bit (bool) | ||||
|     """ | ||||
|     return bit_string & (1 << bit_id) != 0 | ||||
| 
 | ||||
| 
 | ||||
| def set_bit(bit_string: Any, bit_id: int, value: bool) -> Any: | ||||
|     """ | ||||
|     Returns 'bit_string' with bit number 'bit_id' set to 'value'. | ||||
| 
 | ||||
|     :param bit_string: Bit string to alter | ||||
|     :param bit_id: Bit number, 0-indexed from right (lsb) | ||||
|     :param value: Boolean value to set bit to | ||||
|     :return: Altered 'bit_string' | ||||
|     """ | ||||
|     mask = (1 << bit_id) | ||||
|     bit_string &= ~mask | ||||
|     if value: | ||||
|         bit_string |= mask | ||||
|     return bit_string | ||||
| 
 | ||||
| 
 | ||||
| def rotation_matrix_2d(theta: float) -> numpy.ndarray: | ||||
|     """ | ||||
|     2D rotation matrix for rotating counterclockwise around the origin. | ||||
| 
 | ||||
|     :param theta: Angle to rotate, in radians | ||||
|     :return: rotation matrix | ||||
|     """ | ||||
|     return numpy.array([[numpy.cos(theta), -numpy.sin(theta)], | ||||
|                         [numpy.sin(theta), +numpy.cos(theta)]]) | ||||
| 
 | ||||
| 
 | ||||
| def remove_duplicate_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray: | ||||
|         duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1) | ||||
|         if not closed_path: | ||||
|             duplicates[0] = False | ||||
|         return vertices[~duplicates] | ||||
| 
 | ||||
| 
 | ||||
| def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray: | ||||
|         ''' | ||||
|         Given a list of vertices, remove any superflous vertices (i.e. | ||||
|             those which lie along the line formed by their neighbors) | ||||
| 
 | ||||
|         :param vertices: Nx2 ndarray of vertices | ||||
|         :param closed_path: If True, the vertices are assumed to represent an implicitly | ||||
|             closed path. If False, the path is assumed to be open. Default True. | ||||
|         :return: | ||||
|         ''' | ||||
|         # Check for dx0/dy0 == dx1/dy1 | ||||
| 
 | ||||
|         dv = numpy.roll(vertices, 1, axis=0) - vertices #[y0 - yn1, y1-y0, ...] | ||||
|         dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] # [[dx1*dy0, dx1*dy0], ...] | ||||
| 
 | ||||
|         dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0] | ||||
|         err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40 | ||||
| 
 | ||||
|         slopes_equal = (dxdy_diff / err_mult) < 1e-15 | ||||
|         if not closed_path: | ||||
|             slopes_equal[[0, -1]] = False | ||||
| 
 | ||||
|         return vertices[~slopes_equal] | ||||
							
								
								
									
										41
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| from setuptools import setup, find_packages | ||||
| import masque | ||||
| 
 | ||||
| with open('README.md', 'r') as f: | ||||
|     long_description = f.read() | ||||
| 
 | ||||
| setup(name='masque', | ||||
|       version=masque.version, | ||||
|       description='Lithography mask library', | ||||
|       long_description=long_description, | ||||
|       long_description_content_type='text/markdown', | ||||
|       author='Jan Petykiewicz', | ||||
|       author_email='anewusername@gmail.com', | ||||
|       url='https://mpxd.net/code/jan/masque', | ||||
|       packages=find_packages(), | ||||
|       install_requires=[ | ||||
|             'numpy', | ||||
|       ], | ||||
|       extras_require={ | ||||
|           'visualization': ['matplotlib'], | ||||
|           'gdsii': ['python-gdsii'], | ||||
|           'svg': ['svgwrite'], | ||||
|           'text': ['freetype-py', 'matplotlib'], | ||||
|       }, | ||||
|       classifiers=[ | ||||
|             'Programming Language :: Python :: 3', | ||||
|             'Development Status :: 4 - Beta', | ||||
|             'Intended Audience :: Developers', | ||||
|             'Intended Audience :: Information Technology', | ||||
|             'Intended Audience :: Manufacturing', | ||||
|             'Intended Audience :: Science/Research', | ||||
|             'License :: OSI Approved :: GNU Affero General Public License v3', | ||||
|             'Operating System :: POSIX :: Linux', | ||||
|             'Operating System :: Microsoft :: Windows', | ||||
|             'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)', | ||||
|             'Topic :: Scientific/Engineering :: Visualization', | ||||
|       ], | ||||
|       ) | ||||
| 
 | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user