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