diff --git a/.gitignore b/.gitignore index bcdc360..00b2db4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +AUTHORS MANIFEST -build -dist +build/ +dist/ +igor.egg-info/ *.pyc diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..c591e2a --- /dev/null +++ b/.mailmap @@ -0,0 +1,2 @@ +W. Trevor King +Paul Kienzle diff --git a/.update-copyright.conf b/.update-copyright.conf new file mode 100644 index 0000000..d5ffe5e --- /dev/null +++ b/.update-copyright.conf @@ -0,0 +1,18 @@ +[project] +name: igor +vcs: Git + +[files] +authors: yes +files: yes +ignored: COPYING | README | .update-copyright.conf | .git* | test/* + +[copyright] +short: {project} comes with ABSOLUTELY NO WARRANTY and is licensed under the GNU Lesser General Public License. +long: This file is part of {project}. + + {project} is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + {project} 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with {project}. If not, see . diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/COPYING.LESSER b/COPYING.LESSER new file mode 100644 index 0000000..fc8a5de --- /dev/null +++ b/COPYING.LESSER @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser 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 +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f6725a8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include COPYING +include COPYING.LESSER diff --git a/README b/README new file mode 100644 index 0000000..dc21b62 --- /dev/null +++ b/README @@ -0,0 +1,138 @@ +==== +Igor +==== + +:Authors: W. Trevor King ; + Paul Kienzle +:License: GNU General Public License, version 3+ + +Python parsers for Igor Binary Waves (.ibw) and Packed Experiment +(.pxp) files written by WaveMetrics' IGOR Pro software. + +Installation +============ + +Packages +-------- + +If `igor` already exists in your package manager's repository, you +should install `igor` in the usual way. + +Gentoo +~~~~~~ + +I've packaged `igor` for Gentoo. You need layman_ and my `wtk +overlay`_. Install with:: + + # emerge -av app-portage/layman + # layman --add wtk + # emerge -av sci-misc/igor + +Dependencies +------------ + +If you're installing by hand or packaging `igor` for another +distribution, you'll need the following dependencies: + +=========== ================= ============================ +Package Debian_ Gentoo_ +=========== ================= ============================ +Numpy_ python-numpy dev-python/numpy +Matplotlib_ python-matplotlib dev-python/matplotlib +Nose_ python-nose dev-python/nose +=========== ================= ============================ + +Installing by hand +------------------ + +`igor` is available as a Git_ repository:: + + $ git clone git://tremily.us/igor.git + +See the homepage_ for details. To install the checkout, run the +standard:: + + $ python setup.py install + +You can also automate this installation with pip_:: + + $ pip install igor + +Usage +===== + +See the docstrings and unit tests for examples using the Python API. +The package also installs to scripts, ``igorbinarywave.py`` and +``igorpackedexperiment.py`` which can be used to dump files to stdout. +For details on their usage, use the ``--help`` option. For example:: + + $ igorbinarywave.py --help + +For users transitioning from igor.py_, there's a compatibility module +exposing the old interface. Just change:: + + import igor + +to:: + + import igor.igorpy as igor + +in your calling code. + +Testing +======= + +Run internal unit tests with:: + + $ nosetests --with-doctest --doctest-tests igor test + +The data in the ``test/data`` directory is in the Git repository, but +it is not bundled with the source code. If you want the test data, +you'll have to clone the Git repository or download a snapshot. + +Licence +======= + +This project is distributed under the `GNU Lesser General Public +License Version 3`_ or greater, see the ``COPYING`` file distributed +with the project for details. + +Maintenance +=========== + +Maintainer +---------- + +W. Trevor King +wking@tremily.us +Copyright 2008-2012 + +Release procedure +----------------- + +When a new version of the package is ready, increment __version__ +in ``igor/__init__.py`` and run update-copyright_:: + + $ update-copyright.py + +to update the copyright blurbs. Then run:: + + $ python setup.py sdist upload + +This will place a new version on PyPI. + + +.. _layman: http://layman.sourceforge.net/ +.. _wtk overlay: http://blog.tremily.us/posts/Gentoo_overlay/ +.. _Debian: http://www.debian.org/ +.. _Gentoo: http://www.gentoo.org/ +.. _NumPy: http://numpy.scipy.org/ +.. _Matplotlib: http://matplotlib.sourceforge.net/ +.. _Nose: http://somethingaboutorange.com/mrl/projects/nose/ +.. _Git: http://git-scm.com/ +.. _homepage: http://blog.tremily.us/posts/igor/ +.. _pip: http://pypi.python.org/pypi/pip +.. _igor.py: http://pypi.python.org/pypi/igor.py +.. _GNU Lesser General Public License Version 3: + http://www.gnu.org/licenses/lgpl.txt +.. _update-copyright: http://blog.tremily.us/posts/update-copyright/ diff --git a/README.rst b/README.rst deleted file mode 100644 index 8dfe6d4..0000000 --- a/README.rst +++ /dev/null @@ -1,43 +0,0 @@ -Igor.py -======= - -:Author: Paul Kienzle -:License: This program is public domain - -Read Igor Pro files from python. - -Install -------- - -Using pip:: - - $ pip install igor.py - -Using source, download and expand the source tree, change to the source -directory and type:: - - $ python setup.py install - -Change History --------------- - -0.9 2011-10-14 Merlijn van Deen - -* access to data object using f.name in addition to f['name'] and f[i] -* allow a data object to be used directly as an array, e.g., numpy.sum(f.name) -* better unicode handling - -0.8 2011-04-27 Paul Kienzle - -* initial release - -Maintenance ------------ - -When a new version of the package is ready, increment __version__ -in igor.py and enter:: - - $ python setup.py sdist upload - -This will place a new version on pypi. - diff --git a/bin/igorbinarywave.py b/bin/igorbinarywave.py new file mode 100755 index 0000000..70c1bc3 --- /dev/null +++ b/bin/igorbinarywave.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +"IBW -> ASCII conversion" + +import pprint + +import numpy + +from igor.binarywave import load +from igor.script import Script + + +class WaveScript (Script): + def _run(self, args): + wave = load(args.infile) + numpy.savetxt( + args.outfile, wave['wave']['wData'], fmt='%g', delimiter='\t') + self.plot_wave(args, wave) + if args.verbose > 0: + wave['wave'].pop('wData') + pprint.pprint(wave) + + +s = WaveScript(description=__doc__) +s.run() diff --git a/bin/igorpackedexperiment.py b/bin/igorpackedexperiment.py new file mode 100755 index 0000000..0a08444 --- /dev/null +++ b/bin/igorpackedexperiment.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +"PXP -> ASCII conversion" + +import pprint + +from igor.packed import load, walk +from igor.record.wave import WaveRecord +from igor.script import Script + + +class PackedScript (Script): + def _run(self, args): + self.args = args + records,filesystem = load(args.infile) + if hasattr(args.outfile, 'write'): + f = args.outfile # filename is actually a stream object + else: + f = open(args.outfile, 'w') + try: + f.write(pprint.pformat(records)) + f.write('\n') + finally: + if f != args.outfile: + f.close() + if args.verbose > 0: + pprint.pprint(filesystem) + walk(filesystem, self._plot_wave_callback) + + def _plot_wave_callback(self, dirpath, key, value): + if isinstance(value, WaveRecord): + self.plot_wave(self.args, value.wave, title=dirpath + [key]) + + +s = PackedScript( + description=__doc__, filetype='IGOR Packed Experiment (.pxp) file') +s.run() diff --git a/igor.py b/igor.py deleted file mode 100644 index f68c7fc..0000000 --- a/igor.py +++ /dev/null @@ -1,440 +0,0 @@ -# This program is in the public domain -""" -IGOR file reader. - -igor.load('filename') or igor.loads('data') loads the content of an igore file -into memory as a folder structure. - -Returns the root folder. - -Folders have name, path and children. -Children can be indexed by folder[i] or by folder['name']. -To see the whole tree, use: print folder.format() - -The usual igor folder types are given in the technical reports -PTN003.ifn and TN003.ifn. -""" -__version__="0.9" - -import struct -import numpy -import sys -import re - -decode = lambda s: s.decode(sys.getfilesystemencoding()) - -PYKEYWORDS = set(('and','as','assert','break','class','continue', - 'def','elif','else','except','exec','finally', - 'for','global','if','import','in','is','lambda', - 'or','pass','print','raise','return','try','with', - 'yield')) -PYID = re.compile(r"^[^\d\W]\w*$", re.UNICODE) -def valid_identifier(s): - """Check if a name is a valid identifier""" - return PYID.match(s) and s not in PYKEYWORDS - -NUMTYPE = { - 1: numpy.complex64, - 2: numpy.float32, - 3: numpy.complex64, - 4: numpy.float64, - 5: numpy.complex128, - 8: numpy.int8, - 16: numpy.int16, - 32: numpy.int32, - 64+ 8: numpy.uint8, - 64+16: numpy.uint16, - 64+32: numpy.uint32, -} - -ORDER_NUMTYPE = { - 1: 'c8', - 2: 'f4', - 3: 'c8', - 4: 'f8', - 5: 'c16', - 8: 'i1', - 16: 'i2', - 32: 'i4', - 64+ 8: 'u2', - 64+16: 'u2', - 64+32: 'u4', -} - - -class IgorObject(object): - """ Parent class for all objects the parser can return """ - pass - -class Formula(IgorObject): - def __init__(self, formula, value): - self.formula = formula - self.value = value - -class Variables(IgorObject): - """ - Contains system numeric variables (e.g., K0) and user numeric and string variables. - """ - def __init__(self, data, order): - version, = struct.unpack(order+"h",data[:2]) - if version == 1: - pos = 8 - nSysVar, nUserVar, nUserStr \ - = struct.unpack(order+"hhh",data[2:pos]) - nDepVar, nDepStr = 0, 0 - elif version == 2: - pos = 12 - nSysVar, nUservar, nUserStr, nDepVar, nDepStr \ - = struct.unpack(order+"hhh",data[2:pos]) - else: - raise ValueError("Unknown variable record version "+str(version)) - self.sysvar, pos = _parse_sys_numeric(nSysVar, order, data, pos) - self.uservar, pos = _parse_user_numeric(nUserVar, order, data, pos) - if version == 1: - self.userstr, pos = _parse_user_string1(nUserStr, order, data, pos) - else: - self.userstr, pos = _parse_user_string2(nUserStr, order, data, pos) - self.depvar, pos = _parse_dep_numeric(nDepVar, order, data, pos) - self.depstr, pos = _parse_dep_string(nDepStr, order, data, pos) - def format(self, indent=0): - return " "*indent+""\ - %(len(self.sysvar), - len(self.uservar)+len(self.userstr), - len(self.depvar)+len(self.depstr)) - -class History(IgorObject): - """ - Contains the experiment's history as plain text. - """ - def __init__(self, data, order): self.data = data - def format(self, indent=0): - return " "*indent+"" - -class Wave(IgorObject): - """ - Contains the data for a wave - """ - def __init__(self, data, order): - version, = struct.unpack(order+'h',data[:2]) - if version == 1: - pos = 8 - extra_offset,checksum = struct.unpack(order+'ih',data[2:pos]) - formula_size = note_size = pic_size = 0 - elif version == 2: - pos = 16 - extra_offset,note_size,pic_size,checksum \ - = struct.unpack(order+'iiih',data[2:pos]) - formula_size = 0 - elif version == 3: - pos = 20 - extra_offset,note_size,formula_size,pic_size,checksum \ - = struct.unpack(order+'iiiih',data[2:pos]) - elif version == 5: - checksum,extra_offset,formula_size,note_size, \ - = struct.unpack(order+'hiii',data[2:16]) - Esize = struct.unpack(order+'iiiiiiiii',data[16:52]) - textindsize, = struct.unpack('i',data[52:56]) - picsize = 0 - pos = 64 - else: - raise ValueError("unknown wave version "+str(version)) - extra_offset += pos - - if version in (1,2,3): - type, = struct.unpack(order+'h',data[pos:pos+2]) - name = data[pos+6:data.find(chr(0),pos+6,pos+26)] - #print "name3",name,type - data_units = data[pos+34:data.find(chr(0),pos+34,pos+38)] - xaxis = data[pos+38:data.find(chr(0),pos+38,pos+42)] - points, = struct.unpack(order+'i',data[pos+42:pos+46]) - hsA,hsB = struct.unpack(order+'dd',data[pos+48:pos+64]) - fsValid,fsTop,fsBottom \ - = struct.unpack(order+'hdd',data[pos+70:pos+88]) - created,_,modified = struct.unpack(order+'IhI',data[pos+98:pos+108]) - pos += 110 - dims = (points,0,0,0) - sf = (hsA,0,0,0,hsB,0,0,0) - axis_units = (xaxis,"","","") - else: # version is 5 - created,modified,points,type \ - = struct.unpack(order+'IIih',data[pos+4:pos+18]) - name = data[pos+28:data.find(chr(0),pos+28,pos+60)] - #print "name5",name,type - dims = struct.unpack(order+'iiii',data[pos+68:pos+84]) - sf = struct.unpack(order+'dddddddd',data[pos+84:pos+148]) - data_units = data[pos+148:data.find(chr(0),pos+148,pos+152)] - axis_units = tuple(data[pos+152+4*i - : data.find(chr(0),pos+152+4*i,pos+156+4*i)] - for i in range(4)) - fsValid,_,fsTop,fsBottom \ - = struct.unpack(order+'hhdd',data[pos+172:pos+192]) - pos += 320 - - if type == 0: - text = data[pos:extra_offset] - textind = numpy.fromstring(data[-textindsize:], order+'i') - textind = numpy.hstack((0,textind)) - value = [text[textind[i]:textind[i+1]] - for i in range(len(textind)-1)] - else: - trimdims = tuple(d for d in dims if d) - dtype = order+ORDER_NUMTYPE[type] - size = int(dtype[2:])*numpy.prod(trimdims) - value = numpy.fromstring(data[pos:pos+size],dtype) - value = value.reshape(trimdims) - - pos = extra_offset - formula = data[pos:pos+formula_size] - pos += formula_size - notes = data[pos:pos+note_size] - pos += note_size - if version == 5: - offset = numpy.cumsum(numpy.hstack((pos,Esize))) - Edata_units = data[offset[0]:offset[1]] - Eaxis_units = [data[offset[i]:offset[i+1]] for i in range(1,5)] - Eaxis_labels = [data[offset[i]:offset[i+1]] for i in range(5,9)] - if Edata_units: data_units = Edata_units - for i,u in enumerate(Eaxis_units): - if u: axis_units[i] = u - axis_labels = Eaxis_labels - pos = offset[-1] - - - self.name = decode(name) - self.data = value - self.data_units = data_units - self.axis_units = axis_units - self.fs,self.fstop,self.fsbottom = fsValid,fsTop,fsBottom - self.axis = [numpy.linspace(a,b,n) - for a,b,n in zip(sf[:4],sf[4:],dims)] - self.formula = formula - self.notes = notes - def format(self, indent=0): - if isinstance(self.data, list): - type,size = "text", "%d"%len(self.data) - else: - type,size = "data", "x".join(str(d) for d in self.data.shape) - return " "*indent+"%s %s (%s)"%(self.name, type, size) - - def __array__(self): - return self.data - - __repr__ = __str__ = lambda s: u"" % s.format() - -class Recreation(IgorObject): - """ - Contains the experiment's recreation procedures as plain text. - """ - def __init__(self, data, order): self.data = data - def format(self, indent=0): - return " "*indent + "" -class Procedure(IgorObject): - """ - Contains the experiment's main procedure window text as plain text. - """ - def __init__(self, data, order): self.data = data - def format(self, indent=0): - return " "*indent + "" -class GetHistory(IgorObject): - """ - Not a real record but rather, a message to go back and read the history text. - - The reason for GetHistory is that IGOR runs Recreation when it loads the - datafile. This puts entries in the history that shouldn't be there. The - GetHistory entry simply says that the Recreation has run, and the History - can be restored from the previously saved value. - """ - def __init__(self, data, order): self.data = data - def format(self, indent=0): - return " "*indent + "" -class PackedFile(IgorObject): - """ - Contains the data for a procedure file or notebook in packed form. - """ - def __init__(self, data, order): self.data = data - def format(self, indent=0): - return " "*indent + "" -class Unknown(IgorObject): - """ - Record type not documented in PTN003/TN003. - """ - def __init__(self, data, order, type): - self.data = data - self.type = type - def format(self, indent=0): - return " "*indent + ""%self.type - -class _FolderStart(IgorObject): - """ - Marks the start of a new data folder. - """ - def __init__(self, data, order): - self.name = decode(data[:data.find(chr(0))]) -class _FolderEnd(IgorObject): - """ - Marks the end of a data folder. - """ - def __init__(self, data, order): self.data = data - -class Folder(IgorObject): - """ - Hierarchical record container. - """ - def __init__(self, path): - self.name = path[-1] - self.path = path - self.children = [] - - def __getitem__(self, key): - if isinstance(key, int): - return self.children[key] - else: - for r in self.children: - if isinstance(r, (Folder,Wave)) and r.name == key: - return r - raise KeyError("Folder %s does not exist"%key) - - def __str__(self): - return u"" % "/".join(self.path) - - __repr__ = __str__ - - def append(self, record): - """ - Add a record to the folder. - """ - self.children.append(record) - try: - # Record may not have a name, the name may be invalid, or it - # may already be in use. The noname case will be covered by - # record.name raising an attribute error. The others we need - # to test for explicitly. - if valid_identifier(record.name) and not hasattr(self, record.name): - setattr(self, record.name, record) - except AttributeError: - pass - - def format(self, indent=0): - parent = u" "*indent+self.name - children = [r.format(indent=indent+2) for r in self.children] - return u"\n".join([parent]+children) - -PARSER = { -1: Variables, -2: History, -3: Wave, -4: Recreation, -5: Procedure, -7: GetHistory, -8: PackedFile, -9: _FolderStart, -10: _FolderEnd, -} - -def loads(s, ignore_unknown=True): - """Load an igor file from string""" - max = len(s) - pos = 0 - ret = [] - stack = [Folder(path=[u'root'])] - while pos < max: - if pos+8 > max: - raise IOError("invalid record header; bad pxp file?") - ignore = ord(s[pos])&0x80 - order = '<' if ord(s[pos])&0x77 else '>' - type, version, length = struct.unpack(order+'hhi',s[pos:pos+8]) - pos += 8 - if pos+length > len(s): - raise IOError("final record too long; bad pxp file?") - data = s[pos:pos+length] - pos += length - if not ignore: - parse = PARSER.get(type, None) - if parse: - record = parse(data, order) - elif ignore_unknown: - continue - else: - record = Unknown(data=data, order=order, type=type) - if isinstance(record, _FolderStart): - path = stack[-1].path+[record.name] - folder = Folder(path) - stack[-1].append(folder) - stack.append(folder) - elif isinstance(record, _FolderEnd): - stack.pop() - else: - stack[-1].append(record) - if len(stack) != 1: - raise IOError("FolderStart records do not match FolderEnd records") - return stack[0] - -def load(filename, ignore_unknown=True): - """Load an igor file""" - return loads(open(filename,'rb').read(), - ignore_unknown=ignore_unknown) - -# ============== Variable parsing ============== -def _parse_sys_numeric(n, order, data, pos): - values = numpy.fromstring(data[pos:pos+n*4], order+'f') - pos += n*4 - var = dict(('K'+str(i),v) for i,v in enumerate(values)) - return var, pos - -def _parse_user_numeric(n, order, data, pos): - var = {} - for i in range(n): - name = data[pos:data.find(chr(0),pos,pos+32)] - type,numtype,real,imag = struct.unpack(order+"hhdd",data[pos+32:pos+52]) - dtype = NUMTYPE[numtype] - if dtype in (numpy.complex64, numpy.complex128): - value = dtype(real+1j*imag) - else: - value = dtype(real) - var[name] = value - pos += 56 - return var, pos - -def _parse_dep_numeric(n, order, data, pos): - var = {} - for i in range(n): - name = data[pos:data.find(chr(0),pos,pos+32)] - type,numtype,real,imag = struct.unpack(order+"hhdd",data[pos+32:pos+52]) - dtype = NUMTYPE[numtype] - if dtype in (numpy.complex64, numpy.complex128): - value = dtype(real+1j*imag) - else: - value = dtype(real) - length, = struct.unpack(order+"h",data[pos+56:pos+58]) - var[name] = Formula(data[pos+58:pos+58+length-1], value) - pos += 58+length - return var, pos - -def _parse_dep_string(n, order, data, pos): - var = {} - for i in range(n): - name = data[pos:data.find(chr(0),pos,pos+32)] - length, = struct.unpack(order+"h",data[pos+48:pos+50]) - var[name] = Formula(data[pos+50:pos+50+length-1], "") - pos += 50+length - return var, pos - -def _parse_user_string1(n, order, data, pos): - var = {} - for i in range(n): - name = data[pos:data.find(chr(0),pos,pos+32)] - length, = struct.unpack(order+"h",data[pos+32:pos+34]) - value = data[pos+34:pos+34+length] - pos += 34+length - var[name] = value - return var, pos - -def _parse_user_string2(n, order, data, pos): - var = {} - for i in range(n): - name = data[pos:data.find(chr(0),pos,pos+32)] - length, = struct.unpack(order+"i",data[pos+32:pos+36]) - value = data[pos+36:pos+36+length] - pos += 36+length - var[name] = value - return var, pos diff --git a/igor/__init__.py b/igor/__init__.py new file mode 100644 index 0000000..213e9ab --- /dev/null +++ b/igor/__init__.py @@ -0,0 +1,30 @@ +# Copyright (C) 2012-2016 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +"Interface for reading binary IGOR files." + +__version__ = '0.3' + + +import logging as _logging + + +LOG = _logging.getLogger('igor') +LOG.setLevel(_logging.ERROR) +LOG.addHandler(_logging.StreamHandler()) +LOG.handlers[-1].setFormatter( + _logging.Formatter('%(name)s - %(levelname)s - %(message)s')) diff --git a/igor/binarywave.py b/igor/binarywave.py new file mode 100644 index 0000000..6d87d14 --- /dev/null +++ b/igor/binarywave.py @@ -0,0 +1,655 @@ +# Copyright (C) 2010-2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +"Read IGOR Binary Wave files into Numpy arrays." + +# Based on WaveMetric's Technical Note 003, "Igor Binary Format" +# ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/TN003.zip +# From ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/TN000.txt +# We place no restrictions on copying Technical Notes, with the +# exception that you cannot resell them. So read, enjoy, and +# share. We hope IGOR Technical Notes will provide you with lots of +# valuable information while you are developing IGOR applications. + +from __future__ import absolute_import +import array as _array +import struct as _struct +import sys as _sys +import types as _types + +import numpy as _numpy + +from . import LOG as _LOG +from .struct import Structure as _Structure +from .struct import DynamicStructure as _DynamicStructure +from .struct import Field as _Field +from .struct import DynamicField as _DynamicField +from .util import assert_null as _assert_null +from .util import byte_order as _byte_order +from .util import need_to_reorder_bytes as _need_to_reorder_bytes +from .util import checksum as _checksum + + +# Numpy doesn't support complex integers by default, see +# http://mail.python.org/pipermail/python-dev/2002-April/022408.html +# http://mail.scipy.org/pipermail/numpy-discussion/2007-October/029447.html +# So we roll our own types. See +# http://docs.scipy.org/doc/numpy/user/basics.rec.html +# http://docs.scipy.org/doc/numpy/reference/generated/numpy.dtype.html +complexInt8 = _numpy.dtype([('real', _numpy.int8), ('imag', _numpy.int8)]) +complexInt16 = _numpy.dtype([('real', _numpy.int16), ('imag', _numpy.int16)]) +complexInt32 = _numpy.dtype([('real', _numpy.int32), ('imag', _numpy.int32)]) +complexUInt8 = _numpy.dtype([('real', _numpy.uint8), ('imag', _numpy.uint8)]) +complexUInt16 = _numpy.dtype( + [('real', _numpy.uint16), ('imag', _numpy.uint16)]) +complexUInt32 = _numpy.dtype( + [('real', _numpy.uint32), ('imag', _numpy.uint32)]) + + +class StaticStringField (_DynamicField): + _null_terminated = False + _array_size_field = None + def __init__(self, *args, **kwargs): + if 'array' not in kwargs: + kwargs['array'] = True + super(StaticStringField, self).__init__(*args, **kwargs) + + def post_unpack(self, parents, data): + wave_structure = parents[-1] + wave_data = self._get_structure_data(parents, data, wave_structure) + d = self._normalize_string(wave_data[self.name]) + wave_data[self.name] = d + + def _normalize_string(self, d): + if isinstance(d, bytes): + pass + elif hasattr(d, 'tobytes'): + d = d.tobytes() + elif hasattr(d, 'tostring'): # Python 2 compatibility + d = d.tostring() + else: + d = b''.join(d) + if self._array_size_field: + start = 0 + strings = [] + for count in self.counts: + end = start + count + if end > start: + strings.append(d[start:end]) + if self._null_terminated: + strings[-1] = strings[-1].split(b'\x00', 1)[0] + start = end + elif self._null_terminated: + d = d.split(b'\x00', 1)[0] + return d + + +class NullStaticStringField (StaticStringField): + _null_terminated = True + + +# Begin IGOR constants and typedefs from IgorBin.h + +# From IgorMath.h +TYPE_TABLE = { # (key: integer flag, value: numpy dtype) + 0:None, # Text wave, not handled in ReadWave.c + 1:_numpy.complex, # NT_CMPLX, makes number complex. + 2:_numpy.float32, # NT_FP32, 32 bit fp numbers. + 3:_numpy.complex64, + 4:_numpy.float64, # NT_FP64, 64 bit fp numbers. + 5:_numpy.complex128, + 8:_numpy.int8, # NT_I8, 8 bit signed integer. Requires Igor Pro + # 2.0 or later. + 9:complexInt8, + 0x10:_numpy.int16,# NT_I16, 16 bit integer numbers. Requires Igor + # Pro 2.0 or later. + 0x11:complexInt16, + 0x20:_numpy.int32,# NT_I32, 32 bit integer numbers. Requires Igor + # Pro 2.0 or later. + 0x21:complexInt32, +# 0x40:None, # NT_UNSIGNED, Makes above signed integers +# # unsigned. Requires Igor Pro 3.0 or later. + 0x48:_numpy.uint8, + 0x49:complexUInt8, + 0x50:_numpy.uint16, + 0x51:complexUInt16, + 0x60:_numpy.uint32, + 0x61:complexUInt32, +} + +# From wave.h +MAXDIMS = 4 + +# From binary.h +BinHeader1 = _Structure( # `version` field pulled out into Wave + name='BinHeader1', + fields=[ + _Field('l', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'), + _Field('h', 'checksum', help='Checksum over this header and the wave header.'), + ]) + +BinHeader2 = _Structure( # `version` field pulled out into Wave + name='BinHeader2', + fields=[ + _Field('l', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'), + _Field('l', 'noteSize', help='The size of the note text.'), + _Field('l', 'pictSize', default=0, help='Reserved. Write zero. Ignore on read.'), + _Field('h', 'checksum', help='Checksum over this header and the wave header.'), + ]) + +BinHeader3 = _Structure( # `version` field pulled out into Wave + name='BinHeader3', + fields=[ + _Field('l', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'), + _Field('l', 'noteSize', help='The size of the note text.'), + _Field('l', 'formulaSize', help='The size of the dependency formula, if any.'), + _Field('l', 'pictSize', default=0, help='Reserved. Write zero. Ignore on read.'), + _Field('h', 'checksum', help='Checksum over this header and the wave header.'), + ]) + +BinHeader5 = _Structure( # `version` field pulled out into Wave + name='BinHeader5', + fields=[ + _Field('h', 'checksum', help='Checksum over this header and the wave header.'), + _Field('l', 'wfmSize', help='The size of the WaveHeader5 data structure plus the wave data.'), + _Field('l', 'formulaSize', help='The size of the dependency formula, if any.'), + _Field('l', 'noteSize', help='The size of the note text.'), + _Field('l', 'dataEUnitsSize', help='The size of optional extended data units.'), + _Field('l', 'dimEUnitsSize', help='The size of optional extended dimension units.', count=MAXDIMS, array=True), + _Field('l', 'dimLabelsSize', help='The size of optional dimension labels.', count=MAXDIMS, array=True), + _Field('l', 'sIndicesSize', help='The size of string indicies if this is a text wave.'), + _Field('l', 'optionsSize1', default=0, help='Reserved. Write zero. Ignore on read.'), + _Field('l', 'optionsSize2', default=0, help='Reserved. Write zero. Ignore on read.'), + ]) + + +# From wave.h +MAX_WAVE_NAME2 = 18 # Maximum length of wave name in version 1 and 2 + # files. Does not include the trailing null. +MAX_WAVE_NAME5 = 31 # Maximum length of wave name in version 5 + # files. Does not include the trailing null. +MAX_UNIT_CHARS = 3 + +# Header to an array of waveform data. + +# `wData` field pulled out into DynamicWaveDataField1 +WaveHeader2 = _DynamicStructure( + name='WaveHeader2', + fields=[ + _Field('h', 'type', help='See types (e.g. NT_FP64) above. Zero for text waves.'), + _Field('P', 'next', default=0, help='Used in memory only. Write zero. Ignore on read.'), + NullStaticStringField('c', 'bname', help='Name of wave plus trailing null.', count=MAX_WAVE_NAME2+2), + _Field('h', 'whVersion', default=0, help='Write 0. Ignore on read.'), + _Field('h', 'srcFldr', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('P', 'fileName', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('c', 'dataUnits', default=0, help='Natural data units go here - null if none.', count=MAX_UNIT_CHARS+1, array=True), + _Field('c', 'xUnits', default=0, help='Natural x-axis units go here - null if none.', count=MAX_UNIT_CHARS+1, array=True), + _Field('l', 'npnts', help='Number of data points in wave.'), + _Field('h', 'aModified', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('d', 'hsA', help='X value for point p = hsA*p + hsB'), + _Field('d', 'hsB', help='X value for point p = hsA*p + hsB'), + _Field('h', 'wModified', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('h', 'swModified', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('h', 'fsValid', help='True if full scale values have meaning.'), + _Field('d', 'topFullScale', help='The min full scale value for wave.'), # sic, 'min' should probably be 'max' + _Field('d', 'botFullScale', help='The min full scale value for wave.'), + _Field('c', 'useBits', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('c', 'kindBits', default=0, help='Reserved. Write zero. Ignore on read.'), + _Field('P', 'formula', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('l', 'depID', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('L', 'creationDate', help='DateTime of creation. Not used in version 1 files.'), + _Field('c', 'wUnused', default=0, help='Reserved. Write zero. Ignore on read.', count=2, array=True), + _Field('L', 'modDate', help='DateTime of last modification.'), + _Field('P', 'waveNoteH', help='Used in memory only. Write zero. Ignore on read.'), + ]) + +# `sIndices` pointer unset (use Wave5_data['sIndices'] instead). This +# field is filled in by DynamicStringIndicesDataField. +# `wData` field pulled out into DynamicWaveDataField5 +WaveHeader5 = _DynamicStructure( + name='WaveHeader5', + fields=[ + _Field('P', 'next', help='link to next wave in linked list.'), + _Field('L', 'creationDate', help='DateTime of creation.'), + _Field('L', 'modDate', help='DateTime of last modification.'), + _Field('l', 'npnts', help='Total number of points (multiply dimensions up to first zero).'), + _Field('h', 'type', help='See types (e.g. NT_FP64) above. Zero for text waves.'), + _Field('h', 'dLock', default=0, help='Reserved. Write zero. Ignore on read.'), + _Field('c', 'whpad1', default=0, help='Reserved. Write zero. Ignore on read.', count=6, array=True), + _Field('h', 'whVersion', default=1, help='Write 1. Ignore on read.'), + NullStaticStringField('c', 'bname', help='Name of wave plus trailing null.', count=MAX_WAVE_NAME5+1), + _Field('l', 'whpad2', default=0, help='Reserved. Write zero. Ignore on read.'), + _Field('P', 'dFolder', default=0, help='Used in memory only. Write zero. Ignore on read.'), + # Dimensioning info. [0] == rows, [1] == cols etc + _Field('l', 'nDim', help='Number of of items in a dimension -- 0 means no data.', count=MAXDIMS, array=True), + _Field('d', 'sfA', help='Index value for element e of dimension d = sfA[d]*e + sfB[d].', count=MAXDIMS, array=True), + _Field('d', 'sfB', help='Index value for element e of dimension d = sfA[d]*e + sfB[d].', count=MAXDIMS, array=True), + # SI units + _Field('c', 'dataUnits', default=0, help='Natural data units go here - null if none.', count=MAX_UNIT_CHARS+1, array=True), + _Field('c', 'dimUnits', default=0, help='Natural dimension units go here - null if none.', count=(MAXDIMS, MAX_UNIT_CHARS+1), array=True), + _Field('h', 'fsValid', help='TRUE if full scale values have meaning.'), + _Field('h', 'whpad3', default=0, help='Reserved. Write zero. Ignore on read.'), + _Field('d', 'topFullScale', help='The max and max full scale value for wave'), # sic, probably "max and min" + _Field('d', 'botFullScale', help='The max and max full scale value for wave.'), # sic, probably "max and min" + _Field('P', 'dataEUnits', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('P', 'dimEUnits', default=0, help='Used in memory only. Write zero. Ignore on read.', count=MAXDIMS, array=True), + _Field('P', 'dimLabels', default=0, help='Used in memory only. Write zero. Ignore on read.', count=MAXDIMS, array=True), + _Field('P', 'waveNoteH', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('l', 'whUnused', default=0, help='Reserved. Write zero. Ignore on read.', count=16, array=True), + # The following stuff is considered private to Igor. + _Field('h', 'aModified', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('h', 'wModified', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('h', 'swModified', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('c', 'useBits', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('c', 'kindBits', default=0, help='Reserved. Write zero. Ignore on read.'), + _Field('P', 'formula', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('l', 'depID', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('h', 'whpad4', default=0, help='Reserved. Write zero. Ignore on read.'), + _Field('h', 'srcFldr', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('P', 'fileName', default=0, help='Used in memory only. Write zero. Ignore on read.'), + _Field('P', 'sIndices', default=0, help='Used in memory only. Write zero. Ignore on read.'), + ]) + + +class DynamicWaveDataField1 (_DynamicField): + def pre_pack(self, parents, data): + raise NotImplementedError() + + def pre_unpack(self, parents, data): + full_structure = parents[0] + wave_structure = parents[-1] + wave_header_structure = wave_structure.fields[1].format + wave_data = self._get_structure_data(parents, data, wave_structure) + version = data['version'] + bin_header = wave_data['bin_header'] + wave_header = wave_data['wave_header'] + + self.count = wave_header['npnts'] + self.data_size = self._get_size(bin_header, wave_header_structure.size) + + type_ = TYPE_TABLE.get(wave_header['type'], None) + if type_: + self.shape = self._get_shape(bin_header, wave_header) + else: # text wave + type_ = _numpy.dtype('S1') + self.shape = (self.data_size,) + # dtype() wrapping to avoid numpy.generic and + # getset_descriptor issues with the builtin numpy types + # (e.g. int32). It has no effect on our local complex + # integers. + self.dtype = _numpy.dtype(type_).newbyteorder( + wave_structure.byte_order) + if (version == 3 and + self.count > 0 and + bin_header['formulaSize'] > 0 and + self.data_size == 0): + """From TN003: + + Igor Pro 2.00 included support for dependency formulae. If + a wave was governed by a dependency formula then the + actual wave data was not written to disk for that wave, + because on loading the wave Igor could recalculate the + data. However,this prevented the wave from being loaded + into an experiment other than the original + experiment. Consequently, in a version of Igor Pro 3.0x, + we changed it so that the wave data was written even if + the wave was governed by a dependency formula. When + reading a binary wave file, you can detect that the wave + file does not contain the wave data by examining the + wfmSize, formulaSize and npnts fields. If npnts is greater + than zero and formulaSize is greater than zero and + the waveDataSize as calculated above is zero, then this is + a file governed by a dependency formula that was written + without the actual wave data. + """ + self.shape = (0,) + elif TYPE_TABLE.get(wave_header['type'], None) is not None: + assert self.data_size == self.count * self.dtype.itemsize, ( + self.data_size, self.count, self.dtype.itemsize, self.dtype) + else: + assert self.data_size >= 0, ( + bin_header['wfmSize'], wave_header_structure.size) + + def _get_size(self, bin_header, wave_header_size): + return bin_header['wfmSize'] - wave_header_size - 16 + + def _get_shape(self, bin_header, wave_header): + return (self.count,) + + def unpack(self, stream): + data_b = stream.read(self.data_size) + try: + data = _numpy.ndarray( + shape=self.shape, + dtype=self.dtype, + buffer=data_b, + order='F', + ) + except: + _LOG.error( + 'could not reshape data from {} to {}'.format( + self.shape, data_b)) + raise + return data + + +class DynamicWaveDataField5 (DynamicWaveDataField1): + "Adds support for multidimensional data." + def _get_size(self, bin_header, wave_header_size): + return bin_header['wfmSize'] - wave_header_size + + def _get_shape(self, bin_header, wave_header): + return [n for n in wave_header['nDim'] if n > 0] or (0,) + + +# End IGOR constants and typedefs from IgorBin.h + + +class DynamicStringField (StaticStringField): + _size_field = None + + def pre_unpack(self, parents, data): + size = self._get_size_data(parents, data) + if self._array_size_field: + self.counts = size + self.count = sum(self.counts) + else: + self.count = size + self.setup() + + def _get_size_data(self, parents, data): + wave_structure = parents[-1] + wave_data = self._get_structure_data(parents, data, wave_structure) + bin_header = wave_data['bin_header'] + return bin_header[self._size_field] + + +class DynamicWaveNoteField (DynamicStringField): + _size_field = 'noteSize' + + +class DynamicDependencyFormulaField (DynamicStringField): + """Optional wave dependency formula + + Excerpted from TN003: + + A wave has a dependency formula if it has been bound by a + statement such as "wave0 := sin(x)". In this example, the + dependency formula is "sin(x)". The formula is stored with + no trailing null byte. + """ + _size_field = 'formulaSize' + # Except when it is stored with a trailing null byte :p. See, for + # example, test/data/mac-version3Dependent.ibw. + _null_terminated = True + + +class DynamicDataUnitsField (DynamicStringField): + """Optional extended data units data + + Excerpted from TN003: + + dataUnits - Present in versions 1, 2, 3, 5. The dataUnits field + stores the units for the data represented by the wave. It is a C + string terminated with a null character. This field supports + units of 0 to 3 bytes. In version 1, 2 and 3 files, longer units + can not be represented. In version 5 files, longer units can be + stored using the optional extended data units section of the + file. + """ + _size_field = 'dataEUnitsSize' + + +class DynamicDimensionUnitsField (DynamicStringField): + """Optional extended dimension units data + + Excerpted from TN003: + + xUnits - Present in versions 1, 2, 3. The xUnits field stores the + X units for a wave. It is a C string terminated with a null + character. This field supports units of 0 to 3 bytes. In + version 1, 2 and 3 files, longer units can not be represented. + + dimUnits - Present in version 5 only. This field is an array of 4 + strings, one for each possible wave dimension. Each string + supports units of 0 to 3 bytes. Longer units can be stored using + the optional extended dimension units section of the file. + """ + _size_field = 'dimEUnitsSize' + _array_size_field = True + + +class DynamicLabelsField (DynamicStringField): + """Optional dimension label data + + From TN003: + + If the wave has dimension labels for dimension d then the + dimLabelsSize[d] field of the BinHeader5 structure will be + non-zero. + + A wave will have dimension labels if a SetDimLabel command has + been executed on it. + + A 3 point 1D wave has 4 dimension labels. The first dimension + label is the label for the dimension as a whole. The next three + dimension labels are the labels for rows 0, 1, and 2. When Igor + writes dimension labels to disk, it writes each dimension label as + a C string (null-terminated) in a field of 32 bytes. + """ + _size_field = 'dimLabelsSize' + _array_size_field = True + + def post_unpack(self, parents, data): + wave_structure = parents[-1] + wave_data = self._get_structure_data(parents, data, wave_structure) + bin_header = wave_data['bin_header'] + d = wave_data[self.name] + dim_labels = [] + start = 0 + for size in bin_header[self._size_field]: + end = start + size + if end > start: + dim_data = d[start:end] + chunks = [] + for i in range(size//32): + chunks.append(dim_data[32*i:32*(i+1)]) + labels = [b''] + for chunk in chunks: + labels[-1] = labels[-1] + b''.join(chunk) + if b'\x00' in chunk: + labels.append(b'') + labels.pop(-1) + start = end + else: + labels = [] + dim_labels.append(labels) + wave_data[self.name] = dim_labels + + +class DynamicStringIndicesDataField (_DynamicField): + """String indices used for text waves only + """ + def pre_pack(self, parents, data): + raise NotImplementedError() + + def pre_unpack(self, parents, data): + wave_structure = parents[-1] + wave_data = self._get_structure_data(parents, data, wave_structure) + bin_header = wave_data['bin_header'] + wave_header = wave_data['wave_header'] + self.string_indices_size = bin_header['sIndicesSize'] + self.count = self.string_indices_size // 4 + if self.count: # make sure we're in a text wave + assert TYPE_TABLE[wave_header['type']] is None, wave_header + self.setup() + + def post_unpack(self, parents, data): + if not self.count: + return + wave_structure = parents[-1] + wave_data = self._get_structure_data(parents, data, wave_structure) + wave_header = wave_data['wave_header'] + wdata = wave_data['wData'] + strings = [] + start = 0 + for i,offset in enumerate(wave_data['sIndices']): + if offset > start: + chars = wdata[start:offset] + strings.append(b''.join(chars)) + start = offset + elif offset == start: + strings.append(b'') + else: + raise ValueError((offset, wave_data['sIndices'])) + wdata = _numpy.array(strings) + shape = [n for n in wave_header['nDim'] if n > 0] or (0,) + try: + wdata = wdata.reshape(shape) + except ValueError: + _LOG.error( + 'could not reshape strings from {} to {}'.format( + shape, wdata.shape)) + raise + wave_data['wData'] = wdata + + +class DynamicVersionField (_DynamicField): + def pre_pack(self, parents, byte_order): + raise NotImplementedError() + + def post_unpack(self, parents, data): + wave_structure = parents[-1] + wave_data = self._get_structure_data(parents, data, wave_structure) + version = wave_data['version'] + if wave_structure.byte_order in '@=': + need_to_reorder_bytes = _need_to_reorder_bytes(version) + wave_structure.byte_order = _byte_order(need_to_reorder_bytes) + _LOG.debug( + 'get byte order from version: {} (reorder? {})'.format( + wave_structure.byte_order, need_to_reorder_bytes)) + else: + need_to_reorder_bytes = False + + old_format = wave_structure.fields[-1].format + if version == 1: + wave_structure.fields[-1].format = Wave1 + elif version == 2: + wave_structure.fields[-1].format = Wave2 + elif version == 3: + wave_structure.fields[-1].format = Wave3 + elif version == 5: + wave_structure.fields[-1].format = Wave5 + elif not need_to_reorder_bytes: + raise ValueError( + 'invalid binary wave version: {}'.format(version)) + + if wave_structure.fields[-1].format != old_format: + _LOG.debug('change wave headers from {} to {}'.format( + old_format, wave_structure.fields[-1].format)) + wave_structure.setup() + elif need_to_reorder_bytes: + wave_structure.setup() + + # we might need to unpack again with the new byte order + return need_to_reorder_bytes + + +class DynamicWaveField (_DynamicField): + def post_unpack(self, parents, data): + return + raise NotImplementedError() # TODO + checksum_size = bin.size + wave.size + wave_structure = parents[-1] + if version == 5: + # Version 5 checksum does not include the wData field. + checksum_size -= 4 + c = _checksum(b, parents[-1].byte_order, 0, checksum_size) + if c != 0: + raise ValueError( + ('This does not appear to be a valid Igor binary wave file. ' + 'Error in checksum: should be 0, is {}.').format(c)) + +Wave1 = _DynamicStructure( + name='Wave1', + fields=[ + _Field(BinHeader1, 'bin_header', help='Binary wave header'), + _Field(WaveHeader2, 'wave_header', help='Wave header'), + DynamicWaveDataField1('f', 'wData', help='The start of the array of waveform data.', count=0, array=True), + ]) + +Wave2 = _DynamicStructure( + name='Wave2', + fields=[ + _Field(BinHeader2, 'bin_header', help='Binary wave header'), + _Field(WaveHeader2, 'wave_header', help='Wave header'), + DynamicWaveDataField1('f', 'wData', help='The start of the array of waveform data.', count=0, array=True), + _Field('x', 'padding', help='16 bytes of padding in versions 2 and 3.', count=16, array=True), + DynamicWaveNoteField('c', 'note', help='Optional wave note data', count=0, array=True), + ]) + +Wave3 = _DynamicStructure( + name='Wave3', + fields=[ + _Field(BinHeader3, 'bin_header', help='Binary wave header'), + _Field(WaveHeader2, 'wave_header', help='Wave header'), + DynamicWaveDataField1('f', 'wData', help='The start of the array of waveform data.', count=0, array=True), + _Field('x', 'padding', help='16 bytes of padding in versions 2 and 3.', count=16, array=True), + DynamicWaveNoteField('c', 'note', help='Optional wave note data', count=0, array=True), + DynamicDependencyFormulaField('c', 'formula', help='Optional wave dependency formula', count=0, array=True), + ]) + +Wave5 = _DynamicStructure( + name='Wave5', + fields=[ + _Field(BinHeader5, 'bin_header', help='Binary wave header'), + _Field(WaveHeader5, 'wave_header', help='Wave header'), + DynamicWaveDataField5('f', 'wData', help='The start of the array of waveform data.', count=0, array=True), + DynamicDependencyFormulaField('c', 'formula', help='Optional wave dependency formula.', count=0, array=True), + DynamicWaveNoteField('c', 'note', help='Optional wave note data.', count=0, array=True), + DynamicDataUnitsField('c', 'data_units', help='Optional extended data units data.', count=0, array=True), + DynamicDimensionUnitsField('c', 'dimension_units', help='Optional dimension label data', count=0, array=True), + DynamicLabelsField('c', 'labels', help="Optional dimension label data", count=0, array=True), + DynamicStringIndicesDataField('P', 'sIndices', help='Dynamic string indices for text waves.', count=0, array=True), + ]) + +Wave = _DynamicStructure( + name='Wave', + fields=[ + DynamicVersionField('h', 'version', help='Version number for backwards compatibility.'), + DynamicWaveField(Wave1, 'wave', help='The rest of the wave data.'), + ]) + + +def load(filename): + if hasattr(filename, 'read'): + f = filename # filename is actually a stream object + else: + f = open(filename, 'rb') + try: + Wave.byte_order = '=' + Wave.setup() + data = Wave.unpack_stream(f) + finally: + if not hasattr(filename, 'read'): + f.close() + + return data + + +def save(filename): + raise NotImplementedError diff --git a/igor/igorpy.py b/igor/igorpy.py new file mode 100644 index 0000000..f9e0961 --- /dev/null +++ b/igor/igorpy.py @@ -0,0 +1,275 @@ +# This program is in the public domain +"""`igor.py` compatibility layer on top of the `igor` package. + +igor.load('filename') or igor.loads('data') loads the content of an igore file +into memory as a folder structure. + +Returns the root folder. + +Folders have name, path and children. +Children can be indexed by folder[i] or by folder['name']. +To see the whole tree, use: print folder.format() + +The usual igor folder types are given in the technical reports +PTN003.ifn and TN003.ifn. +""" +from __future__ import absolute_import +import io as _io +import locale as _locale +import re as _re +import sys as _sys + +import numpy as _numpy + +from .binarywave import MAXDIMS as _MAXDIMS +from .packed import load as _load +from .record.base import UnknownRecord as _UnknownRecord +from .record.folder import FolderStartRecord as _FolderStartRecord +from .record.folder import FolderEndRecord as _FolderEndRecord +from .record.history import HistoryRecord as _HistoryRecord +from .record.history import GetHistoryRecord as _GetHistoryRecord +from .record.history import RecreationRecord as _RecreationRecord +from .record.packedfile import PackedFileRecord as _PackedFileRecord +from .record.procedure import ProcedureRecord as _ProcedureRecord +from .record.wave import WaveRecord as _WaveRecord +from .record.variables import VariablesRecord as _VariablesRecord + + +__version__='0.10' + + +ENCODING = _locale.getpreferredencoding() or _sys.getdefaultencoding() +PYKEYWORDS = set(('and','as','assert','break','class','continue', + 'def','elif','else','except','exec','finally', + 'for','global','if','import','in','is','lambda', + 'or','pass','print','raise','return','try','with', + 'yield')) +PYID = _re.compile(r"^[^\d\W]\w*$", _re.UNICODE) +def valid_identifier(s): + """Check if a name is a valid identifier""" + return PYID.match(s) and s not in PYKEYWORDS + + +class IgorObject(object): + """ Parent class for all objects the parser can return """ + pass + +class Variables(IgorObject): + """ + Contains system numeric variables (e.g., K0) and user numeric and string variables. + """ + def __init__(self, record): + self.sysvar = record.variables['variables']['sysVars'] + self.uservar = record.variables['variables']['userVars'] + self.userstr = record.variables['variables']['userStrs'] + self.depvar = record.variables['variables'].get('dependentVars', {}) + self.depstr = record.variables['variables'].get('dependentStrs', {}) + + def format(self, indent=0): + return " "*indent+""\ + %(len(self.sysvar), + len(self.uservar)+len(self.userstr), + len(self.depvar)+len(self.depstr)) + +class History(IgorObject): + """ + Contains the experiment's history as plain text. + """ + def __init__(self, data): + self.data = data + def format(self, indent=0): + return " "*indent+"" + +class Wave(IgorObject): + """ + Contains the data for a wave + """ + def __init__(self, record): + d = record.wave['wave'] + self.name = d['wave_header']['bname'].decode(ENCODING) + self.data = d['wData'] + self.fs = d['wave_header']['fsValid'] + self.fstop = d['wave_header']['topFullScale'] + self.fsbottom = d['wave_header']['botFullScale'] + if record.wave['version'] in [1,2,3]: + dims = [d['wave_header']['npnts']] + [0]*(_MAXDIMS-1) + sfA = [d['wave_header']['hsA']] + [0]*(_MAXDIMS-1) + sfB = [d['wave_header']['hsB']] + [0]*(_MAXDIMS-1) + self.data_units = [d['wave_header']['dataUnits']] + self.axis_units = [d['wave_header']['xUnits']] + else: + dims = d['wave_header']['nDim'] + sfA = d['wave_header']['sfA'] + sfB = d['wave_header']['sfB'] + # TODO find example with multiple data units + self.data_units = [d['data_units'].decode(ENCODING)] + self.axis_units = [d['dimension_units'].decode(ENCODING)] + self.data_units.extend(['']*(_MAXDIMS-len(self.data_units))) + self.data_units = tuple(self.data_units) + self.axis_units.extend(['']*(_MAXDIMS-len(self.axis_units))) + self.axis_units = tuple(self.axis_units) + self.axis = [_numpy.linspace(a,b,c) for a,b,c in zip(sfA, sfB, dims)] + self.formula = d.get('formula', '') + self.notes = d.get('note', '') + def format(self, indent=0): + if isinstance(self.data, list): + type,size = "text", "%d"%len(self.data) + else: + type,size = "data", "x".join(str(d) for d in self.data.shape) + return " "*indent+"%s %s (%s)"%(self.name, type, size) + + def __array__(self): + return self.data + + __repr__ = __str__ = lambda s: "" % s.format() + +class Recreation(IgorObject): + """ + Contains the experiment's recreation procedures as plain text. + """ + def __init__(self, data): + self.data = data + def format(self, indent=0): + return " "*indent + "" +class Procedure(IgorObject): + """ + Contains the experiment's main procedure window text as plain text. + """ + def __init__(self, data): + self.data = data + def format(self, indent=0): + return " "*indent + "" +class GetHistory(IgorObject): + """ + Not a real record but rather, a message to go back and read the history text. + + The reason for GetHistory is that IGOR runs Recreation when it loads the + datafile. This puts entries in the history that shouldn't be there. The + GetHistory entry simply says that the Recreation has run, and the History + can be restored from the previously saved value. + """ + def __init__(self, data): + self.data = data + def format(self, indent=0): + return " "*indent + "" +class PackedFile(IgorObject): + """ + Contains the data for a procedure file or notebook in packed form. + """ + def __init__(self, data): + self.data = data + def format(self, indent=0): + return " "*indent + "" +class Unknown(IgorObject): + """ + Record type not documented in PTN003/TN003. + """ + def __init__(self, data, type): + self.data = data + self.type = type + def format(self, indent=0): + return " "*indent + ""%self.type + + +class Folder(IgorObject): + """ + Hierarchical record container. + """ + def __init__(self, path): + self.name = path[-1] + self.path = path + self.children = [] + + def __getitem__(self, key): + if isinstance(key, int): + return self.children[key] + else: + for r in self.children: + if isinstance(r, (Folder,Wave)) and r.name == key: + return r + raise KeyError("Folder %s does not exist"%key) + + def __str__(self): + return "" % "/".join(self.path) + + __repr__ = __str__ + + def append(self, record): + """ + Add a record to the folder. + """ + self.children.append(record) + try: + # Record may not have a name, the name may be invalid, or it + # may already be in use. The noname case will be covered by + # record.name raising an attribute error. The others we need + # to test for explicitly. + if valid_identifier(record.name) and not hasattr(self, record.name): + setattr(self, record.name, record) + except AttributeError: + pass + + def format(self, indent=0): + parent = " "*indent+self.name + children = [r.format(indent=indent+2) for r in self.children] + return "\n".join([parent]+children) + + +def loads(s, **kwargs): + """Load an igor file from string""" + stream = _io.BytesIO(s) + return load(stream, **kwargs) + +def load(filename, **kwargs): + """Load an igor file""" + try: + packed_experiment = _load(filename) + except ValueError as e: + if e.args[0].startswith('not enough data for the next record header'): + raise IOError('invalid record header; bad pxp file?') + elif e.args[0].startswith('not enough data for the next record'): + raise IOError('final record too long; bad pxp file?') + raise + return _convert(packed_experiment, **kwargs) + +def _convert(packed_experiment, ignore_unknown=True): + records, filesystem = packed_experiment + stack = [Folder(path=['root'])] + for record in records: + if isinstance(record, _UnknownRecord): + if ignore_unknown: + continue + else: + r = Unknown(record.data, type=record.header['recordType']) + elif isinstance(record, _GetHistoryRecord): + r = GetHistory(record.text) + elif isinstance(record, _HistoryRecord): + r = History(record.text) + elif isinstance(record, _PackedFileRecord): + r = PackedFile(record.text) + elif isinstance(record, _ProcedureRecord): + r = Procedure(record.text) + elif isinstance(record, _RecreationRecord): + r = Recreation(record.text) + elif isinstance(record, _VariablesRecord): + r = Variables(record) + elif isinstance(record, _WaveRecord): + r = Wave(record) + else: + r = None + + if isinstance(record, _FolderStartRecord): + path = stack[-1].path + [ + record.null_terminated_text.decode(ENCODING)] + folder = Folder(path) + stack[-1].append(folder) + stack.append(folder) + elif isinstance(record, _FolderEndRecord): + stack.pop() + elif r is None: + raise NotImplementedError(record) + else: + stack[-1].append(r) + if len(stack) != 1: + raise IOError("FolderStart records do not match FolderEnd records") + return stack[0] diff --git a/igor/packed.py b/igor/packed.py new file mode 100644 index 0000000..8b5537a --- /dev/null +++ b/igor/packed.py @@ -0,0 +1,194 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +"Read IGOR Packed Experiment files files into records." + +from . import LOG as _LOG +from .struct import Structure as _Structure +from .struct import Field as _Field +from .util import byte_order as _byte_order +from .util import need_to_reorder_bytes as _need_to_reorder_bytes +from .util import _bytes +from .record import RECORD_TYPE as _RECORD_TYPE +from .record.base import UnknownRecord as _UnknownRecord +from .record.base import UnusedRecord as _UnusedRecord +from .record.folder import FolderStartRecord as _FolderStartRecord +from .record.folder import FolderEndRecord as _FolderEndRecord +from .record.variables import VariablesRecord as _VariablesRecord +from .record.wave import WaveRecord as _WaveRecord + + +# From PTN003: +# Igor writes other kinds of records in a packed experiment file, for +# storing things like pictures, page setup records, and miscellaneous +# settings. The format for these records is quite complex and is not +# described in PTN003. If you are writing a program to read packed +# files, you must skip any record with a record type that is not +# listed above. + +PackedFileRecordHeader = _Structure( + name='PackedFileRecordHeader', + fields=[ + _Field('H', 'recordType', help='Record type plus superceded flag.'), + _Field('h', 'version', help='Version information depends on the type of record.'), + _Field('l', 'numDataBytes', help='Number of data bytes in the record following this record header.'), + ]) + +#CR_STR = '\x15' (\r) + +PACKEDRECTYPE_MASK = 0x7FFF # Record type = (recordType & PACKEDREC_TYPE_MASK) +SUPERCEDED_MASK = 0x8000 # Bit is set if the record is superceded by + # a later record in the packed file. + + +def load(filename, strict=True, ignore_unknown=True): + _LOG.debug('loading a packed experiment file from {}'.format(filename)) + records = [] + if hasattr(filename, 'read'): + f = filename # filename is actually a stream object + else: + f = open(filename, 'rb') + byte_order = None + initial_byte_order = '=' + try: + while True: + PackedFileRecordHeader.byte_order = initial_byte_order + PackedFileRecordHeader.setup() + b = bytes(f.read(PackedFileRecordHeader.size)) + if not b: + break + if len(b) < PackedFileRecordHeader.size: + raise ValueError( + ('not enough data for the next record header ({} < {})' + ).format(len(b), PackedFileRecordHeader.size)) + _LOG.debug('reading a new packed experiment file record') + header = PackedFileRecordHeader.unpack_from(b) + if header['version'] and not byte_order: + need_to_reorder = _need_to_reorder_bytes(header['version']) + byte_order = initial_byte_order = _byte_order(need_to_reorder) + _LOG.debug( + 'get byte order from version: {} (reorder? {})'.format( + byte_order, need_to_reorder)) + if need_to_reorder: + PackedFileRecordHeader.byte_order = byte_order + PackedFileRecordHeader.setup() + header = PackedFileRecordHeader.unpack_from(b) + _LOG.debug( + 'reordered version: {}'.format(header['version'])) + data = bytes(f.read(header['numDataBytes'])) + if len(data) < header['numDataBytes']: + raise ValueError( + ('not enough data for the next record ({} < {})' + ).format(len(b), header['numDataBytes'])) + record_type = _RECORD_TYPE.get( + header['recordType'] & PACKEDRECTYPE_MASK, _UnknownRecord) + _LOG.debug('the new record has type {} ({}).'.format( + record_type, header['recordType'])) + if record_type in [_UnknownRecord, _UnusedRecord + ] and not ignore_unknown: + raise KeyError('unkown record type {}'.format( + header['recordType'])) + records.append(record_type(header, data, byte_order=byte_order)) + finally: + _LOG.debug('finished loading {} records from {}'.format( + len(records), filename)) + if not hasattr(filename, 'read'): + f.close() + + filesystem = _build_filesystem(records) + + return (records, filesystem) + +def _build_filesystem(records): + # From PTN003: + """The name must be a valid Igor data folder name. See Object + Names in the Igor Reference help file for name rules. + + When Igor Pro reads the data folder start record, it creates a new + data folder with the specified name. Any subsequent variable, wave + or data folder start records cause Igor to create data objects in + this new data folder, until Igor Pro reads a corresponding data + folder end record.""" + # From the Igor Manual, chapter 2, section 8, page II-123 + # http://www.wavemetrics.net/doc/igorman/II-08%20Data%20Folders.pdf + """Like the Macintosh file system, Igor Pro's data folders use the + colon character (:) to separate components of a path to an + object. This is analogous to Unix which uses / and Windows which + uses \. (Reminder: Igor's data folders exist wholly in memory + while an experiment is open. It is not a disk file system!) + + A data folder named "root" always exists and contains all other + data folders. + """ + # From the Igor Manual, chapter 4, page IV-2 + # http://www.wavemetrics.net/doc/igorman/IV-01%20Commands.pdf + """For waves and data folders only, you can also use "liberal" + names. Liberal names can include almost any character, including + spaces and dots (see Liberal Object Names on page III-415 for + details). + """ + # From the Igor Manual, chapter 3, section 16, page III-416 + # http://www.wavemetrics.net/doc/igorman/III-16%20Miscellany.pdf + """Liberal names have the same rules as standard names except you + may use any character except control characters and the following: + + " ' : ; + """ + filesystem = {'root': {}} + dir_stack = [('root', filesystem['root'])] + for record in records: + cwd = dir_stack[-1][-1] + if isinstance(record, _FolderStartRecord): + name = record.null_terminated_text + cwd[name] = {} + dir_stack.append((name, cwd[name])) + elif isinstance(record, _FolderEndRecord): + dir_stack.pop() + elif isinstance(record, (_VariablesRecord, _WaveRecord)): + if isinstance(record, _VariablesRecord): + sys_vars = record.variables['variables']['sysVars'].keys() + for filename,value in record.namespace.items(): + if len(dir_stack) > 1 and filename in sys_vars: + # From PTN003: + """When reading a packed file, any system + variables encountered while the current data + folder is not the root should be ignored. + """ + continue + _check_filename(dir_stack, filename) + cwd[filename] = value + else: # WaveRecord + filename = record.wave['wave']['wave_header']['bname'] + _check_filename(dir_stack, filename) + cwd[filename] = record + return filesystem + +def _check_filename(dir_stack, filename): + cwd = dir_stack[-1][-1] + if filename in cwd: + raise ValueError('collision on name {} in {}'.format( + filename, ':'.join(d for d,cwd in dir_stack))) + +def walk(filesystem, callback, dirpath=None): + """Walk a packed experiment filesystem, operating on each key,value pair. + """ + if dirpath is None: + dirpath = [] + for key,value in sorted((_bytes(k),v) for k,v in filesystem.items()): + callback(dirpath, key, value) + if isinstance(value, dict): + walk(filesystem=value, callback=callback, dirpath=dirpath+[key]) diff --git a/igor/record/__init__.py b/igor/record/__init__.py new file mode 100644 index 0000000..eafebfb --- /dev/null +++ b/igor/record/__init__.py @@ -0,0 +1,43 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +"Record parsers for IGOR's packed experiment files." + + +from .base import Record, UnknownRecord, UnusedRecord +from .variables import VariablesRecord +from .history import HistoryRecord, RecreationRecord, GetHistoryRecord +from .wave import WaveRecord +from .procedure import ProcedureRecord +from .packedfile import PackedFileRecord +from .folder import FolderStartRecord, FolderEndRecord + + +# From PackedFile.h +RECORD_TYPE = { + 0: UnusedRecord, + 1: VariablesRecord, + 2: HistoryRecord, + 3: WaveRecord, + 4: RecreationRecord, + 5: ProcedureRecord, + 6: UnusedRecord, + 7: GetHistoryRecord, + 8: PackedFileRecord, + 9: FolderStartRecord, + 10: FolderEndRecord, + } diff --git a/igor/record/base.py b/igor/record/base.py new file mode 100644 index 0000000..6b168cf --- /dev/null +++ b/igor/record/base.py @@ -0,0 +1,47 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + + +class Record (object): + def __init__(self, header, data, byte_order=None): + self.header = header + self.data = data + self.byte_order = byte_order + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return '<{} {}>'.format(self.__class__.__name__, id(self)) + + +class UnknownRecord (Record): + def __repr__(self): + return '<{}-{} {}>'.format( + self.__class__.__name__, self.header['recordType'], id(self)) + + +class UnusedRecord (Record): + pass + + +class TextRecord (Record): + def __init__(self, *args, **kwargs): + super(TextRecord, self).__init__(*args, **kwargs) + self.text = bytes(self.data).replace( + b'\r\n', b'\n').replace(b'\r', b'\n') + self.null_terminated_text = self.text.split(b'\x00', 1)[0] diff --git a/igor/record/folder.py b/igor/record/folder.py new file mode 100644 index 0000000..caaeb54 --- /dev/null +++ b/igor/record/folder.py @@ -0,0 +1,26 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +from .base import TextRecord + + +class FolderStartRecord (TextRecord): + pass + + +class FolderEndRecord (TextRecord): + pass diff --git a/igor/record/history.py b/igor/record/history.py new file mode 100644 index 0000000..e5d2199 --- /dev/null +++ b/igor/record/history.py @@ -0,0 +1,30 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +from .base import TextRecord + + +class HistoryRecord (TextRecord): + pass + + +class RecreationRecord (TextRecord): + pass + + +class GetHistoryRecord (TextRecord): + pass diff --git a/igor/record/packedfile.py b/igor/record/packedfile.py new file mode 100644 index 0000000..b457f20 --- /dev/null +++ b/igor/record/packedfile.py @@ -0,0 +1,22 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +from .base import Record + + +class PackedFileRecord (Record): + pass diff --git a/igor/record/procedure.py b/igor/record/procedure.py new file mode 100644 index 0000000..de00e6e --- /dev/null +++ b/igor/record/procedure.py @@ -0,0 +1,22 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +from .base import TextRecord + + +class ProcedureRecord (TextRecord): + pass diff --git a/igor/record/variables.py b/igor/record/variables.py new file mode 100644 index 0000000..a8eaccf --- /dev/null +++ b/igor/record/variables.py @@ -0,0 +1,319 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +import io as _io + +from .. import LOG as _LOG +from ..binarywave import TYPE_TABLE as _TYPE_TABLE +from ..binarywave import NullStaticStringField as _NullStaticStringField +from ..binarywave import DynamicStringField as _DynamicStringField +from ..struct import Structure as _Structure +from ..struct import DynamicStructure as _DynamicStructure +from ..struct import Field as _Field +from ..struct import DynamicField as _DynamicField +from ..util import byte_order as _byte_order +from ..util import need_to_reorder_bytes as _need_to_reorder_bytes +from .base import Record + + +class ListedStaticStringField (_NullStaticStringField): + """Handle string conversions for multi-count dynamic parents. + + If a field belongs to a multi-count dynamic parent, the parent is + called multiple times to parse each count, and the field's + post-unpack hook gets called after the field is unpacked during + each iteration. This requires alternative logic for getting and + setting the string data. The actual string formatting code is not + affected. + """ + def post_unpack(self, parents, data): + parent_structure = parents[-1] + parent_data = self._get_structure_data(parents, data, parent_structure) + d = self._normalize_string(parent_data[-1][self.name]) + parent_data[-1][self.name] = d + + +class ListedStaticStringField (_NullStaticStringField): + """Handle string conversions for multi-count dynamic parents. + + If a field belongs to a multi-count dynamic parent, the parent is + called multiple times to parse each count, and the field's + post-unpack hook gets called after the field is unpacked during + each iteration. This requires alternative logic for getting and + setting the string data. The actual string formatting code is not + affected. + """ + def post_unpack(self, parents, data): + parent_structure = parents[-1] + parent_data = self._get_structure_data(parents, data, parent_structure) + d = self._normalize_string(parent_data[-1][self.name]) + parent_data[-1][self.name] = d + + +class ListedDynamicStrDataField (_DynamicStringField, ListedStaticStringField): + _size_field = 'strLen' + _null_terminated = False + + def _get_size_data(self, parents, data): + parent_structure = parents[-1] + parent_data = self._get_structure_data(parents, data, parent_structure) + return parent_data[-1][self._size_field] + + +class DynamicVarDataField (_DynamicField): + def __init__(self, *args, **kwargs): + if 'array' not in kwargs: + kwargs['array'] = True + super(DynamicVarDataField, self).__init__(*args, **kwargs) + + def pre_pack(self, parents, data): + raise NotImplementedError() + + def post_unpack(self, parents, data): + var_structure = parents[-1] + var_data = self._get_structure_data(parents, data, var_structure) + data = var_data[self.name] + d = {} + for i,value in enumerate(data): + key,value = self._normalize_item(i, value) + d[key] = value + var_data[self.name] = d + + def _normalize_item(self, index, value): + raise NotImplementedError() + + +class DynamicSysVarField (DynamicVarDataField): + def _normalize_item(self, index, value): + name = 'K{}'.format(index) + return (name, value) + + +class DynamicUserVarField (DynamicVarDataField): + def _normalize_item(self, index, value): + name = value['name'] + value = value['num'] + return (name, value) + + +class DynamicUserStrField (DynamicVarDataField): + def _normalize_item(self, index, value): + name = value['name'] + value = value['data'] + return (name, value) + + +class DynamicVarNumField (_DynamicField): + def post_unpack(self, parents, data): + parent_structure = parents[-1] + parent_data = self._get_structure_data(parents, data, parent_structure) + d = self._normalize_numeric_variable(parent_data[-1][self.name]) + parent_data[-1][self.name] = d + + def _normalize_numeric_variable(self, num_var): + t = _TYPE_TABLE[num_var['numType']] + if num_var['numType'] % 2: # complex number + return t(complex(num_var['realPart'], num_var['imagPart'])) + else: + return t(num_var['realPart']) + + +class DynamicFormulaField (_DynamicStringField): + _size_field = 'formulaLen' + _null_terminated = True + + +# From Variables.h +VarHeader1 = _Structure( # `version` field pulled out into VariablesRecord + name='VarHeader1', + fields=[ + _Field('h', 'numSysVars', help='Number of system variables (K0, K1, ...).'), + _Field('h', 'numUserVars', help='Number of user numeric variables -- may be zero.'), + _Field('h', 'numUserStrs', help='Number of user string variables -- may be zero.'), + ]) + +# From Variables.h +VarHeader2 = _Structure( # `version` field pulled out into VariablesRecord + name='VarHeader2', + fields=[ + _Field('h', 'numSysVars', help='Number of system variables (K0, K1, ...).'), + _Field('h', 'numUserVars', help='Number of user numeric variables -- may be zero.'), + _Field('h', 'numUserStrs', help='Number of user string variables -- may be zero.'), + _Field('h', 'numDependentVars', help='Number of dependent numeric variables -- may be zero.'), + _Field('h', 'numDependentStrs', help='Number of dependent string variables -- may be zero.'), + ]) + +# From Variables.h +UserStrVarRec1 = _DynamicStructure( + name='UserStrVarRec1', + fields=[ + ListedStaticStringField('c', 'name', help='Name of the string variable.', count=32), + _Field('h', 'strLen', help='The real size of the following array.'), + ListedDynamicStrDataField('c', 'data'), + ]) + +# From Variables.h +UserStrVarRec2 = _DynamicStructure( + name='UserStrVarRec2', + fields=[ + ListedStaticStringField('c', 'name', help='Name of the string variable.', count=32), + _Field('l', 'strLen', help='The real size of the following array.'), + _Field('c', 'data'), + ]) + +# From Variables.h +VarNumRec = _Structure( + name='VarNumRec', + fields=[ + _Field('h', 'numType', help='Type from binarywave.TYPE_TABLE'), + _Field('d', 'realPart', help='The real part of the number.'), + _Field('d', 'imagPart', help='The imag part if the number is complex.'), + _Field('l', 'reserved', help='Reserved - set to zero.'), + ]) + +# From Variables.h +UserNumVarRec = _DynamicStructure( + name='UserNumVarRec', + fields=[ + ListedStaticStringField('c', 'name', help='Name of the string variable.', count=32), + _Field('h', 'type', help='0 = string, 1 = numeric.'), + DynamicVarNumField(VarNumRec, 'num', help='Type and value of the variable if it is numeric. Not used for string.'), + ]) + +# From Variables.h +UserDependentVarRec = _DynamicStructure( + name='UserDependentVarRec', + fields=[ + ListedStaticStringField('c', 'name', help='Name of the string variable.', count=32), + _Field('h', 'type', help='0 = string, 1 = numeric.'), + _Field(VarNumRec, 'num', help='Type and value of the variable if it is numeric. Not used for string.'), + _Field('h', 'formulaLen', help='The length of the dependency formula.'), + DynamicFormulaField('c', 'formula', help='Start of the dependency formula. A C string including null terminator.'), + ]) + + +class DynamicVarHeaderField (_DynamicField): + def pre_pack(self, parents, data): + raise NotImplementedError() + + def post_unpack(self, parents, data): + var_structure = parents[-1] + var_data = self._get_structure_data( + parents, data, var_structure) + var_header_structure = self.format + data = var_data['var_header'] + sys_vars_field = var_structure.get_field('sysVars') + sys_vars_field.count = data['numSysVars'] + sys_vars_field.setup() + user_vars_field = var_structure.get_field('userVars') + user_vars_field.count = data['numUserVars'] + user_vars_field.setup() + user_strs_field = var_structure.get_field('userStrs') + user_strs_field.count = data['numUserStrs'] + user_strs_field.setup() + if 'numDependentVars' in data: + dependent_vars_field = var_structure.get_field('dependentVars') + dependent_vars_field.count = data['numDependentVars'] + dependent_vars_field.setup() + dependent_strs_field = var_structure.get_field('dependentStrs') + dependent_strs_field.count = data['numDependentStrs'] + dependent_strs_field.setup() + var_structure.setup() + + +Variables1 = _DynamicStructure( + name='Variables1', + fields=[ + DynamicVarHeaderField(VarHeader1, 'var_header', help='Variables header'), + DynamicSysVarField('f', 'sysVars', help='System variables', count=0), + DynamicUserVarField(UserNumVarRec, 'userVars', help='User numeric variables', count=0), + DynamicUserStrField(UserStrVarRec1, 'userStrs', help='User string variables', count=0), + ]) + + +Variables2 = _DynamicStructure( + name='Variables2', + fields=[ + DynamicVarHeaderField(VarHeader2, 'var_header', help='Variables header'), + DynamicSysVarField('f', 'sysVars', help='System variables', count=0), + DynamicUserVarField(UserNumVarRec, 'userVars', help='User numeric variables', count=0), + DynamicUserStrField(UserStrVarRec2, 'userStrs', help='User string variables', count=0), + _Field(UserDependentVarRec, 'dependentVars', help='Dependent numeric variables.', count=0, array=True), + _Field(UserDependentVarRec, 'dependentStrs', help='Dependent string variables.', count=0, array=True), + ]) + + +class DynamicVersionField (_DynamicField): + def pre_pack(self, parents, byte_order): + raise NotImplementedError() + + def post_unpack(self, parents, data): + variables_structure = parents[-1] + variables_data = self._get_structure_data( + parents, data, variables_structure) + version = variables_data['version'] + if variables_structure.byte_order in '@=': + need_to_reorder_bytes = _need_to_reorder_bytes(version) + variables_structure.byte_order = _byte_order(need_to_reorder_bytes) + _LOG.debug( + 'get byte order from version: {} (reorder? {})'.format( + variables_structure.byte_order, need_to_reorder_bytes)) + else: + need_to_reorder_bytes = False + + old_format = variables_structure.fields[-1].format + if version == 1: + variables_structure.fields[-1].format = Variables1 + elif version == 2: + variables_structure.fields[-1].format = Variables2 + elif not need_to_reorder_bytes: + raise ValueError( + 'invalid variables record version: {}'.format(version)) + + if variables_structure.fields[-1].format != old_format: + _LOG.debug('change variables record from {} to {}'.format( + old_format, variables_structure.fields[-1].format)) + variables_structure.setup() + elif need_to_reorder_bytes: + variables_structure.setup() + + # we might need to unpack again with the new byte order + return need_to_reorder_bytes + + +VariablesRecordStructure = _DynamicStructure( + name='VariablesRecord', + fields=[ + DynamicVersionField('h', 'version', help='Version number for this header.'), + _Field(Variables1, 'variables', help='The rest of the variables data.'), + ]) + + +class VariablesRecord (Record): + def __init__(self, *args, **kwargs): + super(VariablesRecord, self).__init__(*args, **kwargs) + # self.header['version'] # record version always 0? + VariablesRecordStructure.byte_order = '=' + VariablesRecordStructure.setup() + stream = _io.BytesIO(bytes(self.data)) + self.variables = VariablesRecordStructure.unpack_stream(stream) + self.namespace = {} + for key,value in self.variables['variables'].items(): + if key not in ['var_header']: + _LOG.debug('update namespace {} with {} for {}'.format( + self.namespace, value, key)) + self.namespace.update(value) diff --git a/igor/record/wave.py b/igor/record/wave.py new file mode 100644 index 0000000..49ed20e --- /dev/null +++ b/igor/record/wave.py @@ -0,0 +1,30 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +from io import BytesIO as _BytesIO + +from ..binarywave import load as _loadibw +from . import Record + + +class WaveRecord (Record): + def __init__(self, *args, **kwargs): + super(WaveRecord, self).__init__(*args, **kwargs) + self.wave = _loadibw(_BytesIO(bytes(self.data))) + + def __str__(self): + return str(self.wave) diff --git a/igor/script.py b/igor/script.py new file mode 100644 index 0000000..83fde93 --- /dev/null +++ b/igor/script.py @@ -0,0 +1,91 @@ +# Copyright (C) 2012-2016 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +"Common code for scripts distributed with the `igor` package." + +from __future__ import absolute_import +import argparse as _argparse +import logging as _logging +import sys as _sys + +try: + import matplotlib as _matplotlib + import matplotlib.pyplot as _matplotlib_pyplot +except ImportError as _matplotlib_import_error: + _matplotlib = None + +from . import __version__ +from . import LOG as _LOG + + +class Script (object): + log_levels = [_logging.ERROR, _logging.WARNING, _logging.INFO, _logging.DEBUG] + + def __init__(self, description=None, filetype='IGOR Binary Wave (.ibw) file'): + self.parser = _argparse.ArgumentParser(description=description) + self.parser.add_argument( + '--version', action='version', + version='%(prog)s {}'.format(__version__)) + self.parser.add_argument( + '-f', '--infile', metavar='FILE', default='-', + help='input {}'.format(filetype)) + self.parser.add_argument( + '-o', '--outfile', metavar='FILE', default='-', + help='file for ASCII output') + self.parser.add_argument( + '-p', '--plot', action='store_const', const=True, + help='use Matplotlib to plot any IGOR waves') + self.parser.add_argument( + '-V', '--verbose', action='count', default=0, + help='increment verbosity') + self._num_plots = 0 + + def run(self, *args, **kwargs): + args = self.parser.parse_args(*args, **kwargs) + if args.infile == '-': + args.infile = _sys.stdin + if args.outfile == '-': + args.outfile = _sys.stdout + if args.verbose > 1: + log_level = self.log_levels[min(args.verbose-1, len(self.log_levels)-1)] + _LOG.setLevel(log_level) + self._run(args) + self.display_plots() + + def _run(self, args): + raise NotImplementedError() + + def plot_wave(self, args, wave, title=None): + if not args.plot: + return # no-op + if not _matplotlib: + raise _matplotlib_import_error + if title is None: + title = wave['wave']['wave_header']['bname'] + figure = _matplotlib_pyplot.figure() + axes = figure.add_subplot(1, 1, 1) + axes.set_title(title) + try: + axes.plot(wave['wave']['wData'], 'r.') + except ValueError as error: + _LOG.error('error plotting {}: {}'.format(title, error)) + pass + self._num_plots += 1 + + def display_plots(self): + if self._num_plots: + _matplotlib_pyplot.show() diff --git a/igor/struct.py b/igor/struct.py new file mode 100644 index 0000000..a50ede5 --- /dev/null +++ b/igor/struct.py @@ -0,0 +1,836 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +"""Structure and Field classes for declaring structures + +There are a few formats that can be used to represent the same data, a +binary packed format with all the data in a buffer, a linearized +format with each field in a single Python list, and a nested format +with each field in a hierarchy of Python dictionaries. +""" + +from __future__ import absolute_import +import io as _io +import logging as _logging +import pprint as _pprint +import struct as _struct + +import numpy as _numpy + +from . import LOG as _LOG + + +class Field (object): + """Represent a Structure field. + + The format argument can be a format character from the ``struct`` + documentation (e.g., ``c`` for ``char``, ``h`` for ``short``, ...) + or ``Structure`` instance (for building nested structures). + + Examples + -------- + + >>> from pprint import pprint + >>> import numpy + + Example of an unsigned short integer field: + + >>> time = Field( + ... 'I', 'time', default=0, help='POSIX time') + >>> time.arg_count + 1 + >>> list(time.pack_data(1)) + [1] + >>> list(time.pack_item(2)) + [2] + >>> time.unpack_data([3]) + 3 + >>> time.unpack_item([4]) + 4 + + Example of a multi-dimensional float field: + + >>> data = Field( + ... 'f', 'data', help='example data', count=(2,3,4), array=True) + >>> data.arg_count + 24 + >>> list(data.indexes()) # doctest: +ELLIPSIS + [[0, 0, 0], [0, 0, 1], [0, 0, 2], [0, 0, 3], [0, 1, 0], ..., [1, 2, 3]] + >>> list(data.pack_data( + ... [[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], + ... [[12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]]]) + ... ) # doctest: +ELLIPSIS + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ..., 19, 20, 21, 22, 23] + >>> list(data.pack_item(3)) + [3] + >>> data.unpack_data(range(data.arg_count)) + array([[[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]], + + [[12, 13, 14, 15], + [16, 17, 18, 19], + [20, 21, 22, 23]]]) + >>> data.unpack_item([3]) + 3 + + Example of a nested structure field: + + >>> run = Structure('run', fields=[time, data]) + >>> runs = Field(run, 'runs', help='pair of runs', count=2, array=True) + >>> runs.arg_count # = 2 * (1 + 24) + 50 + >>> data1 = numpy.arange(data.arg_count).reshape(data.count) + >>> data2 = data1 + data.arg_count + >>> list(runs.pack_data( + ... [{'time': 100, 'data': data1}, + ... {'time': 101, 'data': data2}]) + ... ) # doctest: +ELLIPSIS + [100, 0, 1, 2, ..., 22, 23, 101, 24, 25, ..., 46, 47] + >>> list(runs.pack_item({'time': 100, 'data': data1}) + ... ) # doctest: +ELLIPSIS + [100, 0, 1, 2, ..., 22, 23] + >>> pprint(runs.unpack_data(range(runs.arg_count))) + [{'data': array([[[ 1, 2, 3, 4], + [ 5, 6, 7, 8], + [ 9, 10, 11, 12]], + + [[13, 14, 15, 16], + [17, 18, 19, 20], + [21, 22, 23, 24]]]), + 'time': 0}, + {'data': array([[[26, 27, 28, 29], + [30, 31, 32, 33], + [34, 35, 36, 37]], + + [[38, 39, 40, 41], + [42, 43, 44, 45], + [46, 47, 48, 49]]]), + 'time': 25}] + >>> pprint(runs.unpack_item(range(runs.structure_count))) + {'data': array([[[ 1, 2, 3, 4], + [ 5, 6, 7, 8], + [ 9, 10, 11, 12]], + + [[13, 14, 15, 16], + [17, 18, 19, 20], + [21, 22, 23, 24]]]), + 'time': 0} + + If you don't give enough values for an array field, the remaining + values are filled in with their defaults. + + >>> list(data.pack_data( + ... [[[0, 1, 2, 3], [4, 5, 6]], [[10]]])) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: no default for + >>> data.default = 0 + >>> list(data.pack_data( + ... [[[0, 1, 2, 3], [4, 5, 6]], [[10]]])) + [0, 1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + See Also + -------- + Structure + """ + def __init__(self, format, name, default=None, help=None, count=1, + array=False): + self.format = format + self.name = name + self.default = default + self.help = help + self.count = count + self.array = array + self.setup() + + def setup(self): + """Setup any dynamic properties of a field. + + Use this method to recalculate dynamic properities after + changing the basic properties set during initialization. + """ + _LOG.debug('setup {}'.format(self)) + self.item_count = _numpy.prod(self.count) # number of item repeats + if not self.array and self.item_count != 1: + raise ValueError( + '{} must be an array field to have a count of {}'.format( + self, self.count)) + if isinstance(self.format, Structure): + self.structure_count = sum( + f.arg_count for f in self.format.fields) + self.arg_count = self.item_count * self.structure_count + elif self.format == 'x': + self.arg_count = 0 # no data in padding bytes + else: + self.arg_count = self.item_count # struct.Struct format args + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return '<{} {} {}>'.format( + self.__class__.__name__, self.name, id(self)) + + def indexes(self): + """Iterate through indexes to a possibly multi-dimensional array""" + assert self.array, self + try: + i = [0] * len(self.count) + except TypeError: # non-iterable count + for i in range(self.count): + yield i + else: + for i in range(self.item_count): + index = [] + for j,c in enumerate(reversed(self.count)): + index.insert(0, i % c) + i //= c + yield index + + def pack_data(self, data=None): + """Linearize a single field's data to a flat list. + + If the field is repeated (count > 1), the incoming data should + be iterable with each iteration returning a single item. + """ + if self.array: + if data is None: + data = [] + if hasattr(data, 'flat'): # take advantage of numpy's ndarray.flat + items = 0 + for item in data.flat: + items += 1 + for arg in self.pack_item(item): + yield arg + if items < self.item_count: + if f.default is None: + raise ValueError( + 'no default for {}.{}'.format(self, f)) + for i in range(self.item_count - items): + yield f.default + else: + for index in self.indexes(): + try: + if isinstance(index, int): + item = data[index] + else: + item = data + for i in index: + item = item[i] + except IndexError: + item = None + for arg in self.pack_item(item): + yield arg + else: + for arg in self.pack_item(data): + yield arg + + def pack_item(self, item=None): + """Linearize a single count of the field's data to a flat iterable + """ + if isinstance(self.format, Structure): + for i in self.format._pack_item(item): + yield i + elif item is None: + if self.default is None: + raise ValueError('no default for {}'.format(self)) + yield self.default + else: + yield item + + def unpack_data(self, data): + """Inverse of .pack_data""" + _LOG.debug('unpack {} for {} {}'.format(data, self, self.format)) + iterator = iter(data) + try: + items = [next(iterator) for i in range(self.arg_count)] + except StopIteration: + raise ValueError('not enough data to unpack {}'.format(self)) + try: + next(iterator) + except StopIteration: + pass + else: + raise ValueError('too much data to unpack {}'.format(self)) + if isinstance(self.format, Structure): + # break into per-structure clumps + s = self.structure_count + items = zip(*[items[i::s] for i in range(s)]) + else: + items = [[i] for i in items] + unpacked = [self.unpack_item(i) for i in items] + if self.arg_count: + count = self.count + else: + count = 0 # padding bytes, etc. + if not self.array: + assert count == 1, (self, self.count) + return unpacked[0] + if isinstance(self.format, Structure): + try: + len(self.count) + except TypeError: + pass + else: + raise NotImplementedError('reshape Structure field') + else: + unpacked = _numpy.array(unpacked) + _LOG.debug('reshape {} data from {} to {}'.format( + self, unpacked.shape, count)) + unpacked = unpacked.reshape(count) + return unpacked + + def unpack_item(self, item): + """Inverse of .unpack_item""" + if isinstance(self.format, Structure): + return self.format._unpack_item(item) + else: + assert len(item) == 1, item + return item[0] + + +class DynamicField (Field): + """Represent a DynamicStructure field with a dynamic definition. + + Adds the methods ``.pre_pack``, ``pre_unpack``, and + ``post_unpack``, all of which are called when a ``DynamicField`` + is used by a ``DynamicStructure``. Each method takes the + arguments ``(parents, data)``, where ``parents`` is a list of + ``DynamicStructure``\s that own the field and ``data`` is a dict + hierarchy of the structure data. + + See the ``DynamicStructure`` docstring for the exact timing of the + method calls. + + See Also + -------- + Field, DynamicStructure + """ + def pre_pack(self, parents, data): + "Prepare to pack." + pass + + def pre_unpack(self, parents, data): + "React to previously unpacked data" + pass + + def post_unpack(self, parents, data): + "React to our own data" + pass + + def _get_structure_data(self, parents, data, structure): + """Extract the data belonging to a particular ancestor structure. + """ + d = data + s = parents[0] + if s == structure: + return d + for p in parents[1:]: + for f in s.fields: + if f.format == p: + s = p + d = d[f.name] + break + assert s == p, (s, p) + if p == structure: + break + return d + + +class Structure (_struct.Struct): + r"""Represent a C structure. + + A convenient wrapper around struct.Struct that uses Fields and + adds dict-handling methods for transparent name assignment. + + See Also + -------- + Field + + Examples + -------- + + >>> import array + >>> from pprint import pprint + + Represent the C structures:: + + struct run { + unsigned int time; + short data[2][3]; + }; + + struct experiment { + unsigned short version; + struct run runs[2]; + }; + + As: + + >>> time = Field('I', 'time', default=0, help='POSIX time') + >>> data = Field( + ... 'h', 'data', default=0, help='example data', count=(2,3), + ... array=True) + >>> run = Structure('run', fields=[time, data]) + >>> version = Field( + ... 'H', 'version', default=1, help='example version') + >>> runs = Field(run, 'runs', help='pair of runs', count=2, array=True) + >>> experiment = Structure('experiment', fields=[version, runs]) + + The structures automatically calculate the flattened data format: + + >>> run.format + '@Ihhhhhh' + >>> run.size # 4 + 2*3*2 + 16 + >>> experiment.format + '@HIhhhhhhIhhhhhh' + >>> experiment.size # 2 + 2 + 2*(4 + 2*3*2) + 36 + + The first two elements in the above size calculation are 2 (for + the unsigned short, 'H') and 2 (padding so the unsigned int aligns + with a 4-byte block). If you select a byte ordering that doesn't + mess with alignment and recalculate the format, the padding goes + away and you get: + + >>> experiment.set_byte_order('>') + >>> experiment.get_format() + '>HIhhhhhhIhhhhhh' + >>> experiment.size + 34 + + You can read data out of any object supporting the buffer + interface: + + >>> b = array.array('B', range(experiment.size)) + >>> d = experiment.unpack_from(buffer=b) + >>> pprint(d) + {'runs': [{'data': array([[1543, 2057, 2571], + [3085, 3599, 4113]]), + 'time': 33752069}, + {'data': array([[5655, 6169, 6683], + [7197, 7711, 8225]]), + 'time': 303240213}], + 'version': 1} + >>> [hex(x) for x in d['runs'][0]['data'].flat] + ['0x607L', '0x809L', '0xa0bL', '0xc0dL', '0xe0fL', '0x1011L'] + + You can also read out from strings: + + >>> d = experiment.unpack(b.tostring()) + >>> pprint(d) + {'runs': [{'data': array([[1543, 2057, 2571], + [3085, 3599, 4113]]), + 'time': 33752069}, + {'data': array([[5655, 6169, 6683], + [7197, 7711, 8225]]), + 'time': 303240213}], + 'version': 1} + + If you don't give enough values for an array field, the remaining + values are filled in with their defaults. + + >>> experiment.pack_into(buffer=b, data=d) + >>> b.tostring()[:17] + '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10' + >>> b.tostring()[17:] + '\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !' + >>> run0 = d['runs'].pop(0) + >>> b = experiment.pack(data=d) + >>> b[:17] + '\x00\x01\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ' + >>> b[17:] + '!\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + If you set ``count=0``, the field is ignored. + + >>> experiment2 = Structure('experiment', fields=[ + ... version, Field('f', 'ignored', count=0, array=True), runs], + ... byte_order='>') + >>> experiment2.format + '>HIhhhhhhIhhhhhh' + >>> d = experiment2.unpack(b) + >>> pprint(d) + {'ignored': array([], dtype=float64), + 'runs': [{'data': array([[5655, 6169, 6683], + [7197, 7711, 8225]]), + 'time': 303240213}, + {'data': array([[0, 0, 0], + [0, 0, 0]]), 'time': 0}], + 'version': 1} + >>> del d['ignored'] + >>> b2 = experiment2.pack(d) + >>> b2 == b + True + """ + _byte_order_symbols = '@=<>!' + + def __init__(self, name, fields, byte_order='@'): + # '=' for native byte order, standard size and alignment + # See http://docs.python.org/library/struct for details + self.name = name + self.fields = fields + self.byte_order = byte_order + self.setup() + + def __str__(self): + return self.name + + def __repr__(self): + return '<{} {} {}>'.format( + self.__class__.__name__, self.name, id(self)) + + def setup(self): + """Setup any dynamic properties of a structure. + + Use this method to recalculate dynamic properities after + changing the basic properties set during initialization. + """ + _LOG.debug('setup {!r}'.format(self)) + self.set_byte_order(self.byte_order) + self.get_format() + + def set_byte_order(self, byte_order): + """Allow changing the format byte_order on the fly. + """ + _LOG.debug('set byte order for {!r} to {}'.format(self, byte_order)) + self.byte_order = byte_order + for field in self.fields: + if isinstance(field.format, Structure): + field.format.set_byte_order(byte_order) + + def get_format(self): + format = self.byte_order + ''.join(self.sub_format()) + # P format only allowed for native byte ordering + # Convert P to I for ILP32 compatibility when running on a LP64. + format = format.replace('P', 'I') + try: + super(Structure, self).__init__(format=format) + except _struct.error as e: + raise ValueError((e, format)) + return format + + def sub_format(self): + _LOG.debug('calculate sub-format for {!r}'.format(self)) + for field in self.fields: + if isinstance(field.format, Structure): + field_format = list( + field.format.sub_format()) * field.item_count + else: + field_format = [field.format]*field.item_count + for fmt in field_format: + yield fmt + + def _pack_item(self, item=None): + """Linearize a single count of the structure's data to a flat iterable + """ + if item is None: + item = {} + for f in self.fields: + try: + data = item[f.name] + except TypeError: + raise ValueError((f.name, item)) + except KeyError: + data = None + for arg in f.pack_data(data): + yield arg + + def _unpack_item(self, args): + """Inverse of ._unpack_item""" + data = {} + iterator = iter(args) + for f in self.fields: + try: + items = [next(iterator) for i in range(f.arg_count)] + except StopIteration: + raise ValueError('not enough data to unpack {}.{}'.format( + self, f)) + data[f.name] = f.unpack_data(items) + try: + next(iterator) + except StopIteration: + pass + else: + raise ValueError('too much data to unpack {}'.format(self)) + return data + + def pack(self, data): + args = list(self._pack_item(data)) + try: + return super(Structure, self).pack(*args) + except: + raise ValueError(self.format) + + def pack_into(self, buffer, offset=0, data={}): + args = list(self._pack_item(data)) + return super(Structure, self).pack_into( + buffer, offset, *args) + + def unpack(self, *args, **kwargs): + args = super(Structure, self).unpack(*args, **kwargs) + return self._unpack_item(args) + + def unpack_from(self, buffer, offset=0, *args, **kwargs): + _LOG.debug( + 'unpack {!r} for {!r} ({}, offset={}) with {} ({})'.format( + buffer, self, len(buffer), offset, self.format, self.size)) + args = super(Structure, self).unpack_from( + buffer, offset, *args, **kwargs) + return self._unpack_item(args) + + def get_field(self, name): + return [f for f in self.fields if f.name == name][0] + + +class DebuggingStream (object): + def __init__(self, stream): + self.stream = stream + + def read(self, size): + data = self.stream.read(size) + _LOG.debug('read {} from {}: ({}) {!r}'.format( + size, self.stream, len(data), data)) + return data + + +class DynamicStructure (Structure): + r"""Represent a C structure field with a dynamic definition. + + Any dynamic fields have their ``.pre_pack`` called before any + structure packing is done. ``.pre_unpack`` is called for a + particular field just before that field's ``.unpack_data`` call. + ``.post_unpack`` is called for a particular field just after + ``.unpack_data``. If ``.post_unpack`` returns ``True``, the same + field is unpacked again. + + Examples + -------- + + >>> from pprint import pprint + + This allows you to define structures where some portion of the + global structure depends on earlier data. For example, in the + quasi-C structure:: + + struct vector { + unsigned int length; + short data[length]; + }; + + You can generate a Python version of this structure in two ways, + with a dynamic ``length``, or with a dynamic ``data``. In both + cases, the required methods are the same, the only difference is + where you attach them. + + >>> def packer(self, parents, data): + ... vector_structure = parents[-1] + ... vector_data = self._get_structure_data( + ... parents, data, vector_structure) + ... length = len(vector_data['data']) + ... vector_data['length'] = length + ... data_field = vector_structure.get_field('data') + ... data_field.count = length + ... data_field.setup() + >>> def unpacker(self, parents, data): + ... vector_structure = parents[-1] + ... vector_data = self._get_structure_data( + ... parents, data, vector_structure) + ... length = vector_data['length'] + ... data_field = vector_structure.get_field('data') + ... data_field.count = length + ... data_field.setup() + + >>> class DynamicLengthField (DynamicField): + ... def pre_pack(self, parents, data): + ... packer(self, parents, data) + ... def post_unpack(self, parents, data): + ... unpacker(self, parents, data) + >>> dynamic_length_vector = DynamicStructure('vector', + ... fields=[ + ... DynamicLengthField('I', 'length'), + ... Field('h', 'data', count=0, array=True), + ... ], + ... byte_order='>') + >>> class DynamicDataField (DynamicField): + ... def pre_pack(self, parents, data): + ... packer(self, parents, data) + ... def pre_unpack(self, parents, data): + ... unpacker(self, parents, data) + >>> dynamic_data_vector = DynamicStructure('vector', + ... fields=[ + ... Field('I', 'length'), + ... DynamicDataField('h', 'data', count=0, array=True), + ... ], + ... byte_order='>') + + >>> b = b'\x00\x00\x00\x02\x01\x02\x03\x04' + >>> d = dynamic_length_vector.unpack(b) + >>> pprint(d) + {'data': array([258, 772]), 'length': 2} + >>> d = dynamic_data_vector.unpack(b) + >>> pprint(d) + {'data': array([258, 772]), 'length': 2} + + >>> d['data'] = [1,2,3,4] + >>> dynamic_length_vector.pack(d) + '\x00\x00\x00\x04\x00\x01\x00\x02\x00\x03\x00\x04' + >>> dynamic_data_vector.pack(d) + '\x00\x00\x00\x04\x00\x01\x00\x02\x00\x03\x00\x04' + + The implementation is a good deal more complicated than the one + for ``Structure``, because we must make multiple calls to + ``struct.Struct.unpack`` to unpack the data. + """ + #def __init__(self, *args, **kwargs): + # pass #self.parent = .. + + def _pre_pack(self, parents=None, data=None): + if parents is None: + parents = [self] + else: + parents = parents + [self] + for f in self.fields: + if hasattr(f, 'pre_pack'): + _LOG.debug('pre-pack {}'.format(f)) + f.pre_pack(parents=parents, data=data) + if isinstance(f.format, DynamicStructure): + _LOG.debug('pre-pack {!r}'.format(f.format)) + f._pre_pack(parents=parents, data=data) + + def pack(self, data): + self._pre_pack(data=data) + self.setup() + return super(DynamicStructure, self).pack(data) + + def pack_into(self, buffer, offset=0, data={}): + self._pre_pack(data=data) + self.setup() + return super(DynamicStructure, self).pack_into( + buffer=buffer, offset=offset, data=data) + + def unpack_stream(self, stream, parents=None, data=None, d=None): + # `d` is the working data directory + if data is None: + parents = [self] + data = d = {} + if _LOG.level <= _logging.DEBUG: + stream = DebuggingStream(stream) + else: + parents = parents + [self] + + for f in self.fields: + _LOG.debug('parsing {!r}.{} (count={}, item_count={})'.format( + self, f, f.count, f.item_count)) + if _LOG.level <= _logging.DEBUG: + _LOG.debug('data:\n{}'.format(_pprint.pformat(data))) + if hasattr(f, 'pre_unpack'): + _LOG.debug('pre-unpack {}'.format(f)) + f.pre_unpack(parents=parents, data=data) + + if hasattr(f, 'unpack'): # override default unpacking + _LOG.debug('override unpack for {}'.format(f)) + d[f.name] = f.unpack(stream) + continue + + # setup for unpacking loop + if isinstance(f.format, Structure): + f.format.set_byte_order(self.byte_order) + f.setup() + f.format.setup() + if isinstance(f.format, DynamicStructure): + if f.array: + d[f.name] = [] + for i in range(f.item_count): + x = {} + d[f.name].append(x) + f.format.unpack_stream( + stream, parents=parents, data=data, d=x) + else: + assert f.item_count == 1, (f, f.count) + d[f.name] = {} + f.format.unpack_stream( + stream, parents=parents, data=data, d=d[f.name]) + if hasattr(f, 'post_unpack'): + _LOG.debug('post-unpack {}'.format(f)) + repeat = f.post_unpack(parents=parents, data=data) + if repeat: + raise NotImplementedError( + 'cannot repeat unpack for dynamic structures') + continue + if isinstance(f.format, Structure): + _LOG.debug('parsing {} bytes for {}'.format( + f.format.size, f.format.format)) + bs = [stream.read(f.format.size) for i in range(f.item_count)] + def unpack(): + f.format.set_byte_order(self.byte_order) + f.setup() + f.format.setup() + x = [f.format.unpack_from(b) for b in bs] + if not f.array: + assert len(x) == 1, (f, f.count, x) + x = x[0] + return x + else: + field_format = self.byte_order + f.format*f.item_count + field_format = field_format.replace('P', 'I') + try: + size = _struct.calcsize(field_format) + except _struct.error as e: + _LOG.error(e) + _LOG.error('{}.{}: {}'.format(self, f, field_format)) + raise + _LOG.debug('parsing {} bytes for preliminary {}'.format( + size, field_format)) + raw = stream.read(size) + if len(raw) < size: + raise ValueError( + 'not enough data to unpack {}.{} ({} < {})'.format( + self, f, len(raw), size)) + def unpack(): + field_format = self.byte_order + f.format*f.item_count + field_format = field_format.replace('P', 'I') + _LOG.debug('parse previous bytes using {}'.format( + field_format)) + struct = _struct.Struct(field_format) + items = struct.unpack(raw) + return f.unpack_data(items) + + # unpacking loop + repeat = True + while repeat: + d[f.name] = unpack() + if hasattr(f, 'post_unpack'): + _LOG.debug('post-unpack {}'.format(f)) + repeat = f.post_unpack(parents=parents, data=data) + else: + repeat = False + if repeat: + _LOG.debug('repeat unpack for {}'.format(f)) + + return data + + def unpack(self, string): + stream = _io.BytesIO(string) + return self.unpack_stream(stream) + + def unpack_from(self, buffer, offset=0, *args, **kwargs): + args = super(Structure, self).unpack_from( + buffer, offset, *args, **kwargs) + return self._unpack_item(args) diff --git a/igor/util.py b/igor/util.py new file mode 100644 index 0000000..ecc783a --- /dev/null +++ b/igor/util.py @@ -0,0 +1,128 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +"Utility functions for handling buffers" + +import sys as _sys + +import numpy as _numpy + + +def _ord(byte): + r"""Convert a byte to an integer. + + >>> buffer = b'\x00\x01\x02' + >>> [_ord(b) for b in buffer] + [0, 1, 2] + """ + if _sys.version_info >= (3,): + return byte + else: + return ord(byte) + +def hex_bytes(buffer, spaces=None): + r"""Pretty-printing for binary buffers. + + >>> hex_bytes(b'\x00\x01\x02\x03\x04') + '0001020304' + >>> hex_bytes(b'\x00\x01\x02\x03\x04', spaces=1) + '00 01 02 03 04' + >>> hex_bytes(b'\x00\x01\x02\x03\x04', spaces=2) + '0001 0203 04' + >>> hex_bytes(b'\x00\x01\x02\x03\x04\x05\x06', spaces=2) + '0001 0203 0405 06' + >>> hex_bytes(b'\x00\x01\x02\x03\x04\x05\x06', spaces=3) + '000102 030405 06' + """ + hex_bytes = ['{:02x}'.format(_ord(x)) for x in buffer] + if spaces is None: + return ''.join(hex_bytes) + elif spaces is 1: + return ' '.join(hex_bytes) + for i in range(len(hex_bytes)//spaces): + hex_bytes.insert((spaces+1)*(i+1)-1, ' ') + return ''.join(hex_bytes) + +def assert_null(buffer, strict=True): + r"""Ensure an input buffer is entirely zero. + + >>> import sys + >>> assert_null(b'') + >>> assert_null(b'\x00\x00') + >>> assert_null(b'\x00\x01\x02\x03') + Traceback (most recent call last): + ... + ValueError: 00 01 02 03 + >>> stderr = sys.stderr + >>> sys.stderr = sys.stdout + >>> assert_null(b'\x00\x01\x02\x03', strict=False) + warning: post-data padding not zero: 00 01 02 03 + >>> sys.stderr = stderr + """ + if buffer and _ord(max(buffer)) != 0: + hex_string = hex_bytes(buffer, spaces=1) + if strict: + raise ValueError(hex_string) + else: + _sys.stderr.write( + 'warning: post-data padding not zero: {}\n'.format(hex_string)) + +# From ReadWave.c +def byte_order(needToReorderBytes): + little_endian = _sys.byteorder == 'little' + if needToReorderBytes: + little_endian = not little_endian + if little_endian: + return '<' # little-endian + return '>' # big-endian + +# From ReadWave.c +def need_to_reorder_bytes(version): + # If the low order byte of the version field of the BinHeader + # structure is zero then the file is from a platform that uses + # different byte-ordering and therefore all data will need to be + # reordered. + return version & 0xFF == 0 + +# From ReadWave.c +def checksum(buffer, byte_order, oldcksum, numbytes): + x = _numpy.ndarray( + (numbytes/2,), # 2 bytes to a short -- ignore trailing odd byte + dtype=_numpy.dtype(byte_order+'h'), + buffer=buffer) + oldcksum += x.sum() + if oldcksum > 2**31: # fake the C implementation's int rollover + oldcksum %= 2**32 + if oldcksum > 2**31: + oldcksum -= 2**31 + return oldcksum & 0xffff + +def _bytes(obj, encoding='utf-8'): + """Convert bytes or strings into bytes + + >>> _bytes(b'123') + '123' + >>> _bytes('123') + '123' + """ + if _sys.version_info >= (3,): + if isinstance(obj, bytes): + return obj + else: + return bytes(obj, encoding) + else: + return bytes(obj) diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index f8c7cf9..2c9ff77 --- a/setup.py +++ b/setup.py @@ -1,36 +1,65 @@ -#!/usr/bin/env python -import sys +# Copyright (C) 2011-2016 Paul Kienzle +# W. Trevor King +# +# This file is part of igor. +# +# igor is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# igor 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with igor. If not, see . + +"igor: interface for reading binary IGOR files." + from distutils.core import setup -import igor +import os.path -if len(sys.argv) == 1: - sys.argv.append('install') +from igor import __version__ -# README.rst is only needed to upload the package; -# it isn't needed for download and install. -try: - long_description = open('README.rst').read() -except: - long_description = None -dist = setup( - name = 'igor.py', - version = igor.__version__, - author='Paul Kienzle', - author_email='paul.kienzle@nist.gov', - url='https://github.com/reflectometry/igor.py', - description='Read Igor Pro files from python', - long_description=long_description, - classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Science/Research', - 'License :: Public Domain', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - ], - py_modules = ['igor'], - #data_files = ['README.rst'], -) -# End of file +package_name = 'igor' +_this_dir = os.path.dirname(__file__) +setup(name=package_name, + version=__version__, + maintainer='W. Trevor King', + maintainer_email='wking@tremily.us', + url='http://blog.tremily.us/posts/{}/'.format(package_name), + download_url='http://git.tremily.us/?p={}.git;a=snapshot;h=v{};sf=tgz'.format(package_name, __version__), + license='GNU Lesser General Public License v3 or later (LGPLv3+)', + platforms=['all'], + description=__doc__, + long_description=open(os.path.join(_this_dir, 'README'), 'r').read(), + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'Operating System :: OS Independent', + 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Topic :: Scientific/Engineering', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + packages=[ + 'igor', + 'igor.record', + ], + scripts=[ + 'bin/igorbinarywave.py', + 'bin/igorpackedexperiment.py', + ], + provides=['igor ({})'.format(__version__)], + ) diff --git a/test/data/README b/test/data/README new file mode 100644 index 0000000..7ec9646 --- /dev/null +++ b/test/data/README @@ -0,0 +1,4 @@ +.ibw samples are from TN003.zip. + +polar-graphs-demo.pxp was distributed with IGOR Pro 5.04 as + Examples/Graphing Techniques/Obsolete/Polar Graphs Demo.pxp diff --git a/test/data/mac-double.ibw b/test/data/mac-double.ibw new file mode 100644 index 0000000..9518508 Binary files /dev/null and b/test/data/mac-double.ibw differ diff --git a/test/data/mac-textWave.ibw b/test/data/mac-textWave.ibw new file mode 100644 index 0000000..4d8334e Binary files /dev/null and b/test/data/mac-textWave.ibw differ diff --git a/test/data/mac-version2.ibw b/test/data/mac-version2.ibw new file mode 100644 index 0000000..aabd17a Binary files /dev/null and b/test/data/mac-version2.ibw differ diff --git a/test/data/mac-version3Dependent.ibw b/test/data/mac-version3Dependent.ibw new file mode 100644 index 0000000..ac26da2 Binary files /dev/null and b/test/data/mac-version3Dependent.ibw differ diff --git a/test/data/mac-version5.ibw b/test/data/mac-version5.ibw new file mode 100644 index 0000000..1cf74de Binary files /dev/null and b/test/data/mac-version5.ibw differ diff --git a/test/data/mac-zeroPointWave.ibw b/test/data/mac-zeroPointWave.ibw new file mode 100644 index 0000000..93401bf Binary files /dev/null and b/test/data/mac-zeroPointWave.ibw differ diff --git a/test/data/polar-graphs-demo.pxp b/test/data/polar-graphs-demo.pxp new file mode 100644 index 0000000..63e03cc Binary files /dev/null and b/test/data/polar-graphs-demo.pxp differ diff --git a/test/data/win-double.ibw b/test/data/win-double.ibw new file mode 100644 index 0000000..ec768b8 Binary files /dev/null and b/test/data/win-double.ibw differ diff --git a/test/data/win-textWave.ibw b/test/data/win-textWave.ibw new file mode 100644 index 0000000..416cc02 Binary files /dev/null and b/test/data/win-textWave.ibw differ diff --git a/test/data/win-version2.ibw b/test/data/win-version2.ibw new file mode 100644 index 0000000..1a3f7a0 Binary files /dev/null and b/test/data/win-version2.ibw differ diff --git a/test/data/win-version5.ibw b/test/data/win-version5.ibw new file mode 100644 index 0000000..ed5739f Binary files /dev/null and b/test/data/win-version5.ibw differ diff --git a/test/data/win-zeroPointWave.ibw b/test/data/win-zeroPointWave.ibw new file mode 100644 index 0000000..521af56 Binary files /dev/null and b/test/data/win-zeroPointWave.ibw differ diff --git a/test/test-igorpy.py b/test/test-igorpy.py new file mode 100644 index 0000000..c419311 --- /dev/null +++ b/test/test-igorpy.py @@ -0,0 +1,202 @@ +# Copyright (C) 2012 W. Trevor King +# +# This file is part of %(project)s. +# +# %(project)s is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# %(project)s 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with %(project)s. If not, see . + +r"""Test the igor.igorpy compatibility layer by loading sample files. + +>>> from pprint import pprint +>>> import igor.igorpy as igor +>>> igor.ENCODING = 'UTF-8' + +Load a packed experiment: + +>>> path = data_path('polar-graphs-demo.pxp') +>>> d = igor.load(path) +>>> print(d) + +>>> dir(d) # doctest: +ELLIPSIS +['Packages', 'W_plrX5', 'W_plrX6', ..., 'radiusData', 'radiusQ1'] + + +Navigation: + +>>> print(d.Packages) + +>>> print(d[0]) # doctest: +ELLIPSIS + + + +Variables: + +>>> v = d[0] +>>> dir(v) # doctest: +ELLIPSIS +['__class__', ..., 'depstr', 'depvar', 'format', 'sysvar', 'userstr', 'uservar'] +>>> v.depstr +{} +>>> v.depvar +{} +>>> v.format() +'' +>>> pprint(v.sysvar) # doctest: +REPORT_UDIFF +{'K0': 0.0, + 'K1': 0.0, + 'K10': 0.0, + 'K11': 0.0, + 'K12': 0.0, + 'K13': 0.0, + 'K14': 0.0, + 'K15': 0.0, + 'K16': 0.0, + 'K17': 0.0, + 'K18': 0.0, + 'K19': 0.0, + 'K2': 0.0, + 'K20': 128.0, + 'K3': 0.0, + 'K4': 0.0, + 'K5': 0.0, + 'K6': 0.0, + 'K7': 0.0, + 'K8': 0.0, + 'K9': 0.0} +>>> v.userstr +{} +>>> v.uservar +{} + + +Waves: + +>>> d.W_plrX5 + +>>> dir(d.W_plrX5) # doctest: +ELLIPSIS +['__array__', ..., 'axis', 'axis_units', 'data', ..., 'name', 'notes'] +>>> d.W_plrX5.axis # doctest: +ELLIPSIS +[array([ 0.04908739, 0.04870087, 0.04831436, 0.04792784, 0.04754133, + 0.04715481, 0.0467683 , 0.04638178, 0.04599527, 0.04560875, + ... + 0.00077303, 0.00038651, 0. ]), array([], dtype=float64), array([], dtype=float64), array([], dtype=float64)] +>>> d.W_plrX5.data_units +(u'', '', '', '') +>>> d.W_plrX5.axis_units +(u'', '', '', '') +>>> d.W_plrX5.data # doctest: +ELLIPSIS +array([ 1.83690956e-17, 2.69450769e-02, 7.65399113e-02, + 1.44305170e-01, 2.23293692e-01, 3.04783821e-01, + ... + -2.72719120e-03, 5.24539061e-08], dtype=float32) + + +Dump the whole thing: + +>>> print(d.format()) +root + + + radiusData data (128) + angleData data (128) + W_plrX5 data (128) + W_plrY5 data (128) + angleQ1 data (64) + radiusQ1 data (64) + W_plrX6 data (64) + W_plrY6 data (64) + Packages + WMDataBase + + PolarGraphs + + + + + + +Load a packed experiment without ignoring unknown records: + +>>> d = igor.load(path, ignore_unknown=False) +>>> print(d.format()) +root + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + radiusData data (128) + angleData data (128) + W_plrX5 data (128) + W_plrY5 data (128) + angleQ1 data (64) + radiusQ1 data (64) + W_plrX6 data (64) + W_plrY6 data (64) + Packages + WMDataBase + + PolarGraphs + + + + + + +Try to load a binary wave: + +>>> path = data_path('mac-double.ibw') +>>> d = igor.load(path) +Traceback (most recent call last): + ... +IOError: final record too long; bad pxp file? +""" + +import os.path + +from igor import LOG + + +_this_dir = os.path.dirname(__file__) +_data_dir = os.path.join(_this_dir, 'data') + +def data_path(filename): + LOG.info('Testing igorpy compatibility {}\n'.format(filename)) + path = os.path.join(_data_dir, filename) + return path diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..1b40812 --- /dev/null +++ b/test/test.py @@ -0,0 +1,1500 @@ +# Copyright (C) 2012-2015 W. Trevor King +# +# This file is part of %(project)s. +# +# %(project)s is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# %(project)s 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 Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with %(project)s. If not, see . + +r"""Test the igor module by loading sample files. + +>>> dumpibw('mac-double.ibw') # doctest: +REPORT_UDIFF +{'version': 2, + 'wave': {'bin_header': {'checksum': 25137, + 'noteSize': 0, + 'pictSize': 0, + 'wfmSize': 166}, + 'note': '', + 'padding': array([], dtype=float64), + 'wData': array([ 5., 4., 3., 2., 1.]), + 'wave_header': {'aModified': 0, + 'bname': 'double', + 'botFullScale': 0.0, + 'creationDate': 3001587842, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 0, + 'fileName': 0, + 'formula': 0, + 'fsValid': 0, + 'hsA': 1.0, + 'hsB': 0.0, + 'kindBits': '\x00', + 'modDate': 3001587842, + 'next': 0, + 'npnts': 5, + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 4, + 'useBits': '\x00', + 'wModified': 0, + 'wUnused': array(['', ''], + dtype='|S1'), + 'waveNoteH': 0, + 'whVersion': 0, + 'xUnits': array(['', '', '', ''], + dtype='|S1')}}} + +>>> dumpibw('mac-textWave.ibw') # doctest: +REPORT_UDIFF +{'version': 5, + 'wave': {'bin_header': {'checksum': 5554, + 'dataEUnitsSize': 0, + 'dimEUnitsSize': array([0, 0, 0, 0]), + 'dimLabelsSize': array([0, 0, 0, 0]), + 'formulaSize': 0, + 'noteSize': 0, + 'optionsSize1': 0, + 'optionsSize2': 0, + 'sIndicesSize': 20, + 'wfmSize': 338}, + 'data_units': '', + 'dimension_units': '', + 'formula': '', + 'labels': [[], [], [], []], + 'note': '', + 'sIndices': array([ 4, 7, 8, 14, 18]), + 'wData': array(['Mary', 'had', 'a', 'little', 'lamb'], + dtype='|S6'), + 'wave_header': {'aModified': 0, + 'bname': 'text0', + 'botFullScale': 0.0, + 'creationDate': 3001571199, + 'dFolder': 69554896, + 'dLock': 0, + 'dataEUnits': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 22, + 'dimEUnits': array([0, 0, 0, 0]), + 'dimLabels': array([0, 0, 0, 0]), + 'dimUnits': array([['', '', '', ''], + ['', '', '', ''], + ['', '', '', ''], + ['', '', '', '']], + dtype='|S1'), + 'fileName': 0, + 'formula': 0, + 'fsValid': 0, + 'kindBits': '\x00', + 'modDate': 3001571215, + 'nDim': array([5, 0, 0, 0]), + 'next': 0, + 'npnts': 5, + 'sIndices': 69557296, + 'sfA': array([ 1., 1., 1., 1.]), + 'sfB': array([ 0., 0., 0., 0.]), + 'srcFldr': 0, + 'swModified': 1, + 'topFullScale': 0.0, + 'type': 0, + 'useBits': '\x00', + 'wModified': 0, + 'waveNoteH': 0, + 'whUnused': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 'whVersion': 1, + 'whpad1': array(['', '', '', '', '', ''], + dtype='|S1'), + 'whpad2': 0, + 'whpad3': 0, + 'whpad4': 0}}} + +>>> dumpibw('mac-version2.ibw') # doctest: +REPORT_UDIFF +{'version': 2, + 'wave': {'bin_header': {'checksum': -16803, + 'noteSize': 15, + 'pictSize': 0, + 'wfmSize': 146}, + 'note': 'This is a test.', + 'padding': array([], dtype=float64), + 'wData': array([ 5., 4., 3., 2., 1.], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'version2', + 'botFullScale': 0.0, + 'creationDate': 3001251979, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 0, + 'fileName': 0, + 'formula': 0, + 'fsValid': 0, + 'hsA': 1.0, + 'hsB': 0.0, + 'kindBits': '\x00', + 'modDate': 3001573594, + 'next': 0, + 'npnts': 5, + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'wUnused': array(['', ''], + dtype='|S1'), + 'waveNoteH': 0, + 'whVersion': 0, + 'xUnits': array(['', '', '', ''], + dtype='|S1')}}} + +>>> dumpibw('mac-version3Dependent.ibw') # doctest: +REPORT_UDIFF +{'version': 3, + 'wave': {'bin_header': {'checksum': -32334, + 'formulaSize': 4, + 'noteSize': 0, + 'pictSize': 0, + 'wfmSize': 126}, + 'formula': ' K0', + 'note': '', + 'padding': array([], dtype=float64), + 'wData': array([], dtype=float32), + 'wave_header': {'aModified': 3, + 'bname': 'version3Dependent', + 'botFullScale': 0.0, + 'creationDate': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 23, + 'fileName': 0, + 'formula': 103408364, + 'fsValid': 0, + 'hsA': 1.0, + 'hsB': 0.0, + 'kindBits': '\x00', + 'modDate': 3001672861, + 'next': 0, + 'npnts': 10, + 'srcFldr': 0, + 'swModified': 1, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 1, + 'wUnused': array(['', ''], + dtype='|S1'), + 'waveNoteH': 0, + 'whVersion': 0, + 'xUnits': array(['', '', '', ''], + dtype='|S1')}}} + +>>> dumpibw('mac-version5.ibw') # doctest: +REPORT_UDIFF +{'version': 5, + 'wave': {'bin_header': {'checksum': -12033, + 'dataEUnitsSize': 0, + 'dimEUnitsSize': array([0, 0, 0, 0]), + 'dimLabelsSize': array([64, 0, 0, 0]), + 'formulaSize': 0, + 'noteSize': 15, + 'optionsSize1': 0, + 'optionsSize2': 0, + 'sIndicesSize': 0, + 'wfmSize': 340}, + 'data_units': '', + 'dimension_units': '', + 'formula': '', + 'labels': [['', 'Column0'], [], [], []], + 'note': 'This is a test.', + 'sIndices': array([], dtype=float64), + 'wData': array([ 5., 4., 3., 2., 1.], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'version5', + 'botFullScale': 0.0, + 'creationDate': 3001252180, + 'dFolder': 69554896, + 'dLock': 0, + 'dataEUnits': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 27, + 'dimEUnits': array([0, 0, 0, 0]), + 'dimLabels': array([69554136, 0, 0, 0]), + 'dimUnits': array([['', '', '', ''], + ['', '', '', ''], + ['', '', '', ''], + ['', '', '', '']], + dtype='|S1'), + 'fileName': 69554292, + 'formula': 0, + 'fsValid': 0, + 'kindBits': '\x00', + 'modDate': 3001573601, + 'nDim': array([5, 0, 0, 0]), + 'next': 69555212, + 'npnts': 5, + 'sIndices': 0, + 'sfA': array([ 1., 1., 1., 1.]), + 'sfB': array([ 0., 0., 0., 0.]), + 'srcFldr': -32349, + 'swModified': 1, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'waveNoteH': 69554032, + 'whUnused': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 'whVersion': 1, + 'whpad1': array(['', '', '', '', '', ''], + dtype='|S1'), + 'whpad2': 0, + 'whpad3': 0, + 'whpad4': 0}}} + +>>> dumpibw('mac-zeroPointWave.ibw') # doctest: +REPORT_UDIFF +{'version': 5, + 'wave': {'bin_header': {'checksum': -15649, + 'dataEUnitsSize': 0, + 'dimEUnitsSize': array([0, 0, 0, 0]), + 'dimLabelsSize': array([0, 0, 0, 0]), + 'formulaSize': 0, + 'noteSize': 0, + 'optionsSize1': 0, + 'optionsSize2': 0, + 'sIndicesSize': 0, + 'wfmSize': 320}, + 'data_units': '', + 'dimension_units': '', + 'formula': '', + 'labels': [[], [], [], []], + 'note': '', + 'sIndices': array([], dtype=float64), + 'wData': array([], dtype=float32), + 'wave_header': {'aModified': 3, + 'bname': 'zeroWave', + 'botFullScale': 0.0, + 'creationDate': 3001573964, + 'dFolder': 69554896, + 'dLock': 0, + 'dataEUnits': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 29, + 'dimEUnits': array([0, 0, 0, 0]), + 'dimLabels': array([0, 0, 0, 0]), + 'dimUnits': array([['', '', '', ''], + ['', '', '', ''], + ['', '', '', ''], + ['', '', '', '']], + dtype='|S1'), + 'fileName': 0, + 'formula': 0, + 'fsValid': 0, + 'kindBits': '\x00', + 'modDate': 3001573964, + 'nDim': array([0, 0, 0, 0]), + 'next': 0, + 'npnts': 0, + 'sIndices': 0, + 'sfA': array([ 1., 1., 1., 1.]), + 'sfB': array([ 0., 0., 0., 0.]), + 'srcFldr': 0, + 'swModified': 1, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 1, + 'waveNoteH': 0, + 'whUnused': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 'whVersion': 1, + 'whpad1': array(['', '', '', '', '', ''], + dtype='|S1'), + 'whpad2': 0, + 'whpad3': 0, + 'whpad4': 0}}} + +>>> dumpibw('win-double.ibw') # doctest: +REPORT_UDIFF +{'version': 2, + 'wave': {'bin_header': {'checksum': 28962, + 'noteSize': 0, + 'pictSize': 0, + 'wfmSize': 166}, + 'note': '', + 'padding': array([], dtype=float64), + 'wData': array([ 5., 4., 3., 2., 1.]), + 'wave_header': {'aModified': 0, + 'bname': 'double', + 'botFullScale': 0.0, + 'creationDate': 3001587842, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 0, + 'fileName': 0, + 'formula': 0, + 'fsValid': 0, + 'hsA': 1.0, + 'hsB': 0.0, + 'kindBits': '\x00', + 'modDate': 3001587842, + 'next': 0, + 'npnts': 5, + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 4, + 'useBits': '\x00', + 'wModified': 0, + 'wUnused': array(['', ''], + dtype='|S1'), + 'waveNoteH': 0, + 'whVersion': 0, + 'xUnits': array(['', '', '', ''], + dtype='|S1')}}} + +>>> dumpibw('win-textWave.ibw') # doctest: +REPORT_UDIFF +{'version': 5, + 'wave': {'bin_header': {'checksum': 184, + 'dataEUnitsSize': 0, + 'dimEUnitsSize': array([0, 0, 0, 0]), + 'dimLabelsSize': array([0, 0, 0, 0]), + 'formulaSize': 0, + 'noteSize': 0, + 'optionsSize1': 0, + 'optionsSize2': 0, + 'sIndicesSize': 20, + 'wfmSize': 338}, + 'data_units': '', + 'dimension_units': '', + 'formula': '', + 'labels': [[], [], [], []], + 'note': '', + 'sIndices': array([ 4, 7, 8, 14, 18]), + 'wData': array(['Mary', 'had', 'a', 'little', 'lamb'], + dtype='|S6'), + 'wave_header': {'aModified': 0, + 'bname': 'text0', + 'botFullScale': 0.0, + 'creationDate': 3001571199, + 'dFolder': 8108612, + 'dLock': 0, + 'dataEUnits': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 32, + 'dimEUnits': array([0, 0, 0, 0]), + 'dimLabels': array([0, 0, 0, 0]), + 'dimUnits': array([['', '', '', ''], + ['', '', '', ''], + ['', '', '', ''], + ['', '', '', '']], + dtype='|S1'), + 'fileName': 7814472, + 'formula': 0, + 'fsValid': 0, + 'kindBits': '\x00', + 'modDate': 3001571215, + 'nDim': array([5, 0, 0, 0]), + 'next': 0, + 'npnts': 5, + 'sIndices': 8133100, + 'sfA': array([ 1., 1., 1., 1.]), + 'sfB': array([ 0., 0., 0., 0.]), + 'srcFldr': -1007, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 0, + 'useBits': '\x00', + 'wModified': 1, + 'waveNoteH': 0, + 'whUnused': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 'whVersion': 1, + 'whpad1': array(['', '', '', '', '', ''], + dtype='|S1'), + 'whpad2': 0, + 'whpad3': 0, + 'whpad4': 0}}} + +>>> dumpibw('win-version2.ibw') # doctest: +REPORT_UDIFF +{'version': 2, + 'wave': {'bin_header': {'checksum': 1047, + 'noteSize': 15, + 'pictSize': 0, + 'wfmSize': 146}, + 'note': 'This is a test.', + 'padding': array([], dtype=float64), + 'wData': array([ 5., 4., 3., 2., 1.], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'version2', + 'botFullScale': 0.0, + 'creationDate': 3001251979, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 0, + 'fileName': 0, + 'formula': 0, + 'fsValid': 0, + 'hsA': 1.0, + 'hsB': 0.0, + 'kindBits': '\x00', + 'modDate': 3001573594, + 'next': 0, + 'npnts': 5, + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'wUnused': array(['', ''], + dtype='|S1'), + 'waveNoteH': 0, + 'whVersion': 0, + 'xUnits': array(['', '', '', ''], + dtype='|S1')}}} + +>>> dumpibw('win-version5.ibw') # doctest: +REPORT_UDIFF +{'version': 5, + 'wave': {'bin_header': {'checksum': 13214, + 'dataEUnitsSize': 0, + 'dimEUnitsSize': array([0, 0, 0, 0]), + 'dimLabelsSize': array([64, 0, 0, 0]), + 'formulaSize': 0, + 'noteSize': 15, + 'optionsSize1': 0, + 'optionsSize2': 0, + 'sIndicesSize': 0, + 'wfmSize': 340}, + 'data_units': '', + 'dimension_units': '', + 'formula': '', + 'labels': [['', 'Column0'], [], [], []], + 'note': 'This is a test.', + 'sIndices': array([], dtype=float64), + 'wData': array([ 5., 4., 3., 2., 1.], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'version5', + 'botFullScale': 0.0, + 'creationDate': 3001252180, + 'dFolder': 8108612, + 'dLock': 0, + 'dataEUnits': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 30, + 'dimEUnits': array([0, 0, 0, 0]), + 'dimLabels': array([8138784, 0, 0, 0]), + 'dimUnits': array([['', '', '', ''], + ['', '', '', ''], + ['', '', '', ''], + ['', '', '', '']], + dtype='|S1'), + 'fileName': 8131824, + 'formula': 0, + 'fsValid': 0, + 'kindBits': '\x00', + 'modDate': 3001573601, + 'nDim': array([5, 0, 0, 0]), + 'next': 8125236, + 'npnts': 5, + 'sIndices': 0, + 'sfA': array([ 1., 1., 1., 1.]), + 'sfB': array([ 0., 0., 0., 0.]), + 'srcFldr': -1007, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 1, + 'waveNoteH': 8131596, + 'whUnused': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 'whVersion': 1, + 'whpad1': array(['', '', '', '', '', ''], + dtype='|S1'), + 'whpad2': 0, + 'whpad3': 0, + 'whpad4': 0}}} + +>>> dumpibw('win-zeroPointWave.ibw') # doctest: +REPORT_UDIFF +{'version': 5, + 'wave': {'bin_header': {'checksum': 27541, + 'dataEUnitsSize': 0, + 'dimEUnitsSize': array([0, 0, 0, 0]), + 'dimLabelsSize': array([0, 0, 0, 0]), + 'formulaSize': 0, + 'noteSize': 0, + 'optionsSize1': 0, + 'optionsSize2': 0, + 'sIndicesSize': 0, + 'wfmSize': 320}, + 'data_units': '', + 'dimension_units': '', + 'formula': '', + 'labels': [[], [], [], []], + 'note': '', + 'sIndices': array([], dtype=float64), + 'wData': array([], dtype=float32), + 'wave_header': {'aModified': 3, + 'bname': 'zeroWave', + 'botFullScale': 0.0, + 'creationDate': 3001573964, + 'dFolder': 8108612, + 'dLock': 0, + 'dataEUnits': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 31, + 'dimEUnits': array([0, 0, 0, 0]), + 'dimLabels': array([0, 0, 0, 0]), + 'dimUnits': array([['', '', '', ''], + ['', '', '', ''], + ['', '', '', ''], + ['', '', '', '']], + dtype='|S1'), + 'fileName': 8125252, + 'formula': 0, + 'fsValid': 0, + 'kindBits': '\x00', + 'modDate': 3001573964, + 'nDim': array([0, 0, 0, 0]), + 'next': 8133140, + 'npnts': 0, + 'sIndices': 0, + 'sfA': array([ 1., 1., 1., 1.]), + 'sfB': array([ 0., 0., 0., 0.]), + 'srcFldr': -1007, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 1, + 'waveNoteH': 0, + 'whUnused': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 'whVersion': 1, + 'whpad1': array(['', '', '', '', '', ''], + dtype='|S1'), + 'whpad2': 0, + 'whpad3': 0, + 'whpad4': 0}}} + +>>> dumppxp('polar-graphs-demo.pxp') # doctest: +REPORT_UDIFF, +ELLIPSIS +record 0: + +record 1: + +record 2: + +record 3: + +record 4: + +record 5: + +record 6: + +record 7: + +record 8: + +record 9: + +record 10: + +record 11: + +record 12: + +record 13: + +record 14: + +record 15: + +record 16: + +record 17: + +record 18: + +record 19: + +record 20: + +record 21: + +record 22: + +record 23: + +record 24: + +record 25: + +record 26: + +record 27: + +record 28: + +record 29: + +record 30: +{'variables': {'sysVars': {'K0': 0.0, + 'K1': 0.0, + 'K10': 0.0, + 'K11': 0.0, + 'K12': 0.0, + 'K13': 0.0, + 'K14': 0.0, + 'K15': 0.0, + 'K16': 0.0, + 'K17': 0.0, + 'K18': 0.0, + 'K19': 0.0, + 'K2': 0.0, + 'K20': 128.0, + 'K3': 0.0, + 'K4': 0.0, + 'K5': 0.0, + 'K6': 0.0, + 'K7': 0.0, + 'K8': 0.0, + 'K9': 0.0}, + 'userStrs': {}, + 'userVars': {}, + 'var_header': {'numSysVars': 21, + 'numUserStrs': 0, + 'numUserVars': 0}}, + 'version': 1} +record 31: +'\x95 Polar Graphs Demo, v3.01\n\n' +record 32: +{'version': 2, + 'wave': {'bin_header': {'checksum': -25004, + 'noteSize': 0, + 'pictSize': 0, + 'wfmSize': 638}, + 'note': '', + 'padding': array([], dtype=float64), + 'wData': array([ 0.30000001, 0.5448544 , 0.77480197, 0.97584349, 1.13573945, + 1.24475539, 1.2962544 , 1.28710103, 1.21785283, 1.09272552, + 0.91933674, 0.7082426 , 0.47229454, 0.22585714, -0.01606643, + -0.23874778, -0.42862982, -0.57415301, -0.6664573 , -0.69992352, + -0.67251408, -0.58589762, -0.44534767, -0.25942117, -0.03943586, + 0.20121357, 0.44787762, 0.68553883, 0.89972788, 1.0774051 , + 1.20775461, 1.28283918, 1.29808831, 1.25257373, 1.14906585, + 0.99386656, 0.79642528, 0.56876069, 0.32473388, 0.07920124, + -0.15288824, -0.35740662, -0.52190179, -0.63635898, -0.69381076, + -0.69075894, -0.62739003, -0.5075599 , -0.3385666 , -0.13069656, + 0.10339352, 0.34945396, 0.59250361, 0.81774551, 1.01146686, + 1.16187334, 1.25980926, 1.29931164, 1.27797604, 1.1971004 , + 1.06160903, 0.87975079, 0.66259789, 0.42336911, 0.17663053, + -0.06259823, -0.2797519 , -0.46160996, -0.59710097, -0.67797607, + -0.69931161, -0.65980917, -0.56187314, -0.41146588, -0.21774435, + 0.00749773, 0.25054744, 0.49660596, 0.7306987 , 0.93856692, + 1.10756063, 1.22738981, 1.29075909, 1.29381061, 1.23635852, + 1.1219027 , 0.95740634, 0.7528879 , 0.52079749, 0.2752648 , + 0.03123802, -0.19642642, -0.39386547, -0.54906607, -0.6525743 , + -0.69808841, -0.68283898, -0.60775399, -0.47740453, -0.29972947, + -0.08553842, 0.15212469, 0.39878684, 0.63943672, 0.85942155, + 1.04534864, 1.18589854, 1.2725141 , 1.29992342, 1.2664578 , + 1.17415261, 1.0286293 , 0.83874667, 0.61606491, 0.37414294, + 0.12770344, -0.1082412 , -0.31933719, -0.49272597, -0.61785328, + -0.6871013 , -0.69625437, -0.64475471, -0.53574032, -0.37584305, + -0.17479956, 0.05514668, 0.30000135], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'radiusData', + 'botFullScale': 0.0, + 'creationDate': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 0, + 'fileName': 0, + 'formula': 0, + 'fsValid': 0, + 'hsA': 0.04908738521234052, + 'hsB': 0.0, + 'kindBits': '\x00', + 'modDate': 2845545774, + 'next': 0, + 'npnts': 128, + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'wUnused': array(['', ''], + dtype='|S1'), + 'waveNoteH': 0, + 'whVersion': 0, + 'xUnits': array(['', '', '', ''], + dtype='|S1')}}} +record 33: +{'version': 2, + 'wave': {'bin_header': {'checksum': 28621, + 'noteSize': 0, + 'pictSize': 0, + 'wfmSize': 638}, + 'note': '', + 'padding': array([], dtype=float64), + 'wData': array([ 0. , 0.0494739 , 0.0989478 , 0.1484217 , 0.1978956 , + 0.24736951, 0.29684341, 0.34631732, 0.3957912 , 0.44526511, + 0.49473903, 0.54421294, 0.59368682, 0.6431607 , 0.69263464, + 0.74210852, 0.79158241, 0.84105635, 0.89053023, 0.94000411, + 0.98947805, 1.03895199, 1.08842587, 1.13789964, 1.18737364, + 1.23684752, 1.2863214 , 1.3357954 , 1.38526928, 1.43474305, + 1.48421705, 1.53369093, 1.58316481, 1.63263881, 1.68211269, + 1.73158658, 1.78106046, 1.83053434, 1.88000822, 1.92948222, + 1.9789561 , 2.02842999, 2.07790399, 2.12737775, 2.17685175, + 2.22632551, 2.27579927, 2.32527351, 2.37474728, 2.42422128, + 2.47369504, 2.52316904, 2.5726428 , 2.6221168 , 2.67159081, + 2.72106457, 2.77053857, 2.82001233, 2.86948609, 2.91896009, + 2.9684341 , 3.0179081 , 3.06738186, 3.11685586, 3.16632962, + 3.21580338, 3.26527762, 3.31475139, 3.36422539, 3.41369915, + 3.46317315, 3.51264691, 3.56212091, 3.61159492, 3.66106868, + 3.71054268, 3.76001644, 3.8094902 , 3.85896444, 3.90843821, + 3.95791221, 4.00738621, 4.05685997, 4.10633373, 4.15580797, + 4.20528126, 4.2547555 , 4.30422926, 4.3537035 , 4.40317726, + 4.45265102, 4.50212526, 4.55159855, 4.60107279, 4.65054703, + 4.70002079, 4.74949455, 4.79896832, 4.84844255, 4.89791584, + 4.94739008, 4.99686432, 5.04633808, 5.09581184, 5.14528561, + 5.19475985, 5.24423361, 5.29370737, 5.34318161, 5.3926549 , + 5.44212914, 5.4916029 , 5.54107714, 5.5905509 , 5.64002466, + 5.6894989 , 5.73897219, 5.78844643, 5.83792019, 5.88739443, + 5.93686819, 5.98634195, 6.03581619, 6.08528948, 6.13476372, + 6.18423796, 6.23371172, 6.28318548], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'angleData', + 'botFullScale': 0.0, + 'creationDate': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 0, + 'fileName': 0, + 'formula': 0, + 'fsValid': 0, + 'hsA': 0.04908738521234052, + 'hsB': 0.0, + 'kindBits': '\x00', + 'modDate': 2845470039, + 'next': 0, + 'npnts': 128, + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'wUnused': array(['', ''], + dtype='|S1'), + 'waveNoteH': 0, + 'whVersion': 0, + 'xUnits': array(['', '', '', ''], + dtype='|S1')}}} +record 34: +{'version': 5, + 'wave': {'bin_header': {'checksum': 23021, + 'dataEUnitsSize': 0, + 'dimEUnitsSize': array([0, 0, 0, 0]), + 'dimLabelsSize': array([0, 0, 0, 0]), + 'formulaSize': 80, + 'noteSize': 0, + 'optionsSize1': 0, + 'optionsSize2': 0, + 'sIndicesSize': 0, + 'wfmSize': 832}, + 'data_units': '', + 'dimension_units': '', + 'formula': ' PolarRadiusFunction(radiusData,1,0) * cos(PolarAngleFunction(angleData,3,1,2))', + 'labels': [[], [], [], []], + 'note': '', + 'sIndices': array([], dtype=float64), + 'wData': array([ 1.83690956e-17, 2.69450769e-02, 7.65399113e-02, + 1.44305170e-01, 2.23293692e-01, 3.04783821e-01, + 3.79158467e-01, 4.36888516e-01, 4.69528973e-01, + 4.70633775e-01, 4.36502904e-01, 3.66688997e-01, + 2.64211357e-01, 1.35452762e-01, -1.02594923e-02, + -1.61356136e-01, -3.04955602e-01, -4.27943677e-01, + -5.18107474e-01, -5.65230608e-01, -5.62046587e-01, + -5.04969478e-01, -3.94532531e-01, -2.35490710e-01, + -3.65724117e-02, 1.90097600e-01, 4.29877043e-01, + 6.66696191e-01, 8.84287775e-01, 1.06744885e+00, + 1.20323074e+00, 1.28195620e+00, 1.29798901e+00, + 1.25017929e+00, 1.14195395e+00, 9.81046736e-01, + 7.78884649e-01, 5.49682915e-01, 3.09332967e-01, + 7.41607845e-02, -1.40328899e-01, -3.20629656e-01, + -4.56221938e-01, -5.40310800e-01, -5.70244014e-01, + -5.47582209e-01, -4.77826297e-01, -3.69823217e-01, + -2.34920204e-01, -8.59207287e-02, 6.40354082e-02, + 2.02596441e-01, 3.19209903e-01, 4.05949473e-01, + 4.58081126e-01, 4.74326164e-01, 4.56804305e-01, + 4.10668582e-01, 3.43470216e-01, 2.64317334e-01, + 1.82909429e-01, 1.08534366e-01, 4.91267964e-02, + 1.04717268e-02, -4.36885841e-03, 4.64119762e-03, + 3.45129520e-02, 7.95329511e-02, 1.31838784e-01, + 1.82213545e-01, 2.21028924e-01, 2.39245579e-01, + 2.29380637e-01, 1.86348081e-01, 1.08093813e-01, + -4.03938442e-03, -1.45255283e-01, -3.07566285e-01, + -4.80366081e-01, -6.51240766e-01, -8.07001889e-01, + -9.34792042e-01, -1.02321768e+00, -1.06338477e+00, + -1.04975033e+00, -9.80714381e-01, -8.58889818e-01, + -6.91040277e-01, -4.87653464e-01, -2.62210011e-01, + -3.01902127e-02, 1.92100301e-01, 3.88785005e-01, + 5.45667768e-01, 6.51326835e-01, 6.98035002e-01, + 6.82368934e-01, 6.05477571e-01, 4.72992837e-01, + 2.94585884e-01, 8.31873119e-02, -1.46010652e-01, + -3.76755983e-01, -5.93006968e-01, -7.80143738e-01, + -9.26071882e-01, -1.02209401e+00, -1.06349015e+00, + -1.04976654e+00, -9.84551251e-01, -8.75151932e-01, + -7.31834948e-01, -5.66861272e-01, -3.93398553e-01, + -2.24383846e-01, -7.14399144e-02, 5.60413450e-02, + 1.51621893e-01, 2.12215677e-01, 2.38205954e-01, + 2.33226836e-01, 2.03656554e-01, 1.57870770e-01, + 1.05330117e-01, 5.55786416e-02, 1.72677450e-02, + -2.72719120e-03, 5.24539061e-08], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'W_plrX5', + 'botFullScale': 0.0, + 'creationDate': 0, + 'dFolder': 7848580, + 'dLock': 0, + 'dataEUnits': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 24, + 'dimEUnits': array([0, 0, 0, 0]), + 'dimLabels': array([0, 0, 0, 0]), + 'dimUnits': array([['', '', '', ''], + ['', '', '', ''], + ['', '', '', ''], + ['', '', '', '']], + dtype='|S1'), + 'fileName': 0, + 'formula': 8054500, + 'fsValid': 0, + 'kindBits': '\x00', + 'modDate': 2985072242, + 'nDim': array([128, 0, 0, 0]), + 'next': 8054516, + 'npnts': 128, + 'sIndices': 0, + 'sfA': array([ 0.04908739, 1. , 1. , 1. ]), + 'sfB': array([ 0., 0., 0., 0.]), + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'waveNoteH': 0, + 'whUnused': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 'whVersion': 1, + 'whpad1': array(['', '', '', '', '', ''], + dtype='|S1'), + 'whpad2': 0, + 'whpad3': 0, + 'whpad4': 0}}} +record 35: +{'version': 5, + 'wave': {'bin_header': {'checksum': -9146, + 'dataEUnitsSize': 0, + 'dimEUnitsSize': array([0, 0, 0, 0]), + 'dimLabelsSize': array([0, 0, 0, 0]), + 'formulaSize': 80, + 'noteSize': 82, + 'optionsSize1': 0, + 'optionsSize2': 0, + 'sIndicesSize': 0, + 'wfmSize': 832}, + 'data_units': '', + 'dimension_units': '', + 'formula': ' PolarRadiusFunction(radiusData,1,0) * sin(PolarAngleFunction(angleData,3,1,2))', + 'labels': [[], [], [], []], + 'note': 'shadowX=W_plrX5,appendRadius=radiusData,appendAngleData=angleData,angleDataUnits=2', + 'sIndices': array([], dtype=float64), + 'wData': array([ 0.30000001, 0.54418772, 0.77101213, 0.96511477, 1.1135726 , + 1.20686483, 1.23956215, 1.21068466, 1.12370288, 0.98618096, + 0.80910152, 0.60592639, 0.39147732, 0.18073183, -0.01236418, + -0.17596789, -0.30120692, -0.38277394, -0.41920158, -0.41280419, + -0.36929506, -0.29712263, -0.20658807, -0.10882771, -0.01475283, + 0.06595302, 0.12569843, 0.15962352, 0.16596791, 0.14613269, + 0.10443594, 0.04758934, -0.01605497, -0.0774129 , -0.12764584, + -0.15911636, -0.16622847, -0.14607331, -0.09881912, -0.02780312, + 0.06068454, 0.15791172, 0.25346208, 0.33617997, 0.3952153 , + 0.42107204, 0.40657136, 0.34763175, 0.24380288, 0.09848462, + -0.08117689, -0.28473276, -0.49916485, -0.70986813, -0.90179092, + -1.06064332, -1.17407382, -1.23270524, -1.23095524, -1.16755545, + -1.04573321, -0.87303019, -0.66077417, -0.42323959, -0.1765765 , + 0.06242594, 0.2776148 , 0.45470679, 0.58236426, 0.65303123, + 0.66346282, 0.61490625, 0.51291907, 0.36684951, 0.18901938, + -0.00631659, -0.20414437, -0.389898 , -0.55060786, -0.67586488, + -0.75857663, -0.79539269, -0.78681922, -0.73699296, -0.65315133, + -0.54485315, -0.42300734, -0.29883695, -0.18282266, -0.08376524, + -0.00802278, 0.0409977 , 0.06305727, 0.06099379, 0.04033075, + 0.00863387, -0.02533132, -0.05255322, -0.06475239, -0.05528941, + -0.01991711, 0.04269439, 0.13071296, 0.23921135, 0.36052904, + 0.48491719, 0.60139763, 0.69877088, 0.76667541, 0.79660165, + 0.78277934, 0.72283876, 0.6181944 , 0.47410288, 0.29939076, + 0.10585135, -0.09260413, -0.28104633, -0.44468346, -0.57008827, + -0.64630753, -0.66580337, -0.62512833, -0.52528399, -0.37171093, + -0.17394456, 0.0550792 , 0.30000135], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'W_plrY5', + 'botFullScale': 0.0, + 'creationDate': 0, + 'dFolder': 7848580, + 'dLock': 0, + 'dataEUnits': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 26, + 'dimEUnits': array([0, 0, 0, 0]), + 'dimLabels': array([0, 0, 0, 0]), + 'dimUnits': array([['', '', '', ''], + ['', '', '', ''], + ['', '', '', ''], + ['', '', '', '']], + dtype='|S1'), + 'fileName': 0, + 'formula': 8054532, + 'fsValid': 0, + 'kindBits': '\x00', + 'modDate': 2985072242, + 'nDim': array([128, 0, 0, 0]), + 'next': 8084972, + 'npnts': 128, + 'sIndices': 0, + 'sfA': array([ 0.04908739, 1. , 1. , 1. ]), + 'sfB': array([ 0., 0., 0., 0.]), + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'waveNoteH': 7996608, + 'whUnused': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 'whVersion': 1, + 'whpad1': array(['', '', '', '', '', ''], + dtype='|S1'), + 'whpad2': 0, + 'whpad3': 0, + 'whpad4': 0}}} +record 36: +{'version': 2, + 'wave': {'bin_header': {'checksum': 14307, + 'noteSize': 0, + 'pictSize': 0, + 'wfmSize': 382}, + 'note': '', + 'padding': array([], dtype=float64), + 'wData': array([ 0.2617994 , 0.27842158, 0.29504377, 0.31166595, 0.32828814, + 0.34491032, 0.36153251, 0.3781547 , 0.39477688, 0.41139907, + 0.42802125, 0.44464344, 0.46126559, 0.47788778, 0.49450997, + 0.51113212, 0.52775431, 0.54437649, 0.56099868, 0.57762086, + 0.59424305, 0.61086524, 0.62748742, 0.64410961, 0.66073179, + 0.67735398, 0.69397616, 0.71059835, 0.72722054, 0.74384272, + 0.76046491, 0.77708709, 0.79370928, 0.81033146, 0.82695365, + 0.84357584, 0.86019802, 0.87682021, 0.89344239, 0.91006458, + 0.92668676, 0.94330889, 0.95993114, 0.97655326, 0.99317551, + 1.00979757, 1.02641988, 1.04304194, 1.05966425, 1.07628632, + 1.09290862, 1.10953069, 1.12615299, 1.14277506, 1.15939736, + 1.17601943, 1.19264174, 1.2092638 , 1.22588611, 1.24250817, + 1.25913048, 1.27575254, 1.29237485, 1.30899692], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'angleQ1', + 'botFullScale': 0.0, + 'creationDate': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 0, + 'fileName': 0, + 'formula': 0, + 'fsValid': 0, + 'hsA': 1.0, + 'hsB': 0.0, + 'kindBits': '\x00', + 'modDate': 2845473705, + 'next': 0, + 'npnts': 64, + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'wUnused': array(['', ''], + dtype='|S1'), + 'waveNoteH': 0, + 'whVersion': 0, + 'xUnits': array(['', '', '', ''], + dtype='|S1')}}} +record 37: +{'version': 2, + 'wave': {'bin_header': {'checksum': -12080, + 'noteSize': 0, + 'pictSize': 0, + 'wfmSize': 382}, + 'note': '', + 'padding': array([], dtype=float64), + 'wData': array([ -8.34064484, -7.66960144, -6.62294245, -6.82878971, + -8.6383152 , -11.20019722, -13.83398628, -15.95139503, + -16.18096733, -13.58062267, -9.26843071, -5.34649038, + -3.01010084, -2.30953455, -2.73682952, -3.72112942, + -4.85171413, -5.63053226, -5.48626232, -4.49401283, + -3.53216696, -3.34821796, -4.07400894, -5.87675714, + -9.11268425, -12.98700237, -15.06296921, -13.71571922, + -10.23535728, -7.01303005, -5.23288727, -5.71091986, + -9.24852943, -14.06335735, -15.846241 , -12.78800964, + -7.8465519 , -4.56293297, -3.54999399, -3.67789125, + -4.10172844, -4.78980875, -6.20238352, -8.17891598, + -9.2803278 , -8.36780167, -6.3059268 , -4.85605574, + -4.54975414, -4.52917624, -3.99160147, -3.1971693 , + -2.93472862, -3.47230864, -4.7322526 , -6.80173016, + -9.08601665, -10.00928402, -8.87677383, -6.88120317, + -5.61007977, -5.6351161 , -6.41880989, -6.8738699 ], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'radiusQ1', + 'botFullScale': 0.0, + 'creationDate': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 0, + 'fileName': 0, + 'formula': 0, + 'fsValid': 0, + 'hsA': 1.0, + 'hsB': 0.0, + 'kindBits': '\x00', + 'modDate': 2845473634, + 'next': 0, + 'npnts': 64, + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'wUnused': array(['', ''], + dtype='|S1'), + 'waveNoteH': 0, + 'whVersion': 0, + 'xUnits': array(['', '', '', ''], + dtype='|S1')}}} +record 38: +{'version': 5, + 'wave': {'bin_header': {'checksum': -5745, + 'dataEUnitsSize': 0, + 'dimEUnitsSize': array([0, 0, 0, 0]), + 'dimLabelsSize': array([0, 0, 0, 0]), + 'formulaSize': 78, + 'noteSize': 0, + 'optionsSize1': 0, + 'optionsSize2': 0, + 'sIndicesSize': 0, + 'wfmSize': 576}, + 'data_units': '', + 'dimension_units': '', + 'formula': ' PolarRadiusFunction(radiusQ1,1,-40) * cos(PolarAngleFunction(angleQ1,2,2,2))', + 'labels': [[], [], [], []], + 'note': '', + 'sIndices': array([], dtype=float64), + 'wData': array([ 30.58058929, 31.08536911, 31.93481636, 31.57315445, + 29.68683434, 27.10366058, 24.47453499, 22.3495121 , + 21.98692894, 24.21500397, 27.95923996, 31.28394508, + 33.12408066, 33.46794128, 32.79909515, 31.64211464, + 30.36601639, 29.40137291, 29.22361755, 29.74564171, + 30.21624565, 30.02338219, 29.0822773 , 27.28613091, + 24.38687515, 21.04944038, 19.16931915, 19.92274094, + 22.23493385, 24.27418709, 25.1893177 , 24.44671249, + 21.56310272, 17.87704659, 16.35500908, 18.09041786, + 20.97328949, 22.66550255, 22.84443283, 22.29068756, + 21.55643272, 20.67234993, 19.38551521, 17.81604385, + 16.77393341, 16.8293457 , 17.4496479 , 17.6982975 , + 17.34101677, 16.83446693, 16.56042671, 16.38027191, + 15.94310474, 15.16159916, 14.10328865, 12.76812935, + 11.41363049, 10.60795975, 10.52314186, 10.67826462, + 10.5454855 , 9.99268055, 9.22939587, 8.5736742 ], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'W_plrX6', + 'botFullScale': 0.0, + 'creationDate': 0, + 'dFolder': 7848580, + 'dLock': 0, + 'dataEUnits': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 30, + 'dimEUnits': array([0, 0, 0, 0]), + 'dimLabels': array([0, 0, 0, 0]), + 'dimUnits': array([['', '', '', ''], + ['', '', '', ''], + ['', '', '', ''], + ['', '', '', '']], + dtype='|S1'), + 'fileName': 0, + 'formula': 8052116, + 'fsValid': 0, + 'kindBits': '\x00', + 'modDate': 2985072242, + 'nDim': array([64, 0, 0, 0]), + 'next': 8324392, + 'npnts': 64, + 'sIndices': 0, + 'sfA': array([ 1., 1., 1., 1.]), + 'sfB': array([ 0., 0., 0., 0.]), + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'waveNoteH': 0, + 'whUnused': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 'whVersion': 1, + 'whpad1': array(['', '', '', '', '', ''], + dtype='|S1'), + 'whpad2': 0, + 'whpad3': 0, + 'whpad4': 0}}} +record 39: +{'version': 5, + 'wave': {'bin_header': {'checksum': -16604, + 'dataEUnitsSize': 0, + 'dimEUnitsSize': array([0, 0, 0, 0]), + 'dimLabelsSize': array([0, 0, 0, 0]), + 'formulaSize': 78, + 'noteSize': 78, + 'optionsSize1': 0, + 'optionsSize2': 0, + 'sIndicesSize': 0, + 'wfmSize': 576}, + 'data_units': '', + 'dimension_units': '', + 'formula': ' PolarRadiusFunction(radiusQ1,1,-40) * sin(PolarAngleFunction(angleQ1,2,2,2))', + 'labels': [[], [], [], []], + 'note': 'shadowX=W_plrX6,appendRadius=radiusQ1,appendAngleData=angleQ1,angleDataUnits=2', + 'sIndices': array([], dtype=float64), + 'wData': array([ 8.19404411, 8.88563347, 9.70543861, 10.17177773, + 10.11173058, 9.73756695, 9.25513077, 8.8788929 , + 9.16085339, 10.56489944, 12.75579453, 14.90572262, + 16.46352959, 17.33401871, 17.68511391, 17.74635315, + 17.70048141, 17.79942513, 18.36241531, 19.38741684, + 20.41767311, 21.02259827, 21.09260368, 20.4905529 , + 18.95538521, 16.9299469 , 15.94969368, 17.14490509, + 19.78741264, 22.33615875, 23.96352196, 24.04369545, + 21.92454147, 18.79150391, 17.77407646, 20.32803917, + 24.37140465, 27.24079132, 28.40307808, 28.67787933, + 28.70550728, 28.50283432, 27.68538666, 26.36607552, + 25.73583984, 26.78374672, 28.8236084 , 30.36226463, + 30.91939545, 31.22146797, 31.97431755, 32.95656204, + 33.4611969 , 33.23248672, 32.3250885 , 30.64473915, + 28.72983551, 28.05199242, 29.29024887, 31.3501091 , + 32.7331543 , 32.87995529, 32.28799438, 31.99738503], dtype=float32), + 'wave_header': {'aModified': 0, + 'bname': 'W_plrY6', + 'botFullScale': 0.0, + 'creationDate': 0, + 'dFolder': 7848580, + 'dLock': 0, + 'dataEUnits': 0, + 'dataUnits': array(['', '', '', ''], + dtype='|S1'), + 'depID': 32, + 'dimEUnits': array([0, 0, 0, 0]), + 'dimLabels': array([0, 0, 0, 0]), + 'dimUnits': array([['', '', '', ''], + ['', '', '', ''], + ['', '', '', ''], + ['', '', '', '']], + dtype='|S1'), + 'fileName': 0, + 'formula': 7995612, + 'fsValid': 0, + 'kindBits': '\x00', + 'modDate': 2985072242, + 'nDim': array([64, 0, 0, 0]), + 'next': 0, + 'npnts': 64, + 'sIndices': 0, + 'sfA': array([ 1., 1., 1., 1.]), + 'sfB': array([ 0., 0., 0., 0.]), + 'srcFldr': 0, + 'swModified': 0, + 'topFullScale': 0.0, + 'type': 2, + 'useBits': '\x00', + 'wModified': 0, + 'waveNoteH': 7998208, + 'whUnused': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 'whVersion': 1, + 'whpad1': array(['', '', '', '', '', ''], + dtype='|S1'), + 'whpad2': 0, + 'whpad3': 0, + 'whpad4': 0}}} +record 40: +'Packages' +record 41: +'WMDataBase' +record 42: +{'variables': {'sysVars': {'K0': 0.0, + 'K1': 0.0, + 'K10': 0.0, + 'K11': 0.0, + 'K12': 0.0, + 'K13': 0.0, + 'K14': 0.0, + 'K15': 0.0, + 'K16': 0.0, + 'K17': 0.0, + 'K18': 0.0, + 'K19': 0.0, + 'K2': 0.0, + 'K20': 128.0, + 'K3': 0.0, + 'K4': 0.0, + 'K5': 0.0, + 'K6': 0.0, + 'K7': 0.0, + 'K8': 0.0, + 'K9': 0.0}, + 'userStrs': {'u_dataBase': ';PolarGraph0:,...,useCircles=2,maxArcLine=6;', + 'u_dbBadStringChars': ',;=:', + 'u_dbCurrBag': 'PolarGraph1', + 'u_dbCurrContents': ',appendRadius=radiusQ1,...,useCircles=2,maxArcLine=6;', + 'u_dbReplaceBadChars': '\xa9\xae\x99\x9f', + 'u_str': '2'}, + 'userVars': {}, + 'var_header': {'numSysVars': 21, + 'numUserStrs': 6, + 'numUserVars': 0}}, + 'version': 1} +record 43: +'' +record 44: +'PolarGraphs' +record 45: +{'variables': {'sysVars': {'K0': 0.0, + 'K1': 0.0, + 'K10': 0.0, + 'K11': 0.0, + 'K12': 0.0, + 'K13': 0.0, + 'K14': 0.0, + 'K15': 0.0, + 'K16': 0.0, + 'K17': 0.0, + 'K18': 0.0, + 'K19': 0.0, + 'K2': 0.0, + 'K20': 128.0, + 'K3': 0.0, + 'K4': 0.0, + 'K5': 0.0, + 'K6': 0.0, + 'K7': 0.0, + 'K8': 0.0, + 'K9': 0.0}, + 'userStrs': {'u_colorList': 'black;blue;green;cyan;red;magenta;yellow;white;special', + 'u_debugStr': 'Turn Debugging On', + 'u_polAngleAxesWherePop': 'Off;Radius Start;Radius End;Radius Start and End;All Major Radii;At Listed Radii', + 'u_polAngleUnitsPop': 'deg;rad', + 'u_polLineStylePop': 'solid;dash 1;dash 2;dash 3;dash 4;dash 5;dash 6;dash 7;dash 8;dash 9;dash 10;dash 11;dash 12;dash 13;dash 14;dash 15;dash 16;dash 17;', + 'u_polOffOn': 'Off;On', + 'u_polRadAxesWherePop': ' Off; Angle Start; Angle Middle; Angle End; Angle Start and End; 0; 90; 180; -90; 0, 90; 90, 180; -180, -90; -90, 0; 0, 180; 90, -90; 0, 90, 180, -90; All Major Angles; At Listed Angles', + 'u_polRotPop': ' -90; 0; +90; +180', + 'u_popup': '', + 'u_prompt': ''}, + 'userVars': {'V_bottom': 232.0, + 'V_left': 1.0, + 'V_max': 2.4158518093414401, + 'V_min': -2.1848498883412, + 'V_right': 232.0, + 'V_top': 1.0, + 'u_UniqWaveNdx': 8.0, + 'u_UniqWinNdx': 3.0, + 'u_angle0': 0.0, + 'u_angleRange': 6.2831853071795862, + 'u_debug': 0.0, + 'u_majorDelta': 0.0, + 'u_numPlaces': 0.0, + 'u_polAngle0': 0.26179938779914941, + 'u_polAngleRange': 1.0471975511965976, + 'u_polInnerRadius': -20.0, + 'u_polMajorAngleInc': 0.26179938779914941, + 'u_polMajorRadiusInc': 10.0, + 'u_polMinorAngleTicks': 3.0, + 'u_polMinorRadiusTicks': 1.0, + 'u_polOuterRadius': 0.0, + 'u_segsPerMinorArc': 3.0, + 'u_tickDelta': 0.0, + 'u_var': 0.0, + 'u_x1': 11.450159535018935, + 'u_x2': 12.079591517721363, + 'u_y1': 42.732577139459856, + 'u_y2': 45.081649278814126}, + 'var_header': {'numSysVars': 21, + 'numUserStrs': 10, + 'numUserVars': 28}}, + 'version': 1} +record 46: +'' +record 47: +'' +record 48: +'| Platform=Windows95, IGORVersion=3.130\n\n\n\nMoveWindow/P 5.25,40.25,504.75,335\n...hook=PolarWindowHook\nEndMacro\n' +record 49: +'' +record 50: +'#include version >= 3.0\n' + +filesystem: +{'root': {'K0': 0.0, + 'K1': 0.0, + 'K10': 0.0, + 'K11': 0.0, + 'K12': 0.0, + 'K13': 0.0, + 'K14': 0.0, + 'K15': 0.0, + 'K16': 0.0, + 'K17': 0.0, + 'K18': 0.0, + 'K19': 0.0, + 'K2': 0.0, + 'K20': 128.0, + 'K3': 0.0, + 'K4': 0.0, + 'K5': 0.0, + 'K6': 0.0, + 'K7': 0.0, + 'K8': 0.0, + 'K9': 0.0, + 'Packages': {'PolarGraphs': {'V_bottom': 232.0, + 'V_left': 1.0, + 'V_max': 2.4158518093414401, + 'V_min': -2.1848498883412, + 'V_right': 232.0, + 'V_top': 1.0, + 'u_UniqWaveNdx': 8.0, + 'u_UniqWinNdx': 3.0, + 'u_angle0': 0.0, + 'u_angleRange': 6.2831853071795862, + 'u_colorList': 'black;blue;green;cyan;red;magenta;yellow;white;special', + 'u_debug': 0.0, + 'u_debugStr': 'Turn Debugging On', + 'u_majorDelta': 0.0, + 'u_numPlaces': 0.0, + 'u_polAngle0': 0.26179938779914941, + 'u_polAngleAxesWherePop': 'Off;Radius Start;Radius End;Radius Start and End;All Major Radii;At Listed Radii', + 'u_polAngleRange': 1.0471975511965976, + 'u_polAngleUnitsPop': 'deg;rad', + 'u_polInnerRadius': -20.0, + 'u_polLineStylePop': 'solid;dash 1;dash 2;dash 3;dash 4;dash 5;dash 6;dash 7;dash 8;dash 9;dash 10;dash 11;dash 12;dash 13;dash 14;dash 15;dash 16;dash 17;', + 'u_polMajorAngleInc': 0.26179938779914941, + 'u_polMajorRadiusInc': 10.0, + 'u_polMinorAngleTicks': 3.0, + 'u_polMinorRadiusTicks': 1.0, + 'u_polOffOn': 'Off;On', + 'u_polOuterRadius': 0.0, + 'u_polRadAxesWherePop': ' Off; Angle Start; Angle Middle; Angle End; Angle Start and End; 0; 90; 180; -90; 0, 90; 90, 180; -180, -90; -90, 0; 0, 180; 90, -90; 0, 90, 180, -90; All Major Angles; At Listed Angles', + 'u_polRotPop': ' -90; 0; +90; +180', + 'u_popup': '', + 'u_prompt': '', + 'u_segsPerMinorArc': 3.0, + 'u_tickDelta': 0.0, + 'u_var': 0.0, + 'u_x1': 11.450159535018935, + 'u_x2': 12.079591517721363, + 'u_y1': 42.732577139459856, + 'u_y2': 45.081649278814126}, + 'WMDataBase': {'u_dataBase': ';PolarGraph0:,appendRadius=radiusData,...,useCircles=2,maxArcLine=6;', + 'u_dbBadStringChars': ',;=:', + 'u_dbCurrBag': 'PolarGraph1', + 'u_dbCurrContents': ',appendRadius=radiusQ1,...,useCircles=2,maxArcLine=6;', + 'u_dbReplaceBadChars': '\xa9\xae\x99\x9f', + 'u_str': '2'}}, + 'W_plrX5': , + 'W_plrX6': , + 'W_plrY5': , + 'W_plrY6': , + 'angleData': , + 'angleQ1': , + 'radiusData': , + 'radiusQ1': }} + +walking filesystem: +walk callback on ([], root, {'K0': 0.0,...}) +walk callback on (['root'], K0, 0.0) +walk callback on (['root'], K1, 0.0) +walk callback on (['root'], K10, 0.0) +... +walk callback on (['root'], K9, 0.0) +walk callback on (['root'], Packages, {'PolarGraphs': ...}) +walk callback on (['root', 'Packages'], PolarGraphs, {...}) +walk callback on (['root', 'Packages', 'PolarGraphs'], V_bottom, 232.0) +... +walk callback on (['root', 'Packages'], WMDataBase, {...}) +... +walk callback on (['root'], radiusQ1, ) +""" + +import os.path +from pprint import pformat + +from igor import LOG +from igor.binarywave import load as loadibw +from igor.packed import load as loadpxp +from igor.packed import walk as _walk +from igor.record.base import TextRecord +from igor.record.folder import FolderStartRecord, FolderEndRecord +from igor.record.variables import VariablesRecord +from igor.record.wave import WaveRecord + + +_this_dir = os.path.dirname(__file__) +_data_dir = os.path.join(_this_dir, 'data') + +def dumpibw(filename): + LOG.info('Testing {}\n'.format(filename)) + path = os.path.join(_data_dir, filename) + data = loadibw(path) + pprint(data) + +def walk_callback(dirpath, key, value): + print('walk callback on ({}, {}, {})'.format( + dirpath, key, pformat(value))) + +def dumppxp(filename, walk=True): + LOG.info('Testing {}\n'.format(filename)) + path = os.path.join(_data_dir, filename) + records,filesystem = loadpxp(path) + for i,record in enumerate(records): + print('record {}:'.format(i)) + if isinstance(record, (FolderStartRecord, FolderEndRecord)): + pprint(record.null_terminated_text) + elif isinstance(record, TextRecord): + pprint(record.text) + elif isinstance(record, VariablesRecord): + pprint(record.variables) + elif isinstance(record, WaveRecord): + pprint(record.wave) + else: + pprint(record) + print('\nfilesystem:') + pprint(filesystem) + if walk: + print('\nwalking filesystem:') + _walk(filesystem, walk_callback) + +def pprint(data): + lines = pformat(data).splitlines() + print('\n'.join([line.rstrip() for line in lines]))