diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/FCPCodebase.iml b/.idea/FCPCodebase.iml new file mode 100644 index 0000000..8b8c395 --- /dev/null +++ b/.idea/FCPCodebase.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..d42459f --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a33389a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..cb91ae6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -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/One_vs_One.py b/One_vs_One.py new file mode 100644 index 0000000..dca9fd8 --- /dev/null +++ b/One_vs_One.py @@ -0,0 +1,36 @@ +from agent.Base_Agent import Base_Agent as Agent +from math_ops.Math_Ops import Math_Ops as M +from scripts.commons.Script import Script + +script = Script() +a = script.args + +# Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name +p1 = Agent(a.i, a.p, a.m, a.u, a.r, a.t) +p2 = Agent(a.i, a.p, a.m, a.u, a.r, "Opponent") +players = [p1,p2] + +p1.scom.unofficial_beam((-3,0,p1.world.robot.beam_height), 0) +p2.scom.unofficial_beam((-3,0,p2.world.robot.beam_height), 0) + +getting_up = [False]*2 + +while True: + for i in range(len(players)): + p = players[i] + w = p.world + + player_2d = w.robot.loc_head_position[:2] + ball_2d = w.ball_abs_pos[:2] + goal_dir = M.vector_angle( (15,0)-player_2d ) # Goal direction + + if p.behavior.is_ready("Get_Up") or getting_up[i]: + getting_up[i] = not p.behavior.execute("Get_Up") # True on completion + else: + p.behavior.execute("Basic_Kick", goal_dir) + + p.scom.commit_and_send( w.robot.get_command() ) + + # all players must commit and send before the server updates + p1.scom.receive() + p2.scom.receive() \ No newline at end of file diff --git a/Run_Full_Team.py b/Run_Full_Team.py new file mode 100644 index 0000000..74587ef --- /dev/null +++ b/Run_Full_Team.py @@ -0,0 +1,13 @@ +from scripts.commons.Script import Script +script = Script() # Initialize: load config file, parse arguments, build cpp modules +a = script.args + +from agent.Agent import Agent + +# Args: Server IP, Agent Port, Monitor Port, Uniform No., Team name, Enable Log, Enable Draw +team_args = ((a.i, a.p, a.m, u, a.t, True, True) for u in range(1,12)) +script.batch_create(Agent,team_args) + +while True: + script.batch_execute_agent() + script.batch_receive() diff --git a/Run_One_vs_One.py b/Run_One_vs_One.py new file mode 100644 index 0000000..98aed42 --- /dev/null +++ b/Run_One_vs_One.py @@ -0,0 +1,14 @@ +from scripts.commons.Script import Script +script = Script() # Initialize: load config file, parse arguments, build cpp modules +a = script.args + +from agent.Agent import Agent + +# Args: Server IP, Agent Port, Monitor Port, Uniform No., Team name, Enable Log, Enable Draw +script.batch_create(Agent, ((a.i, a.p, a.m, a.u, a.t, True, True),)) #one player for home team +script.batch_create(Agent, ((a.i, a.p, a.m, a.u, "Opponent", True, True),)) #one player for away team + + +while True: + script.batch_execute_agent() + script.batch_receive() diff --git a/Run_Player.py b/Run_Player.py new file mode 100644 index 0000000..46419b8 --- /dev/null +++ b/Run_Player.py @@ -0,0 +1,18 @@ +from scripts.commons.Script import Script +script = Script(cpp_builder_unum=1) # Initialize: load config file, parse arguments, build cpp modules +a = script.args + +if a.P: # penalty shootout + from agent.Agent_Penalty import Agent +else: # normal agent + from agent.Agent import Agent + +# Args: Server IP, Agent Port, Monitor Port, Uniform No., Team name, Enable Log, Enable Draw, Wait for Server, is magmaFatProxy +if a.D: # debug mode + player = Agent(a.i, a.p, a.m, a.u, a.t, True, True, False, a.F) +else: + player = Agent(a.i, a.p, None, a.u, a.t, False, False, False, a.F) + +while True: + player.think_and_send() + player.scom.receive() diff --git a/Run_Utils.py b/Run_Utils.py new file mode 100644 index 0000000..66f7e21 --- /dev/null +++ b/Run_Utils.py @@ -0,0 +1,96 @@ +def main(): + + from scripts.commons.Script import Script + script = Script() #Initialize: load config file, parse arguments, build cpp modules (warns the user about inconsistencies before choosing a test script) + + # Allows using local version of StableBaselines3 (e.g. https://github.com/m-abr/Adaptive-Symmetry-Learning) + # place the 'stable-baselines3' folder in the parent directory of this project + import sys + from os.path import dirname, abspath, join + sys.path.insert( 0, join( dirname(dirname( abspath(__file__) )), "stable-baselines3") ) + + from scripts.commons.UI import UI + from os.path import isfile, join, realpath, dirname + from os import listdir, getcwd + from importlib import import_module + + _cwd = realpath( join(getcwd(), dirname(__file__))) + gyms_path = _cwd + "/scripts/gyms/" + utils_path = _cwd + "/scripts/utils/" + exclusions = ["__init__.py"] + + utils = sorted([f[:-3] for f in listdir(utils_path) if isfile(join(utils_path, f)) and f.endswith(".py") and f not in exclusions], key=lambda x: (x != "Server", x)) + gyms = sorted([f[:-3] for f in listdir(gyms_path ) if isfile(join(gyms_path , f)) and f.endswith(".py") and f not in exclusions]) + + while True: + _, col_idx, col = UI.print_table( [utils, gyms], ["Demos & Tests & Utils","Gyms"], cols_per_title=[2,1], numbering=[True]*2, prompt='Choose script (ctrl+c to exit): ' ) + + is_gym = False + if col == 0: + chosen = ("scripts.utils." , utils[col_idx]) + elif col == 1: + chosen = ("scripts.gyms." , gyms[col_idx]) + is_gym = True + + cls_name = chosen[1] + mod = import_module(chosen[0]+chosen[1]) + + ''' + An imported script should not automatically execute the main code because: + - Multiprocessing methods, such as 'forkserver' and 'spawn', will execute the main code in every child + - The script can only be called once unless it is reloaded + ''' + if not is_gym: + ''' + Utils receive a script support object with user-defined arguments and useful methods + Each util must implement an execute() method, which may or may not return + ''' + from world.commons.Draw import Draw + from agent.Base_Agent import Base_Agent + obj = getattr(mod,cls_name)(script) + try: + obj.execute() # Util may return normally or through KeyboardInterrupt + except KeyboardInterrupt: + print("\nctrl+c pressed, returning...\n") + Draw.clear_all() # clear all drawings + Base_Agent.terminate_all() # close all server sockets + monitor socket + script.players = [] # clear list of players created through batch commands + del obj + + else: + ''' + Gyms must implement a class Train() which is initialized with user-defined arguments and implements: + train() - method to run the optimization and save a new model + test(folder_dir, folder_name, model_file) - method to load an existing model and test it + ''' + from scripts.commons.Train_Base import Train_Base + + print("\nBefore using GYMS, make sure all server parameters are set correctly") + print("(sync mode should be 'On', real time should be 'Off', cheats should be 'On', ...)") + print("To change these parameters go to the previous menu, and select Server\n") + print("Also, GYMS start their own servers, so don't run any server manually") + + while True: + try: + idx = UI.print_table([["Train","Test","Retrain"]], numbering=[True], prompt='Choose option (ctrl+c to return): ')[0] + except KeyboardInterrupt: + print() + break + + if idx == 0: + mod.Train(script).train(dict()) + else: + model_info = Train_Base.prompt_user_for_model() + if model_info is not None and idx == 1: + mod.Train(script).test(model_info) + elif model_info is not None: + mod.Train(script).train(model_info) + + +# allow child processes to bypass this file +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nctrl+c pressed, exiting...") + exit() \ No newline at end of file diff --git a/T_vs_T.py b/T_vs_T.py new file mode 100644 index 0000000..2d6c1fd --- /dev/null +++ b/T_vs_T.py @@ -0,0 +1,35 @@ +from agent.Base_Agent import Base_Agent as Agent +from math_ops.Math_Ops import Math_Ops as M +from scripts.commons.Script import Script + +script = Script() +a = script.args + +# Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name +# This could be done in 1 line, but calling batch_create twice enhances readability + +script.batch_create(Agent, ((a.i,a.p,a.m,u+1,a.r,"Home") for u in range(3)) ) +script.batch_create(Agent, ((a.i,a.p,a.m,u+1,a.r,"Away") for u in range(3)) ) + +players = script.players +p_num = len(players) # total number of players +script.batch_unofficial_beam( (-3, 4.5-abs(3*i-7.5), 0.5, 0) for i in range(p_num) ) + +getting_up = [False]*p_num + +while True: + for i in range(p_num): + p = players[i] + w = p.world + + player_2d = w.robot.loc_head_position[:2] + ball_2d = w.ball_abs_pos[:2] + goal_dir = M.vector_angle( (15,0)-player_2d ) # Goal direction + + if p.behavior.is_ready("Get_Up") or getting_up[i]: + getting_up[i] = not p.behavior.execute("Get_Up") # True on completion + else: + p.behavior.execute("Basic_Kick", goal_dir) + + script.batch_commit_and_send() + script.batch_receive() \ No newline at end of file diff --git a/agent/Agent.py b/agent/Agent.py new file mode 100644 index 0000000..c5500e2 --- /dev/null +++ b/agent/Agent.py @@ -0,0 +1,271 @@ +from agent.Base_Agent import Base_Agent +from math_ops.Math_Ops import Math_Ops as M +import math +import numpy as np + + +class Agent(Base_Agent): + def __init__(self, host:str, agent_port:int, monitor_port:int, unum:int, + team_name:str, enable_log, enable_draw, wait_for_server=True, is_fat_proxy=False) -> None: + + # define robot type + robot_type = (0,1,1,1,2,3,3,3,4,4,4)[unum-1] + + # Initialize base agent + # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name, Enable Log, Enable Draw, play mode correction, Wait for Server, Hear Callback + super().__init__(host, agent_port, monitor_port, unum, robot_type, team_name, enable_log, enable_draw, True, wait_for_server, None) + + self.enable_draw = enable_draw + self.state = 0 # 0-Normal, 1-Getting up, 2-Kicking + self.kick_direction = 0 + self.kick_distance = 0 + self.fat_proxy_cmd = "" if is_fat_proxy else None + self.fat_proxy_walk = np.zeros(3) # filtered walk parameters for fat proxy + + self.init_pos = ([-14,0],[-9,-5],[-9,0],[-9,5],[-5,-5],[-5,0],[-5,5],[-1,-6],[-1,-2.5],[-1,2.5],[-1,6])[unum-1] # initial formation + + + def beam(self, avoid_center_circle=False): + r = self.world.robot + pos = self.init_pos[:] # copy position list + self.state = 0 + + # Avoid center circle by moving the player back + if avoid_center_circle and np.linalg.norm(self.init_pos) < 2.5: + pos[0] = -2.3 + + if np.linalg.norm(pos - r.loc_head_position[:2]) > 0.1 or self.behavior.is_ready("Get_Up"): + self.scom.commit_beam(pos, M.vector_angle((-pos[0],-pos[1]))) # beam to initial position, face coordinate (0,0) + else: + if self.fat_proxy_cmd is None: # normal behavior + self.behavior.execute("Zero_Bent_Knees_Auto_Head") + else: # fat proxy behavior + self.fat_proxy_cmd += "(proxy dash 0 0 0)" + self.fat_proxy_walk = np.zeros(3) # reset fat proxy walk + + + def move(self, target_2d=(0,0), orientation=None, is_orientation_absolute=True, + avoid_obstacles=True, priority_unums=[], is_aggressive=False, timeout=3000): + ''' + Walk to target position + + Parameters + ---------- + target_2d : array_like + 2D target in absolute coordinates + orientation : float + absolute or relative orientation of torso, in degrees + set to None to go towards the target (is_orientation_absolute is ignored) + is_orientation_absolute : bool + True if orientation is relative to the field, False if relative to the robot's torso + avoid_obstacles : bool + True to avoid obstacles using path planning (maybe reduce timeout arg if this function is called multiple times per simulation cycle) + priority_unums : list + list of teammates to avoid (since their role is more important) + is_aggressive : bool + if True, safety margins are reduced for opponents + timeout : float + restrict path planning to a maximum duration (in microseconds) + ''' + r = self.world.robot + + if self.fat_proxy_cmd is not None: # fat proxy behavior + self.fat_proxy_move(target_2d, orientation, is_orientation_absolute) # ignore obstacles + return + + if avoid_obstacles: + target_2d, _, distance_to_final_target = self.path_manager.get_path_to_target( + target_2d, priority_unums=priority_unums, is_aggressive=is_aggressive, timeout=timeout) + else: + distance_to_final_target = np.linalg.norm(target_2d - r.loc_head_position[:2]) + + self.behavior.execute("Walk", target_2d, True, orientation, is_orientation_absolute, distance_to_final_target) # Args: target, is_target_abs, ori, is_ori_abs, distance + + + + + + def kick(self, kick_direction=None, kick_distance=None, abort=False, enable_pass_command=False): + ''' + Walk to ball and kick + + Parameters + ---------- + kick_direction : float + kick direction, in degrees, relative to the field + kick_distance : float + kick distance in meters + abort : bool + True to abort. + The method returns True upon successful abortion, which is immediate while the robot is aligning itself. + However, if the abortion is requested during the kick, it is delayed until the kick is completed. + avoid_pass_command : bool + When False, the pass command will be used when at least one opponent is near the ball + + Returns + ------- + finished : bool + Returns True if the behavior finished or was successfully aborted. + ''' + + if self.min_opponent_ball_dist < 1.45 and enable_pass_command: + self.scom.commit_pass_command() + + self.kick_direction = self.kick_direction if kick_direction is None else kick_direction + self.kick_distance = self.kick_distance if kick_distance is None else kick_distance + + if self.fat_proxy_cmd is None: # normal behavior + return self.behavior.execute("Basic_Kick", self.kick_direction, abort) # Basic_Kick has no kick distance control + else: # fat proxy behavior + return self.fat_proxy_kick() + + + + + def think_and_send(self): + w = self.world + r = self.world.robot + my_head_pos_2d = r.loc_head_position[:2] + my_ori = r.imu_torso_orientation + ball_2d = w.ball_abs_pos[:2] + ball_vec = ball_2d - my_head_pos_2d + ball_dir = M.vector_angle(ball_vec) + ball_dist = np.linalg.norm(ball_vec) + ball_sq_dist = ball_dist * ball_dist # for faster comparisons + ball_speed = np.linalg.norm(w.get_ball_abs_vel(6)[:2]) + behavior = self.behavior + goal_dir = M.target_abs_angle(ball_2d,(15.05,0)) + path_draw_options = self.path_manager.draw_options + PM = w.play_mode + PM_GROUP = w.play_mode_group + + #--------------------------------------- 1. Preprocessing + + slow_ball_pos = w.get_predicted_ball_pos(0.5) # predicted future 2D ball position when ball speed <= 0.5 m/s + + # list of squared distances between teammates (including self) and slow ball (sq distance is set to 1000 in some conditions) + teammates_ball_sq_dist = [np.sum((p.state_abs_pos[:2] - slow_ball_pos) ** 2) # squared distance between teammate and ball + if p.state_last_update != 0 and (w.time_local_ms - p.state_last_update <= 360 or p.is_self) and not p.state_fallen + else 1000 # force large distance if teammate does not exist, or its state info is not recent (360 ms), or it has fallen + for p in w.teammates ] + + # list of squared distances between opponents and slow ball (sq distance is set to 1000 in some conditions) + opponents_ball_sq_dist = [np.sum((p.state_abs_pos[:2] - slow_ball_pos) ** 2) # squared distance between teammate and ball + if p.state_last_update != 0 and w.time_local_ms - p.state_last_update <= 360 and not p.state_fallen + else 1000 # force large distance if opponent does not exist, or its state info is not recent (360 ms), or it has fallen + for p in w.opponents ] + + min_teammate_ball_sq_dist = min(teammates_ball_sq_dist) + self.min_teammate_ball_dist = math.sqrt(min_teammate_ball_sq_dist) # distance between ball and closest teammate + self.min_opponent_ball_dist = math.sqrt(min(opponents_ball_sq_dist)) # distance between ball and closest opponent + + active_player_unum = teammates_ball_sq_dist.index(min_teammate_ball_sq_dist) + 1 + + + #--------------------------------------- 2. Decide action + + + + if PM == w.M_GAME_OVER: + pass + elif PM_GROUP == w.MG_ACTIVE_BEAM: + self.beam() + elif PM_GROUP == w.MG_PASSIVE_BEAM: + self.beam(True) # avoid center circle + elif self.state == 1 or (behavior.is_ready("Get_Up") and self.fat_proxy_cmd is None): + self.state = 0 if behavior.execute("Get_Up") else 1 # return to normal state if get up behavior has finished + elif PM == w.M_OUR_KICKOFF: + if r.unum == 9: + self.kick(120,3) # no need to change the state when PM is not Play On + else: + self.move(self.init_pos, orientation=ball_dir) # walk in place + elif PM == w.M_THEIR_KICKOFF: + self.move(self.init_pos, orientation=ball_dir) # walk in place + elif active_player_unum != r.unum: # I am not the active player + if r.unum == 1: # I am the goalkeeper + self.move(self.init_pos, orientation=ball_dir) # walk in place + else: + # compute basic formation position based on ball position + new_x = max(0.5,(ball_2d[0]+15)/15) * (self.init_pos[0]+15) - 15 + if self.min_teammate_ball_dist < self.min_opponent_ball_dist: + new_x = min(new_x + 3.5, 13) # advance if team has possession + self.move((new_x,self.init_pos[1]), orientation=ball_dir, priority_unums=[active_player_unum]) + + else: # I am the active player + path_draw_options(enable_obstacles=True, enable_path=True, use_team_drawing_channel=True) # enable path drawings for active player (ignored if self.enable_draw is False) + enable_pass_command = (PM == w.M_PLAY_ON and ball_2d[0]<6) + + if r.unum == 1 and PM_GROUP == w.MG_THEIR_KICK: # goalkeeper during their kick + self.move(self.init_pos, orientation=ball_dir) # walk in place + if PM == w.M_OUR_CORNER_KICK: + self.kick( -np.sign(ball_2d[1])*95, 5.5) # kick the ball into the space in front of the opponent's goal + # no need to change the state when PM is not Play On + elif self.min_opponent_ball_dist + 0.5 < self.min_teammate_ball_dist: # defend if opponent is considerably closer to the ball + if self.state == 2: # commit to kick while aborting + self.state = 0 if self.kick(abort=True) else 2 + else: # move towards ball, but position myself between ball and our goal + self.move(slow_ball_pos + M.normalize_vec((-16,0) - slow_ball_pos) * 0.2, is_aggressive=True) + else: + self.state = 0 if self.kick(goal_dir,9,False,enable_pass_command) else 2 + + path_draw_options(enable_obstacles=False, enable_path=False) # disable path drawings + + #--------------------------------------- 3. Broadcast + self.radio.broadcast() + + #--------------------------------------- 4. Send to server + if self.fat_proxy_cmd is None: # normal behavior + self.scom.commit_and_send( r.get_command() ) + else: # fat proxy behavior + self.scom.commit_and_send( self.fat_proxy_cmd.encode() ) + self.fat_proxy_cmd = "" + + #---------------------- annotations for debugging + if self.enable_draw: + d = w.draw + if active_player_unum == r.unum: + d.point(slow_ball_pos, 3, d.Color.pink, "status", False) # predicted future 2D ball position when ball speed <= 0.5 m/s + d.point(w.ball_2d_pred_pos[-1], 5, d.Color.pink, "status", False) # last ball prediction + d.annotation((*my_head_pos_2d, 0.6), "I've got it!" , d.Color.yellow, "status") + else: + d.clear("status") + + + + + #--------------------------------------- Fat proxy auxiliary methods + + + def fat_proxy_kick(self): + w = self.world + r = self.world.robot + ball_2d = w.ball_abs_pos[:2] + my_head_pos_2d = r.loc_head_position[:2] + + if np.linalg.norm(ball_2d - my_head_pos_2d) < 0.25: + # fat proxy kick arguments: power [0,10]; relative horizontal angle [-180,180]; vertical angle [0,70] + self.fat_proxy_cmd += f"(proxy kick 10 {M.normalize_deg( self.kick_direction - r.imu_torso_orientation ):.2f} 20)" + self.fat_proxy_walk = np.zeros(3) # reset fat proxy walk + return True + else: + self.fat_proxy_move(ball_2d-(-0.1,0), None, True) # ignore obstacles + return False + + + def fat_proxy_move(self, target_2d, orientation, is_orientation_absolute): + r = self.world.robot + + target_dist = np.linalg.norm(target_2d - r.loc_head_position[:2]) + target_dir = M.target_rel_angle(r.loc_head_position[:2], r.imu_torso_orientation, target_2d) + + if target_dist > 0.1 and abs(target_dir) < 8: + self.fat_proxy_cmd += (f"(proxy dash {100} {0} {0})") + return + + if target_dist < 0.1: + if is_orientation_absolute: + orientation = M.normalize_deg( orientation - r.imu_torso_orientation ) + target_dir = np.clip(orientation, -60, 60) + self.fat_proxy_cmd += (f"(proxy dash {0} {0} {target_dir:.1f})") + else: + self.fat_proxy_cmd += (f"(proxy dash {20} {0} {target_dir:.1f})") \ No newline at end of file diff --git a/agent/Agent_Penalty.py b/agent/Agent_Penalty.py new file mode 100644 index 0000000..b90b3e3 --- /dev/null +++ b/agent/Agent_Penalty.py @@ -0,0 +1,88 @@ +from agent.Base_Agent import Base_Agent +from math_ops.Math_Ops import Math_Ops as M +import numpy as np +import random + + +class Agent(Base_Agent): + def __init__(self, host:str, agent_port:int, monitor_port:int, unum:int, + team_name:str, enable_log, enable_draw, wait_for_server=True, is_fat_proxy=False) -> None: + + # define robot type + robot_type = 0 if unum == 1 else 4 # assume the goalkeeper uses uniform number 1 and the kicker uses any other number + + # Initialize base agent + # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name, Enable Log, Enable Draw, play mode correction, Wait for Server, Hear Callback + super().__init__(host, agent_port, monitor_port, unum, robot_type, team_name, enable_log, enable_draw, False, wait_for_server, None) + + self.enable_draw = enable_draw + self.state = 0 # 0-Normal, 1-Getting up, 2-Dive Left, 3-Dive Right, 4-Wait + + self.kick_dir = 0 # kick direction + self.reset_kick = True # when True, a new random kick direction is generated + + + def think_and_send(self): + w = self.world + r = self.world.robot + my_head_pos_2d = r.loc_head_position[:2] + my_ori = r.imu_torso_orientation + ball_2d = w.ball_abs_pos[:2] + ball_vec = ball_2d - my_head_pos_2d + ball_dir = M.vector_angle(ball_vec) + ball_dist = np.linalg.norm(ball_vec) + ball_speed = np.linalg.norm(w.get_ball_abs_vel(6)[:2]) + behavior = self.behavior + PM = w.play_mode + + #--------------------------------------- 1. Decide action + + if PM in [w.M_BEFORE_KICKOFF, w.M_THEIR_GOAL, w.M_OUR_GOAL]: # beam to initial position and wait + self.state = 0 + self.reset_kick = True + pos = (-14,0) if r.unum == 1 else (4.9,0) + if np.linalg.norm(pos - r.loc_head_position[:2]) > 0.1 or behavior.is_ready("Get_Up"): + self.scom.commit_beam(pos, 0) # beam to initial position + else: + behavior.execute("Zero_Bent_Knees") # wait + elif self.state == 2: # dive left + self.state = 4 if behavior.execute("Dive_Left") else 2 # change state to wait after skill has finished + elif self.state == 3: # dive right + self.state = 4 if behavior.execute("Dive_Right") else 3 # change state to wait after skill has finished + elif self.state == 4: # wait (after diving or during opposing kick) + pass + elif self.state == 1 or behavior.is_ready("Get_Up"): # if getting up or fallen + self.state = 0 if behavior.execute("Get_Up") else 1 # return to normal state if get up behavior has finished + elif PM == w.M_OUR_KICKOFF and r.unum == 1 or PM == w.M_THEIR_KICKOFF and r.unum != 1: + self.state = 4 # wait until next beam + elif r.unum == 1: # goalkeeper + y_coordinate = np.clip(ball_2d[1], -1.1, 1.1) + behavior.execute("Walk", (-14,y_coordinate), True, 0, True, None) # Args: target, is_target_abs, ori, is_ori_abs, distance + if ball_2d[0] < -10: + self.state = 2 if ball_2d[1] > 0 else 3 # dive to defend + else: # kicker + if PM == w.M_OUR_KICKOFF and ball_2d[0] > 5: # check ball position to make sure I see it + if self.reset_kick: + self.kick_dir = random.choice([-7.5,7.5]) + self.reset_kick = False + behavior.execute("Basic_Kick", self.kick_dir) + else: + behavior.execute("Zero_Bent_Knees") # wait + + #--------------------------------------- 2. Broadcast + self.radio.broadcast() + + #--------------------------------------- 3. Send to server + self.scom.commit_and_send( r.get_command() ) + + #---------------------- annotations for debugging + if self.enable_draw: + d = w.draw + if r.unum == 1: + d.annotation((*my_head_pos_2d, 0.8), "Goalkeeper" , d.Color.yellow, "status") + else: + d.annotation((*my_head_pos_2d, 0.8), "Kicker" , d.Color.yellow, "status") + if PM == w.M_OUR_KICKOFF: # draw arrow to indicate kick direction + d.arrow(ball_2d, ball_2d + 5*M.vector_from_angle(self.kick_dir), 0.4, 3, d.Color.cyan_light, "Target") + + diff --git a/agent/Base_Agent.py b/agent/Base_Agent.py new file mode 100644 index 0000000..650e203 --- /dev/null +++ b/agent/Base_Agent.py @@ -0,0 +1,47 @@ +from abc import abstractmethod +from behaviors.Behavior import Behavior +from communication.Radio import Radio +from communication.Server_Comm import Server_Comm +from communication.World_Parser import World_Parser +from logs.Logger import Logger +from math_ops.Inverse_Kinematics import Inverse_Kinematics +from world.commons.Path_Manager import Path_Manager +from world.World import World + +class Base_Agent(): + all_agents = [] + + def __init__(self, host:str, agent_port:int, monitor_port:int, unum:int, robot_type:int, team_name:str, enable_log:bool=True, + enable_draw:bool=True, apply_play_mode_correction:bool=True, wait_for_server:bool=True, hear_callback=None) -> None: + + self.radio = None # hear_message may be called during Server_Comm instantiation + self.logger = Logger(enable_log, f"{team_name}_{unum}") + self.world = World(robot_type, team_name, unum, apply_play_mode_correction, enable_draw, self.logger, host) + self.world_parser = World_Parser(self.world, self.hear_message if hear_callback is None else hear_callback) + self.scom = Server_Comm(host,agent_port,monitor_port,unum,robot_type,team_name,self.world_parser,self.world,Base_Agent.all_agents,wait_for_server) + self.inv_kinematics = Inverse_Kinematics(self.world.robot) + self.behavior = Behavior(self) + self.path_manager = Path_Manager(self.world) + self.radio = Radio(self.world, self.scom.commit_announcement) + self.behavior.create_behaviors() + Base_Agent.all_agents.append(self) + + @abstractmethod + def think_and_send(self): + pass + + def hear_message(self, msg:bytearray, direction, timestamp:float) -> None: + if direction != "self" and self.radio is not None: + self.radio.receive(msg) + + def terminate(self): + # close shared monitor socket if this is the last agent on this thread + self.scom.close(close_monitor_socket=(len(Base_Agent.all_agents)==1)) + Base_Agent.all_agents.remove(self) + + @staticmethod + def terminate_all(): + for o in Base_Agent.all_agents: + o.scom.close(True) # close shared monitor socket, if it exists + Base_Agent.all_agents = [] + diff --git a/behaviors/Behavior.py b/behaviors/Behavior.py new file mode 100644 index 0000000..acbb4be --- /dev/null +++ b/behaviors/Behavior.py @@ -0,0 +1,183 @@ +import numpy as np + + +class Behavior(): + + def __init__(self, base_agent) -> None: + from agent.Base_Agent import Base_Agent # for type hinting + self.base_agent : Base_Agent = base_agent + self.world = self.base_agent.world + self.state_behavior_name = None + self.state_behavior_init_ms = 0 + self.previous_behavior = None + self.previous_behavior_duration = None + + #Initialize standard behaviors + from behaviors.Poses import Poses + from behaviors.Slot_Engine import Slot_Engine + from behaviors.Head import Head + + self.poses = Poses(self.world) + self.slot_engine = Slot_Engine(self.world) + self.head = Head(self.world) + + + def create_behaviors(self): + ''' + Behaviors dictionary: + creation: key: ( description, auto_head, lambda reset[,a,b,c,..]: self.execute(...), lambda: self.is_ready(...) ) + usage: key: ( description, auto_head, execute_func(reset, *args), is_ready_func ) + ''' + self.behaviors = self.poses.get_behaviors_callbacks() + self.behaviors.update(self.slot_engine.get_behaviors_callbacks()) + self.behaviors.update(self.get_custom_callbacks()) + + + def get_custom_callbacks(self): + ''' + Searching custom behaviors could be implemented automatically + However, for code distribution, loading code dynamically is not ideal (unless we load byte code or some other import solution) + Currently, adding custom behaviors is a manual process: + 1. Add import statement below + 2. Add class to 'classes' list + ''' + + # Declaration of behaviors + from behaviors.custom.Basic_Kick.Basic_Kick import Basic_Kick + from behaviors.custom.Dribble.Dribble import Dribble + from behaviors.custom.Fall.Fall import Fall + from behaviors.custom.Get_Up.Get_Up import Get_Up + from behaviors.custom.Step.Step import Step + from behaviors.custom.Walk.Walk import Walk + classes = [Basic_Kick,Dribble,Fall,Get_Up,Step,Walk] + + '''---- End of manual declarations ----''' + + # Prepare callbacks + self.objects = {cls.__name__ : cls(self.base_agent) for cls in classes} + + return {name: (o.description,o.auto_head, + lambda reset,*args,o=o: o.execute(reset,*args), lambda *args,o=o: o.is_ready(*args)) for name, o in self.objects.items()} + + + def get_custom_behavior_object(self, name): + ''' Get unique object from class "name" ("name" must represent a custom behavior) ''' + assert name in self.objects, f"There is no custom behavior called {name}" + return self.objects[name] + + + def get_all_behaviors(self): + ''' Get name and description of all behaviors ''' + return [ key for key in self.behaviors ], [ val[0] for val in self.behaviors.values() ] + + + def get_current(self): + ''' Get name and duration (in seconds) of current behavior ''' + duration = (self.world.time_local_ms - self.state_behavior_init_ms) / 1000.0 + return self.state_behavior_name, duration + + + def get_previous(self): + ''' Get name and duration (in seconds) of previous behavior ''' + return self.previous_behavior, self.previous_behavior_duration + + + def force_reset(self): + ''' Force reset next executed behavior ''' + self.state_behavior_name = None + + + def execute(self, name, *args) -> bool: + ''' + Execute one step of behavior `name` with arguments `*args` + - Automatically resets behavior on first call + - Call get_current() to get the current behavior (and its duration) + + Returns + ------- + finished : bool + True if behavior has finished + ''' + + assert name in self.behaviors, f"Behavior {name} does not exist!" + + # Check if transitioning from other behavior + reset = bool(self.state_behavior_name != name) + if reset: + if self.state_behavior_name is not None: + self.previous_behavior = self.state_behavior_name # Previous behavior was interrupted (did not finish) + self.previous_behavior_duration = (self.world.time_local_ms - self.state_behavior_init_ms) / 1000.0 + self.state_behavior_name = name + self.state_behavior_init_ms = self.world.time_local_ms + + # Control head orientation if behavior allows it + if self.behaviors[name][1]: + self.head.execute() + + # Execute behavior + if not self.behaviors[name][2](reset,*args): + return False + + # The behavior has finished + self.previous_behavior = self.state_behavior_name # Store current behavior name + self.state_behavior_name = None + return True + + + def execute_sub_behavior(self, name, reset, *args): + ''' + Execute one step of behavior `name` with arguments `*args` + Useful for custom behaviors that call other behaviors + - Behavior reset is performed manually + - Calling get_current() will return the main behavior (not the sub behavior) + - Poses ignore the reset argument + + Returns + ------- + finished : bool + True if behavior has finished + ''' + + assert name in self.behaviors, f"Behavior {name} does not exist!" + + # Control head orientation if behavior allows it + if self.behaviors[name][1]: + self.head.execute() + + # Execute behavior + return self.behaviors[name][2](reset,*args) + + + def execute_to_completion(self, name, *args): + ''' + Execute steps and communicate with server until completion + - Slot behaviors indicate that the behavior has finished when sending the last command (which is promptly sent) + - Poses are finished when the server returns the desired robot state (so the last command is irrelevant) + - For custom behaviors, we assume the same logic, and so, the last command is ignored + + Notes + ----- + - Before exiting, the `Robot.joints_target_speed` array is reset to avoid polluting the next command + - For poses and custom behaviors that indicate a finished behavior on the 1st call, nothing is committed or sent + - Warning: this function may get stuck in an infinite loop if the behavior never ends + ''' + + r = self.world.robot + skip_last = name not in self.slot_engine.behaviors + + while True: + done = self.execute(name, *args) + if done and skip_last: break # Exit here if last command is irrelevant + self.base_agent.scom.commit_and_send( r.get_command() ) + self.base_agent.scom.receive() + if done: break # Exit here if last command is part of the behavior + + # reset to avoid polluting the next command + r.joints_target_speed = np.zeros_like(r.joints_target_speed) + + + def is_ready(self, name, *args) -> bool: + ''' Checks if behavior is ready to start under current game/robot conditions ''' + + assert name in self.behaviors, f"Behavior {name} does not exist!" + return self.behaviors[name][3](*args) \ No newline at end of file diff --git a/behaviors/Head.py b/behaviors/Head.py new file mode 100644 index 0000000..9c3383c --- /dev/null +++ b/behaviors/Head.py @@ -0,0 +1,105 @@ +from math_ops.Math_Ops import Math_Ops as M +from world.World import World +import numpy as np + +class Head(): + FIELD_FLAGS = World.FLAGS_CORNERS_POS + World.FLAGS_POSTS_POS + HEAD_PITCH = -35 + + def __init__(self, world : World) -> None: + self.world = world + self.look_left = True + self.state = 0 + + + def execute(self): + ''' + Try to compute best head orientation if possible, otherwise look around + + state: + 0 - Adjust position - ball is in FOV and robot can self-locate + 1..TIMEOUT-1 - Guided search - attempt to use recent visual/radio information to guide the search + TIMEOUT - Random search - look around (default mode after guided search fails by timeout) + ''' + TIMEOUT = 30 + w = self.world + r = w.robot + can_self_locate = r.loc_last_update > w.time_local_ms - w.VISUALSTEP_MS + + #--------------------------------------- A. Ball is in FOV and robot can self-locate + + if w.ball_last_seen > w.time_local_ms - w.VISUALSTEP_MS: # ball is in FOV + if can_self_locate: + best_dir = self.compute_best_direction(can_self_locate, use_ball_from_vision=True) + self.state = 0 + elif self.state < TIMEOUT: + #--------------------------------------- B. Ball is in FOV but robot cannot currently self-locate + best_dir = self.compute_best_direction(can_self_locate, use_ball_from_vision=True) + self.state += 1 # change to guided search and increment time + elif self.state < TIMEOUT: + #--------------------------------------- C. Ball is not in FOV + best_dir = self.compute_best_direction(can_self_locate) + self.state += 1 # change to guided search and increment time + + + if self.state == TIMEOUT: # Random search + + if w.ball_last_seen > w.time_local_ms - w.VISUALSTEP_MS: # Ball is in FOV (search 45 deg to both sides of the ball) + ball_dir = M.vector_angle(w.ball_rel_torso_cart_pos[:2]) + targ = np.clip(ball_dir + (45 if self.look_left else -45), -119, 119) + else: # Ball is not in FOV (search 119 deg to both sides) + targ = 119 if self.look_left else -119 + + if r.set_joints_target_position_direct([0,1], np.array([targ,Head.HEAD_PITCH]), False) <= 0: + self.look_left = not self.look_left + + else: # Adjust position or guided search + r.set_joints_target_position_direct([0,1], np.array([best_dir,Head.HEAD_PITCH]), False) + + + def compute_best_direction(self, can_self_locate, use_ball_from_vision=False): + FOV_MARGIN = 15 # safety margin, avoid margin horizontally + SAFE_RANGE = 120 - FOV_MARGIN*2 + HALF_RANGE = SAFE_RANGE / 2 + + w = self.world + r = w.robot + + if use_ball_from_vision: + ball_2d_dist = np.linalg.norm(w.ball_rel_torso_cart_pos[:2]) + else: + ball_2d_dist = np.linalg.norm(w.ball_abs_pos[:2]-r.loc_head_position[:2]) + + if ball_2d_dist > 0.12: + if use_ball_from_vision: + ball_dir = M.vector_angle(w.ball_rel_torso_cart_pos[:2]) + else: + ball_dir = M.target_rel_angle(r.loc_head_position, r.imu_torso_orientation, w.ball_abs_pos) + else: # ball is very close to robot + ball_dir = 0 + + flags_diff = dict() + + # iterate flags + for f in Head.FIELD_FLAGS: + flag_dir = M.target_rel_angle(r.loc_head_position, r.imu_torso_orientation, f) + diff = M.normalize_deg( flag_dir - ball_dir ) + if abs(diff) < HALF_RANGE and can_self_locate: + return ball_dir # return ball direction if robot can self-locate + flags_diff[f] = diff + + + closest_flag = min( flags_diff, key=lambda k: abs(flags_diff[k]) ) + closest_diff = flags_diff[closest_flag] + + if can_self_locate: # at this point, if it can self-locate, then abs(closest_diff) > HALF_RANGE + # return position that centers the ball as much as possible in the FOV, including the nearest flag if possible + final_diff = min( abs(closest_diff) - HALF_RANGE, SAFE_RANGE ) * np.sign(closest_diff) + else: + # position that centers the flag as much as possible, until it is seen, while keeping the ball in the FOV + final_diff = np.clip( closest_diff, -SAFE_RANGE, SAFE_RANGE ) + # saturate instead of normalizing angle to avoid a complete neck rotation + return np.clip(ball_dir + final_diff, -119, 119) + + + return M.normalize_deg( ball_dir + final_diff ) \ No newline at end of file diff --git a/behaviors/Poses.py b/behaviors/Poses.py new file mode 100644 index 0000000..80458e2 --- /dev/null +++ b/behaviors/Poses.py @@ -0,0 +1,97 @@ +''' +Pose - angles in degrees for the specified joints +Note: toes positions are ignored by robots that have no toes + +Poses may control all joints or just a subgroup defined by the "indices" variable +''' + +import numpy as np +from world.World import World + + +class Poses(): + def __init__(self, world : World) -> None: + self.world = world + self.tolerance = 0.05 # angle error tolerance to consider that behavior is finished + + ''' + Instruction to add new pose: + 1. add new entry to the following dictionary, using a unique behavior name + 2. that's it + ''' + self.poses = { + "Zero":( + "Neutral pose, including head", # description + False, # disable automatic head orientation + np.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]), # indices + np.array([0,0,0,0,0,0,0,0,0,0, 0, 0, 0, 0,-90,-90, 0, 0,90,90, 0, 0, 0, 0]) # values + ), + "Zero_Legacy":( + "Neutral pose, including head, elbows cause collision (legacy)", # description + False, # disable automatic head orientation + np.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]), # indices + np.array([0,0,0,0,0,0,0,0,0,0, 0, 0, 0, 0,-90,-90, 0, 0, 0, 0, 0, 0, 0, 0]) # values + ), + "Zero_Bent_Knees":( + "Neutral pose, including head, bent knees", # description + False, # disable automatic head orientation + np.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]), # indices + np.array([0,0,0,0,0,0,30,30,-60,-60,30,30, 0, 0,-90,-90, 0, 0,90,90, 0, 0, 0, 0]) # values + ), + "Zero_Bent_Knees_Auto_Head":( + "Neutral pose, automatic head, bent knees", # description + True, # enable automatic head orientation + np.array([2,3,4,5,6, 7, 8, 9,10,11,12,13, 14, 15,16,17,18,19,20,21,22,23]), # indices + np.array([0,0,0,0,30,30,-60,-60,30,30, 0, 0,-90,-90, 0, 0,90,90, 0, 0, 0, 0]) # values + ), + "Fall_Back":( + "Incline feet to fall back", # description + True, # enable automatic head orientation + np.array([ 10, 11]), # indices + np.array([-20,-20]) # values + ), + "Fall_Front":( + "Incline feet to fall forward", # description + True, # enable automatic head orientation + np.array([10,11]), # indices + np.array([45,45]) # values + ), + "Fall_Left":( + "Incline legs to fall to left", # description + True, # enable automatic head orientation + np.array([ 4, 5]), # indices + np.array([-20,20]) # values + ), + "Fall_Right":( + "Incline legs to fall to right", # description + True, # enable automatic head orientation + np.array([ 4, 5]), # indices + np.array([20,-20]) # values + ), + } + + # Remove toes if not robot 4 + if world.robot.type != 4: + for key, val in self.poses.items(): + idxs = np.where(val[2] >= 22)[0] # search for joint 22 & 23 + if len(idxs) > 0: + self.poses[key] = (val[0], val[1], np.delete(val[2],idxs), np.delete(val[3],idxs)) # remove those joints + + + def get_behaviors_callbacks(self): + ''' + Returns callbacks for each pose behavior (used internally) + + Implementation note: + -------------------- + Using dummy default parameters because lambda expression will remember the scope and var name. + In the loop, the scope does not change, nor does the var name. + However, default parameters are evaluated when the lambda is defined. + ''' + return {key: (val[0], val[1], lambda reset, key=key: self.execute(key), lambda: True) for key, val in self.poses.items()} + + def execute(self,name) -> bool: + _, _, indices, values = self.poses[name] + remaining_steps = self.world.robot.set_joints_target_position_direct(indices,values,True,tolerance=self.tolerance) + return bool(remaining_steps == -1) + diff --git a/behaviors/Slot_Engine.py b/behaviors/Slot_Engine.py new file mode 100644 index 0000000..7566228 --- /dev/null +++ b/behaviors/Slot_Engine.py @@ -0,0 +1,110 @@ +from math_ops.Math_Ops import Math_Ops as M +from os import listdir +from os.path import isfile, join +from world.World import World +import numpy as np +import xml.etree.ElementTree as xmlp + +class Slot_Engine(): + + def __init__(self, world : World) -> None: + self.world = world + self.state_slot_number = 0 + self.state_slot_start_time = 0 + self.state_slot_start_angles = None + self.state_init_zero = True + + # ------------- Parse slot behaviors + + dir = M.get_active_directory("/behaviors/slot/") + + common_dir = f"{dir}common/" + files = [(f,join(common_dir, f)) for f in listdir(common_dir) if isfile(join(common_dir, f)) and f.endswith(".xml")] + robot_dir = f"{dir}r{world.robot.type}" + files += [(f,join(robot_dir, f)) for f in listdir(robot_dir) if isfile(join(robot_dir, f)) and f.endswith(".xml")] + + self.behaviors = dict() + self.descriptions = dict() + self.auto_head_flags = dict() + + for fname, file in files: + robot_xml_root = xmlp.parse(file).getroot() + slots = [] + bname = fname[:-4] # remove extension ".xml" + + for xml_slot in robot_xml_root: + assert xml_slot.tag == 'slot', f"Unexpected XML element in slot behavior {fname}: '{xml_slot.tag}'" + indices, angles = [],[] + + for action in xml_slot: + indices.append( int(action.attrib['id']) ) + angles.append( float(action.attrib['angle']) ) + + delta_ms = float(xml_slot.attrib['delta']) * 1000 + assert delta_ms > 0, f"Invalid delta <=0 found in Slot Behavior {fname}" + slots.append((delta_ms, indices, angles)) + + assert bname not in self.behaviors, f"Found at least 2 slot behaviors with same name: {fname}" + + self.descriptions[bname] = robot_xml_root.attrib["description"] if "description" in robot_xml_root.attrib else bname + self.auto_head_flags[bname] = (robot_xml_root.attrib["auto_head"] == "1") + self.behaviors[bname] = slots + + + def get_behaviors_callbacks(self): + ''' + Returns callbacks for each slot behavior (used internally) + + Implementation note: + -------------------- + Using dummy default parameters because lambda expression will remember the scope and var name. + In the loop, the scope does not change, nor does the var name. + However, default parameters are evaluated when the lambda is defined. + ''' + return {key: (self.descriptions[key],self.auto_head_flags[key], + lambda reset,key=key: self.execute(key,reset), lambda key=key: self.is_ready(key)) for key in self.behaviors} + + + def is_ready(self,name) -> bool: + return True + + + def reset(self, name): + ''' Initialize/Reset slot behavior ''' + + self.state_slot_number = 0 + self.state_slot_start_time_ms = self.world.time_local_ms + self.state_slot_start_angles = np.copy(self.world.robot.joints_position) + assert name in self.behaviors, f"Requested slot behavior does not exist: {name}" + + + def execute(self,name,reset) -> bool: + ''' Execute one step ''' + + if reset: self.reset(name) + + elapsed_ms = self.world.time_local_ms - self.state_slot_start_time_ms + delta_ms, indices, angles = self.behaviors[name][self.state_slot_number] + + # Check slot progression + if elapsed_ms >= delta_ms: + self.state_slot_start_angles[indices] = angles #update start angles based on last target + + # Prevent 2 rare scenarios: + # 1 - this function is called after the behavior is finished & reset==False + # 2 - we are in the last slot, syncmode is not active, and we lost the last step + if self.state_slot_number+1 == len(self.behaviors[name]): + return True # So, the return indicates a finished behavior until a reset is sent via the arguments + + self.state_slot_number += 1 + elapsed_ms = 0 + self.state_slot_start_time_ms = self.world.time_local_ms + delta_ms, indices, angles = self.behaviors[name][self.state_slot_number] + + # Execute + progress = (elapsed_ms+20) / delta_ms + target = (angles - self.state_slot_start_angles[indices]) * progress + self.state_slot_start_angles[indices] + self.world.robot.set_joints_target_position_direct(indices,target,False) + + # Return True if finished (this is the last step) + return bool(elapsed_ms+20 >= delta_ms and self.state_slot_number + 1 == len(self.behaviors[name])) # true if next step (now+20ms) is out of bounds diff --git a/behaviors/custom/Basic_Kick/Basic_Kick.py b/behaviors/custom/Basic_Kick/Basic_Kick.py new file mode 100644 index 0000000..274bbb8 --- /dev/null +++ b/behaviors/custom/Basic_Kick/Basic_Kick.py @@ -0,0 +1,74 @@ +from agent.Base_Agent import Base_Agent +from behaviors.custom.Step.Step_Generator import Step_Generator +from math_ops.Math_Ops import Math_Ops as M + + +class Basic_Kick(): + + def __init__(self, base_agent : Base_Agent) -> None: + self.behavior = base_agent.behavior + self.path_manager = base_agent.path_manager + self.world = base_agent.world + self.description = "Walk to ball and perform a basic kick" + self.auto_head = True + + r_type = self.world.robot.type + self.bias_dir = [22,29,26,29,22][self.world.robot.type] + self.ball_x_limits = ((0.19,0.215), (0.2,0.22), (0.19,0.22), (0.2,0.215), (0.2,0.215))[r_type] + self.ball_y_limits = ((-0.115,-0.1), (-0.125,-0.095), (-0.12,-0.1), (-0.13,-0.105), (-0.09,-0.06))[r_type] + self.ball_x_center = (self.ball_x_limits[0] + self.ball_x_limits[1])/2 + self.ball_y_center = (self.ball_y_limits[0] + self.ball_y_limits[1])/2 + + def execute(self,reset, direction, abort=False) -> bool: # You can add more arguments + ''' + Parameters + ---------- + direction : float + kick direction relative to field, in degrees + abort : bool + True to abort. + The method returns True upon successful abortion, which is immediate while the robot is aligning itself. + However, if the abortion is requested during the kick, it is delayed until the kick is completed. + ''' + + w = self.world + r = self.world.robot + b = w.ball_rel_torso_cart_pos + t = w.time_local_ms + gait : Step_Generator = self.behavior.get_custom_behavior_object("Walk").env.step_generator + + if reset: + self.phase = 0 + self.reset_time = t + + if self.phase == 0: + biased_dir = M.normalize_deg(direction + self.bias_dir) # add bias to rectify direction + ang_diff = abs(M.normalize_deg( biased_dir - r.loc_torso_orientation )) # the reset was learned with loc, not IMU + + next_pos, next_ori, dist_to_final_target = self.path_manager.get_path_to_ball( + x_ori=biased_dir, x_dev=-self.ball_x_center, y_dev=-self.ball_y_center, torso_ori=biased_dir) + + if (w.ball_last_seen > t - w.VISUALSTEP_MS and ang_diff < 5 and # ball is visible & aligned + self.ball_x_limits[0] < b[0] < self.ball_x_limits[1] and # ball is in kick area (x) + self.ball_y_limits[0] < b[1] < self.ball_y_limits[1] and # ball is in kick area (y) + t - w.ball_abs_pos_last_update < 100 and # ball absolute location is recent + dist_to_final_target < 0.03 and # if absolute ball position is updated + not gait.state_is_left_active and gait.state_current_ts == 2 and # walk gait phase is adequate + t - self.reset_time > 500): # to avoid kicking immediately without preparation & stability + + self.phase += 1 + + return self.behavior.execute_sub_behavior("Kick_Motion", True) + else: + dist = max(0.07, dist_to_final_target) + reset_walk = reset and self.behavior.previous_behavior != "Walk" # reset walk if it wasn't the previous behavior + self.behavior.execute_sub_behavior("Walk", reset_walk, next_pos, True, next_ori, True, dist) # target, is_target_abs, ori, is_ori_abs, distance + return abort # abort only if self.phase == 0 + + else: # define kick parameters and execute + return self.behavior.execute_sub_behavior("Kick_Motion", False) + + + def is_ready(self) -> any: # You can add more arguments + ''' Returns True if this behavior is ready to start/continue under current game/robot conditions ''' + return True diff --git a/behaviors/custom/Dribble/Dribble.py b/behaviors/custom/Dribble/Dribble.py new file mode 100644 index 0000000..d5518b3 --- /dev/null +++ b/behaviors/custom/Dribble/Dribble.py @@ -0,0 +1,207 @@ +from agent.Base_Agent import Base_Agent +from behaviors.custom.Dribble.Env import Env +from math_ops.Math_Ops import Math_Ops as M +from math_ops.Neural_Network import run_mlp +import numpy as np +import pickle + + +class Dribble(): + + def __init__(self, base_agent : Base_Agent) -> None: + self.behavior = base_agent.behavior + self.path_manager = base_agent.path_manager + self.world = base_agent.world + self.description = "RL dribble" + self.auto_head = True + self.env = Env(base_agent, 0.9 if self.world.robot.type == 3 else 1.2) + + with open(M.get_active_directory([ + "/behaviors/custom/Dribble/dribble_R0.pkl", + "/behaviors/custom/Dribble/dribble_R1.pkl", + "/behaviors/custom/Dribble/dribble_R2.pkl", + "/behaviors/custom/Dribble/dribble_R3.pkl", + "/behaviors/custom/Dribble/dribble_R4.pkl" + ][self.world.robot.type]), 'rb') as f: + self.model = pickle.load(f) + + def define_approach_orientation(self): + + w = self.world + b = w.ball_abs_pos[:2] + me = w.robot.loc_head_position[:2] + + self.approach_orientation = None + + MARGIN = 0.8 # safety margin (if ball is near the field limits by this amount, the approach orientation is considered) + M90 = 90/MARGIN # auxiliary variable + DEV = 25 # when standing on top of sidelines or endlines, the approach direction deviates from that line by this amount + MDEV = (90+DEV)/MARGIN # auxiliary variable + + a1 = -180 # angle range start (counterclockwise rotation) + a2 = 180 # angle range end (counterclockwise rotation) + + if b[1] < -10 + MARGIN: + if b[0] < -15 + MARGIN: + a1 = DEV - M90 * (b[1]+10) + a2 = 90 - DEV + M90 * (b[0]+15) + elif b[0] > 15 - MARGIN: + a1 = 90 + DEV - M90 * (15-b[0]) + a2 = 180 - DEV + M90 * (b[1]+10) + else: + a1 = DEV - MDEV * (b[1]+10) + a2 = 180 - DEV + MDEV * (b[1]+10) + elif b[1] > 10 - MARGIN: + if b[0] < -15 + MARGIN: + a1 = -90 + DEV - M90 * (b[0]+15) + a2 = -DEV + M90 * (10-b[1]) + elif b[0] > 15 - MARGIN: + a1 = 180 + DEV - M90 * (10-b[1]) + a2 = 270 - DEV + M90 * (15-b[0]) + else: + a1 = -180 + DEV - MDEV * (10-b[1]) + a2 = -DEV + MDEV * (10-b[1]) + elif b[0] < -15 + MARGIN: + a1 = -90 + DEV - MDEV * (b[0]+15) + a2 = 90 - DEV + MDEV * (b[0]+15) + elif b[0] > 15 - MARGIN and abs(b[1]) > 1.2: + a1 = 90 + DEV - MDEV * (15-b[0]) + a2 = 270 - DEV + MDEV * (15-b[0]) + + + cad = M.vector_angle(b - me) # current approach direction + + a1 = M.normalize_deg(a1) + a2 = M.normalize_deg(a2) + + if a10.4 + + if reset: + self.phase = 0 + if behavior.previous_behavior == "Push_RL" and 0 0.4: # stop defining orientation after getting near the ball to avoid noise + self.define_approach_orientation() + + #------------------------ 2A. A better approach orientation is needed (ball is almost out of bounds) + if self.approach_orientation is not None: + next_pos, next_ori, dist_to_final_target = self.path_manager.get_path_to_ball( + x_ori=self.approach_orientation, x_dev=-0.24, torso_ori=self.approach_orientation, safety_margin=0.4) + + if b_rel[0]<0.26 and b_rel[0]>0.18 and abs(b_rel[1])<0.04 and w.ball_is_visible: # ready to start dribble + self.phase += 1 + reset_dribble = True + else: + dist = max(0.08, dist_to_final_target*0.7) + behavior.execute_sub_behavior("Walk", reset_walk, next_pos, True, next_ori, True, dist) # target, is_target_abs, ori, is_ori_abs, distance + + #------------------------ 2B. A better approach orientation is not needed but the robot cannot see the ball + elif w.time_local_ms - w.ball_last_seen > 200: # walk to absolute target if ball was not seen + abs_ori = M.vector_angle( b - me ) + behavior.execute_sub_behavior("Walk", reset_walk, b, True, abs_ori, True, None) # target, is_target_abs, ori, is_ori_abs, distance + + #------------------------ 2C. A better approach orientation is not needed and the robot can see the ball + else: # walk to relative target + if 0.18 0.5 and lost_ball)): # go back to walking + self.phase += 1 + elif self.phase == 1: # dribble + #------------------------ 1. Define dribble parameters + self.env.dribble_speed = speed + + # Relative orientation values are decreased to avoid overshoot + if orientation is None: + if b[0] < 0: # dribble to the sides + if b[1] > 0: + dribble_target = (15,5) + else: + dribble_target = (15,-5) + else: + dribble_target = None # go to goal + self.env.dribble_rel_orientation = self.path_manager.get_dribble_path(optional_2d_target=dribble_target)[1] + elif is_orientation_absolute: + self.env.dribble_rel_orientation = M.normalize_deg( orientation - r.imu_torso_orientation ) + else: + self.env.dribble_rel_orientation = float(orientation) # copy if numpy float + + #------------------------ 2. Execute behavior + obs = self.env.observe(reset_dribble) + action = run_mlp(obs, self.model) + self.env.execute(action) + + # wind down dribbling, and then reset phase + if self.phase > 1: + WIND_DOWN_STEPS = 60 + #------------------------ 1. Define dribble wind down parameters + self.env.dribble_speed = 1 - self.phase/WIND_DOWN_STEPS + self.env.dribble_rel_orientation = 0 + + #------------------------ 2. Execute behavior + obs = self.env.observe(reset_dribble, virtual_ball=True) + action = run_mlp(obs, self.model) + self.env.execute(action) + + #------------------------ 3. Reset behavior + self.phase += 1 + if self.phase >= WIND_DOWN_STEPS - 5: + self.phase = 0 + return True + + + return False + + def is_ready(self): + ''' Returns True if this behavior is ready to start/continue under current game/robot conditions ''' + return True \ No newline at end of file diff --git a/behaviors/custom/Dribble/Env.py b/behaviors/custom/Dribble/Env.py new file mode 100644 index 0000000..479e17d --- /dev/null +++ b/behaviors/custom/Dribble/Env.py @@ -0,0 +1,181 @@ +from agent.Base_Agent import Base_Agent +from behaviors.custom.Step.Step_Generator import Step_Generator +from math_ops.Math_Ops import Math_Ops as M +import math +import numpy as np + +class Env(): + def __init__(self, base_agent : Base_Agent, step_width) -> None: + + self.world = base_agent.world + self.ik = base_agent.inv_kinematics + + # State space + self.obs = np.zeros(76, np.float32) + + # Step behavior defaults + self.STEP_DUR = 8 + self.STEP_Z_SPAN = 0.02 + self.STEP_Z_MAX = 0.70 + + # IK + r = self.world.robot + nao_specs = self.ik.NAO_SPECS + self.leg_length = nao_specs[1] + nao_specs[3] # upper leg height + lower leg height + feet_y_dev = nao_specs[0] * step_width # wider step + sample_time = r.STEPTIME + max_ankle_z = nao_specs[5] + + self.step_generator = Step_Generator(feet_y_dev, sample_time, max_ankle_z) + self.DEFAULT_ARMS = np.array([-90,-90,8,8,90,90,70,70],np.float32) + + self.dribble_rel_orientation = None # relative to imu_torso_orientation (in degrees) + self.dribble_speed = 1 + + + def observe(self, init=False, virtual_ball=False): + + w = self.world + r = self.world.robot + + if init: # reset variables + self.step_counter = 0 + self.act = np.zeros(16, np.float32) # memory variable + + # index observation naive normalization + self.obs[0] = min(self.step_counter,12*8) /100 # simple counter: 0,1,2,3... + self.obs[1] = r.loc_head_z *3 # z coordinate (torso) + self.obs[2] = r.loc_head_z_vel /2 # z velocity (torso) + self.obs[3] = r.imu_torso_roll /15 # absolute torso roll in deg + self.obs[4] = r.imu_torso_pitch /15 # absolute torso pitch in deg + self.obs[5:8] = r.gyro /100 # gyroscope + self.obs[8:11] = r.acc /10 # accelerometer + + self.obs[11:17] = r.frp.get('lf', np.zeros(6)) * (10,10,10,0.01,0.01,0.01) # left foot: relative point of origin (p) and force vector (f) -> (px,py,pz,fx,fy,fz)* + self.obs[17:23] = r.frp.get('rf', np.zeros(6)) * (10,10,10,0.01,0.01,0.01) # right foot: relative point of origin (p) and force vector (f) -> (px,py,pz,fx,fy,fz)* + # *if foot is not touching the ground, then (px=0,py=0,pz=0,fx=0,fy=0,fz=0) + + self.obs[23:43] = r.joints_position[2:22] /100 # position of all joints except head & toes (for robot type 4) + self.obs[43:63] = r.joints_speed[2:22] /6.1395 # speed of all joints except head & toes (for robot type 4) + + ''' + Expected observations for walking state: + Time step R 0 1 2 3 4 5 6 7 0 + Progress 1 0 .14 .28 .43 .57 .71 .86 1 0 + Left leg active T F F F F F F F F T + ''' + + if init: # the walking parameters refer to the last parameters in effect (after a reset, they are pointless) + self.obs[63] = 1 # step progress + self.obs[64] = 1 # 1 if left leg is active + self.obs[65] = 0 # 1 if right leg is active + self.obs[66] = 0 + else: + self.obs[63] = self.step_generator.external_progress # step progress + self.obs[64] = float(self.step_generator.state_is_left_active) # 1 if left leg is active + self.obs[65] = float(not self.step_generator.state_is_left_active) # 1 if right leg is active + self.obs[66] = math.sin(self.step_generator.state_current_ts / self.step_generator.ts_per_step * math.pi) + + # Ball + ball_rel_hip_center = self.ik.torso_to_hip_transform(w.ball_rel_torso_cart_pos) + ball_dist_hip_center = np.linalg.norm( ball_rel_hip_center ) + + if init: + self.obs[67:70] = (0,0,0) # Initial velocity is 0 + elif w.ball_is_visible: + self.obs[67:70] = (ball_rel_hip_center - self.obs[70:73]) * 10 # Ball velocity, relative to ankle's midpoint + + self.obs[70:73] = ball_rel_hip_center # Ball position, relative to hip + self.obs[73] = ball_dist_hip_center * 2 + + if virtual_ball: # simulate the ball between the robot's feet + self.obs[67:74] = (0,0,0,0.05,0,-0.175,0.36) + + ''' + Create internal target with a smoother variation + ''' + + MAX_ROTATION_DIFF = 20 # max difference (degrees) per visual step + MAX_ROTATION_DIST = 80 + + if init: + self.internal_rel_orientation = 0 + self.internal_target_vel = 0 + self.gym_last_internal_abs_ori = r.imu_torso_orientation # for training purposes (reward) + + + #---------------------------------------------------------------- compute internal target + + if w.vision_is_up_to_date: + + previous_internal_rel_orientation = np.copy(self.internal_rel_orientation) + + internal_ori_diff = np.clip( M.normalize_deg( self.dribble_rel_orientation - self.internal_rel_orientation ), -MAX_ROTATION_DIFF, MAX_ROTATION_DIFF) + self.internal_rel_orientation = np.clip(M.normalize_deg( self.internal_rel_orientation + internal_ori_diff ), -MAX_ROTATION_DIST, MAX_ROTATION_DIST) + + # Observations + self.internal_target_vel = self.internal_rel_orientation - previous_internal_rel_orientation + + self.gym_last_internal_abs_ori = self.internal_rel_orientation + r.imu_torso_orientation + + #----------------------------------------------------------------- observations + + self.obs[74] = self.internal_rel_orientation / MAX_ROTATION_DIST + self.obs[75] = self.internal_target_vel / MAX_ROTATION_DIFF + + return self.obs + + + def execute_ik(self, l_pos, l_rot, r_pos, r_rot): + r = self.world.robot + # Apply IK to each leg + Set joint targets + + # Left leg + indices, self.values_l, error_codes = self.ik.leg(l_pos, l_rot, True, dynamic_pose=False) + + r.set_joints_target_position_direct(indices, self.values_l, harmonize=False) + + # Right leg + indices, self.values_r, error_codes = self.ik.leg(r_pos, r_rot, False, dynamic_pose=False) + + r.set_joints_target_position_direct(indices, self.values_r, harmonize=False) + + + def execute(self, action): + + r = self.world.robot + + # Actions: + # 0,1,2 left ankle pos + # 3,4,5 right ankle pos + # 6,7,8 left foot rotation + # 9,10,11 right foot rotation + # 12,13 left/right arm pitch + # 14,15 left/right arm roll + + # exponential moving average + self.act = 0.85 * self.act + 0.15 * action * 0.7 * 0.95 * self.dribble_speed + + # execute Step behavior to extract the target positions of each leg (we will override these targets) + lfy,lfz,rfy,rfz = self.step_generator.get_target_positions(self.step_counter == 0, self.STEP_DUR, self.STEP_Z_SPAN, self.leg_length * self.STEP_Z_MAX) + + # Leg IK + a = self.act + l_ankle_pos = (a[0]*0.025-0.01, a[1]*0.01 + lfy, a[2]*0.01 + lfz) + r_ankle_pos = (a[3]*0.025-0.01, a[4]*0.01 + rfy, a[5]*0.01 + rfz) + l_foot_rot = a[6:9] * (2,2,3) + r_foot_rot = a[9:12] * (2,2,3) + + # Limit leg yaw/pitch (and add bias) + l_foot_rot[2] = max(0,l_foot_rot[2] + 18.3) + r_foot_rot[2] = min(0,r_foot_rot[2] - 18.3) + + # Arms actions + arms = np.copy(self.DEFAULT_ARMS) # default arms pose + arms[0:4] += a[12:16]*4 # arms pitch+roll + + # Set target positions + self.execute_ik(l_ankle_pos, l_foot_rot, r_ankle_pos, r_foot_rot) # legs + r.set_joints_target_position_direct( slice(14,22), arms, harmonize=False ) # arms + + self.step_counter += 1 \ No newline at end of file diff --git a/behaviors/custom/Dribble/dribble_R0.pkl b/behaviors/custom/Dribble/dribble_R0.pkl new file mode 100644 index 0000000..7d0e050 Binary files /dev/null and b/behaviors/custom/Dribble/dribble_R0.pkl differ diff --git a/behaviors/custom/Dribble/dribble_R1.pkl b/behaviors/custom/Dribble/dribble_R1.pkl new file mode 100644 index 0000000..9e1043c Binary files /dev/null and b/behaviors/custom/Dribble/dribble_R1.pkl differ diff --git a/behaviors/custom/Dribble/dribble_R2.pkl b/behaviors/custom/Dribble/dribble_R2.pkl new file mode 100644 index 0000000..9d9d61f Binary files /dev/null and b/behaviors/custom/Dribble/dribble_R2.pkl differ diff --git a/behaviors/custom/Dribble/dribble_R3.pkl b/behaviors/custom/Dribble/dribble_R3.pkl new file mode 100644 index 0000000..aa33258 Binary files /dev/null and b/behaviors/custom/Dribble/dribble_R3.pkl differ diff --git a/behaviors/custom/Dribble/dribble_R4.pkl b/behaviors/custom/Dribble/dribble_R4.pkl new file mode 100644 index 0000000..781d55a Binary files /dev/null and b/behaviors/custom/Dribble/dribble_R4.pkl differ diff --git a/behaviors/custom/Fall/Fall.py b/behaviors/custom/Fall/Fall.py new file mode 100644 index 0000000..398e719 --- /dev/null +++ b/behaviors/custom/Fall/Fall.py @@ -0,0 +1,43 @@ +from agent.Base_Agent import Base_Agent +from math_ops.Math_Ops import Math_Ops as M +from math_ops.Neural_Network import run_mlp +import pickle, numpy as np + +class Fall(): + + def __init__(self, base_agent : Base_Agent) -> None: + self.world = base_agent.world + self.description = "Fall example" + self.auto_head = False + + with open(M.get_active_directory("/behaviors/custom/Fall/fall.pkl"), 'rb') as f: + self.model = pickle.load(f) + + self.action_size = len(self.model[-1][0]) # extracted from size of Neural Network's last layer bias + self.obs = np.zeros(self.action_size+1, np.float32) + + self.controllable_joints = min(self.world.robot.no_of_joints, self.action_size) # compatibility between different robot types + + def observe(self): + r = self.world.robot + + for i in range(self.action_size): + self.obs[i] = r.joints_position[i] / 100 # naive scale normalization + + self.obs[self.action_size] = r.cheat_abs_pos[2] # head.z (alternative: r.loc_head_z) + + def execute(self,reset) -> bool: + self.observe() + action = run_mlp(self.obs, self.model) + + self.world.robot.set_joints_target_position_direct( # commit actions: + slice(self.controllable_joints), # act on trained joints + action*10, # scale actions up to motivate early exploration + harmonize=False # there is no point in harmonizing actions if the targets change at every step + ) + + return self.world.robot.loc_head_z < 0.15 # finished when head height < 0.15 m + + def is_ready(self) -> any: + ''' Returns True if this behavior is ready to start/continue under current game/robot conditions ''' + return True diff --git a/behaviors/custom/Fall/fall.pkl b/behaviors/custom/Fall/fall.pkl new file mode 100644 index 0000000..d7976e4 Binary files /dev/null and b/behaviors/custom/Fall/fall.pkl differ diff --git a/behaviors/custom/Get_Up/Get_Up.py b/behaviors/custom/Get_Up/Get_Up.py new file mode 100644 index 0000000..8dd88ac --- /dev/null +++ b/behaviors/custom/Get_Up/Get_Up.py @@ -0,0 +1,68 @@ +from agent.Base_Agent import Base_Agent +from collections import deque +import numpy as np + +class Get_Up(): + + def __init__(self, base_agent : Base_Agent) -> None: + self.behavior = base_agent.behavior + self.world = base_agent.world + self.description = "Get Up using the most appropriate skills" + self.auto_head = False + self.MIN_HEIGHT = 0.3 # minimum value for the head's height + self.MAX_INCLIN = 50 # maximum torso inclination in degrees + self.STABILITY_THRESHOLD = 4 + + def reset(self): + self.state = 0 + self.gyro_queue = deque(maxlen=self.STABILITY_THRESHOLD) + self.watchdog = 0 # when player has the shaking bug, it is never stable enough to get up + + def execute(self,reset): + + r = self.world.robot + execute_sub_behavior = self.behavior.execute_sub_behavior + + if reset: + self.reset() + + if self.state == 0: # State 0: go to pose "Zero" + + self.watchdog += 1 + self.gyro_queue.append( max(abs(r.gyro)) ) # log last STABILITY_THRESHOLD values + + # advance to next state if behavior is complete & robot is stable + if (execute_sub_behavior("Zero",None) and len(self.gyro_queue) == self.STABILITY_THRESHOLD + and all(g < 10 for g in self.gyro_queue)) or self.watchdog > 100: + + # determine how to get up + if r.acc[0] < -4 and abs(r.acc[1]) < 2 and abs(r.acc[2]) < 3: + execute_sub_behavior("Get_Up_Front", True) # reset behavior + self.state = 1 + elif r.acc[0] > 4 and abs(r.acc[1]) < 2 and abs(r.acc[2]) < 3: + execute_sub_behavior("Get_Up_Back", True) # reset behavior + self.state = 2 + elif r.acc[2] > 8: # fail-safe if vision is not up to date: if pose is 'Zero' and torso is upright, the robot is already up + return True + else: + execute_sub_behavior("Flip", True) # reset behavior + self.state = 3 + + elif self.state == 1: + if execute_sub_behavior("Get_Up_Front", False): + return True + elif self.state == 2: + if execute_sub_behavior("Get_Up_Back", False): + return True + elif self.state == 3: + if execute_sub_behavior("Flip", False): + self.reset() + + return False + + + def is_ready(self): + ''' Returns True if the Get Up behavior is ready (= robot is down) ''' + r = self.world.robot + # check if z < 5 and acc magnitude > 8 and any visual indicator says we fell + return r.acc[2] < 5 and np.dot(r.acc,r.acc) > 64 and (r.loc_head_z < self.MIN_HEIGHT or r.imu_torso_inclination > self.MAX_INCLIN) diff --git a/behaviors/custom/Kick_8/Kick_8.py b/behaviors/custom/Kick_8/Kick_8.py new file mode 100644 index 0000000..9dbfc9d --- /dev/null +++ b/behaviors/custom/Kick_8/Kick_8.py @@ -0,0 +1,75 @@ +from agent.Base_Agent import Base_Agent +from behaviors.custom.Step.Step_Generator import Step_Generator +from math_ops.Math_Ops import Math_Ops as M + + +class Kick_8(): + + def __init__(self, base_agent: Base_Agent) -> None: + self.behavior = base_agent.behavior + self.path_manager = base_agent.path_manager + self.world = base_agent.world + self.description = "Walk to ball and perform a Kick_8" + self.auto_head = True + + r_type = self.world.robot.type + self.bias_dir = [22, 29, 26, 29, 22][self.world.robot.type] + self.ball_x_limits = ((0.19, 0.215), (0.2, 0.22), (0.19, 0.22), (0.2, 0.215), (0.2, 0.215))[r_type] + self.ball_y_limits = ((-0.115, -0.1), (-0.125, -0.095), (-0.12, -0.1), (-0.13, -0.105), (-0.09, -0.06))[r_type] + self.ball_x_center = (self.ball_x_limits[0] + self.ball_x_limits[1]) / 2 + self.ball_y_center = (self.ball_y_limits[0] + self.ball_y_limits[1]) / 2 + + def execute(self, reset, direction, distance, abort=False) -> bool: # You can add more arguments + ''' + Parameters + ---------- + direction : float + kick direction relative to field, in degrees + abort : bool + True to abort. + The method returns True upon successful abortion, which is immediate while the robot is aligning itself. + However, if the abortion is requested during the kick, it is delayed until the kick is completed. + ''' + + w = self.world + r = self.world.robot + b = w.ball_rel_torso_cart_pos + t = w.time_local_ms + gait: Step_Generator = self.behavior.get_custom_behavior_object("Walk").env.step_generator + + if reset: + self.phase = 0 + self.reset_time = t + + if self.phase == 0: + biased_dir = M.normalize_deg(direction + self.bias_dir) # add bias to rectify direction + ang_diff = abs( + M.normalize_deg(biased_dir - r.loc_torso_orientation)) # the reset was learned with loc, not IMU + + next_pos, next_ori, dist_to_final_target = self.path_manager.get_path_to_ball( + x_ori=biased_dir, x_dev=-self.ball_x_center, y_dev=-self.ball_y_center, torso_ori=biased_dir) + + if (w.ball_last_seen > t - w.VISUALSTEP_MS and ang_diff < 5 and # ball is visible & aligned + self.ball_x_limits[0] < b[0] < self.ball_x_limits[1] and # ball is in kick area (x) + self.ball_y_limits[0] < b[1] < self.ball_y_limits[1] and # ball is in kick area (y) + t - w.ball_abs_pos_last_update < 100 and # ball absolute location is recent + dist_to_final_target < 0.03 and # if absolute ball position is updated + not gait.state_is_left_active and gait.state_current_ts == 2 and # walk gait phase is adequate + t - self.reset_time > 500): # to avoid kicking immediately without preparation & stability + + self.phase += 1 + + return self.behavior.execute_sub_behavior("Kick_Motion", True) + else: + dist = max(0.07, dist_to_final_target) + reset_walk = reset and self.behavior.previous_behavior != "Walk" # reset walk if it wasn't the previous behavior + self.behavior.execute_sub_behavior("Walk", reset_walk, next_pos, True, next_ori, True, + dist) # target, is_target_abs, ori, is_ori_abs, distance + return abort # abort only if self.phase == 0 + + else: # define kick parameters and execute + return self.behavior.execute_sub_behavior("Kick_Motion", False) + + def is_ready(self) -> any: # You can add more arguments + ''' Returns True if this behavior is ready to start/continue under current game/robot conditions ''' + return True diff --git a/behaviors/custom/Step/Step.py b/behaviors/custom/Step/Step.py new file mode 100644 index 0000000..04d7f3c --- /dev/null +++ b/behaviors/custom/Step/Step.py @@ -0,0 +1,59 @@ +from agent.Base_Agent import Base_Agent +from behaviors.custom.Step.Step_Generator import Step_Generator +import numpy as np + +class Step(): + + def __init__(self, base_agent : Base_Agent) -> None: + self.world = base_agent.world + self.ik = base_agent.inv_kinematics + self.description = "Step (Skill-Set-Primitive)" + self.auto_head = True + + nao_specs = self.ik.NAO_SPECS + self.leg_length = nao_specs[1] + nao_specs[3] # upper leg height + lower leg height + + feet_y_dev = nao_specs[0] * 1.2 # wider step + sample_time = self.world.robot.STEPTIME + max_ankle_z = nao_specs[5] + + # Initialize step generator with constants + self.step_generator = Step_Generator(feet_y_dev, sample_time, max_ankle_z) + + + def execute(self,reset, ts_per_step=7, z_span=0.03, z_max=0.8): + + lfy,lfz,rfy,rfz = self.step_generator.get_target_positions(reset, ts_per_step, z_span, self.leg_length * z_max) + + #----------------- Apply IK to each leg + Set joint targets + + # Left leg + indices, self.values_l, error_codes = self.ik.leg((0,lfy,lfz), (0,0,0), True, dynamic_pose=False) + for i in error_codes: + print(f"Joint {i} is out of range!" if i!=-1 else "Position is out of reach!") + + self.world.robot.set_joints_target_position_direct(indices, self.values_l) + + # Right leg + indices, self.values_r, error_codes = self.ik.leg((0,rfy,rfz), (0,0,0), False, dynamic_pose=False) + for i in error_codes: + print(f"Joint {i} is out of range!" if i!=-1 else "Position is out of reach!") + + self.world.robot.set_joints_target_position_direct(indices, self.values_r) + + # ----------------- Fixed arms + + indices = [14,16,18,20] + values = np.array([-80,20,90,0]) + self.world.robot.set_joints_target_position_direct(indices,values) + + indices = [15,17,19,21] + values = np.array([-80,20,90,0]) + self.world.robot.set_joints_target_position_direct(indices,values) + + return False + + + def is_ready(self): + ''' Returns True if Step Behavior is ready to start under current game/robot conditions ''' + return True \ No newline at end of file diff --git a/behaviors/custom/Step/Step_Generator.py b/behaviors/custom/Step/Step_Generator.py new file mode 100644 index 0000000..1efd6f8 --- /dev/null +++ b/behaviors/custom/Step/Step_Generator.py @@ -0,0 +1,75 @@ +import math + + +class Step_Generator(): + GRAVITY = 9.81 + Z0 = 0.2 + + def __init__(self, feet_y_dev, sample_time, max_ankle_z) -> None: + self.feet_y_dev = feet_y_dev + self.sample_time = sample_time + self.state_is_left_active = False + self.state_current_ts = 0 + self.switch = False # switch legs + self.external_progress = 0 # non-overlaped progress + self.max_ankle_z = max_ankle_z + + + def get_target_positions(self, reset, ts_per_step, z_span, z_extension): + ''' + Get target positions for each foot + + Returns + ------- + target : `tuple` + (Left leg y, Left leg z, Right leg y, Right leg z) + ''' + + assert type(ts_per_step)==int and ts_per_step > 0, "ts_per_step must be a positive integer!" + + #-------------------------- Advance 1ts + if reset: + self.ts_per_step = ts_per_step # step duration in time steps + self.swing_height = z_span + self.max_leg_extension = z_extension # maximum distance between ankle to center of both hip joints + self.state_current_ts = 0 + self.state_is_left_active = False + self.switch = False + elif self.switch: + self.state_current_ts = 0 + self.state_is_left_active = not self.state_is_left_active # switch leg + self.switch = False + else: + self.state_current_ts += 1 + + #-------------------------- Compute COM.y + W = math.sqrt(self.Z0/self.GRAVITY) + + step_time = self.ts_per_step * self.sample_time + time_delta = self.state_current_ts * self.sample_time + + y0 = self.feet_y_dev # absolute initial y value + y_swing = y0 + y0 * ( math.sinh((step_time - time_delta)/W) + math.sinh(time_delta/W) ) / math.sinh(-step_time/W) + + #-------------------------- Cap maximum extension and swing height + z0 = min(-self.max_leg_extension, self.max_ankle_z) # capped initial z value + zh = min(self.swing_height, self.max_ankle_z - z0) # capped swing height + + #-------------------------- Compute Z Swing + progress = self.state_current_ts / self.ts_per_step + self.external_progress = self.state_current_ts / (self.ts_per_step-1) + active_z_swing = zh * math.sin(math.pi * progress) + + #-------------------------- Accept new parameters after final step + if self.state_current_ts + 1 >= self.ts_per_step: + self.ts_per_step = ts_per_step # step duration in time steps + self.swing_height = z_span + self.max_leg_extension = z_extension # maximum distance between ankle to center of both hip joints + self.switch = True + + #-------------------------- Distinguish active leg + if self.state_is_left_active: + return y0+y_swing, active_z_swing+z0, -y0+y_swing, z0 + else: + return y0-y_swing, z0, -y0-y_swing, active_z_swing+z0 + diff --git a/behaviors/custom/Walk/Env.py b/behaviors/custom/Walk/Env.py new file mode 100644 index 0000000..274764b --- /dev/null +++ b/behaviors/custom/Walk/Env.py @@ -0,0 +1,201 @@ +from agent.Base_Agent import Base_Agent +from behaviors.custom.Step.Step_Generator import Step_Generator +from math_ops.Math_Ops import Math_Ops as M +import math +import numpy as np + + +class Env(): + def __init__(self, base_agent : Base_Agent) -> None: + + self.world = base_agent.world + self.ik = base_agent.inv_kinematics + + # State space + self.obs = np.zeros(63, np.float32) + + # Step behavior defaults + self.STEP_DUR = 8 + self.STEP_Z_SPAN = 0.02 + self.STEP_Z_MAX = 0.70 + + # IK + nao_specs = self.ik.NAO_SPECS + self.leg_length = nao_specs[1] + nao_specs[3] # upper leg height + lower leg height + feet_y_dev = nao_specs[0] * 1.12 # wider step + sample_time = self.world.robot.STEPTIME + max_ankle_z = nao_specs[5] + + self.step_generator = Step_Generator(feet_y_dev, sample_time, max_ankle_z) + self.DEFAULT_ARMS = np.array([-90,-90,8,8,90,90,70,70],np.float32) + + self.walk_rel_orientation = None + self.walk_rel_target = None + self.walk_distance = None + + + def observe(self, init=False): + + r = self.world.robot + + if init: # reset variables + self.act = np.zeros(16, np.float32) # memory variable + self.step_counter = 0 + + # index observation naive normalization + self.obs[0] = min(self.step_counter,15*8) /100 # simple counter: 0,1,2,3... + self.obs[1] = r.loc_head_z *3 # z coordinate (torso) + self.obs[2] = r.loc_head_z_vel /2 # z velocity (torso) + self.obs[3] = r.imu_torso_roll /15 # absolute torso roll in deg + self.obs[4] = r.imu_torso_pitch /15 # absolute torso pitch in deg + self.obs[5:8] = r.gyro /100 # gyroscope + self.obs[8:11] = r.acc /10 # accelerometer + + self.obs[11:17] = r.frp.get('lf', np.zeros(6)) * (10,10,10,0.01,0.01,0.01) # left foot: relative point of origin (p) and force vector (f) -> (px,py,pz,fx,fy,fz)* + self.obs[17:23] = r.frp.get('rf', np.zeros(6)) * (10,10,10,0.01,0.01,0.01) # right foot: relative point of origin (p) and force vector (f) -> (px,py,pz,fx,fy,fz)* + # *if foot is not touching the ground, then (px=0,py=0,pz=0,fx=0,fy=0,fz=0) + + # Joints: Forward kinematics for ankles + feet rotation + arms (pitch + roll) + rel_lankle = self.ik.get_body_part_pos_relative_to_hip("lankle") # ankle position relative to center of both hip joints + rel_rankle = self.ik.get_body_part_pos_relative_to_hip("rankle") # ankle position relative to center of both hip joints + lf = r.head_to_body_part_transform("torso", r.body_parts['lfoot'].transform ) # foot transform relative to torso + rf = r.head_to_body_part_transform("torso", r.body_parts['rfoot'].transform ) # foot transform relative to torso + lf_rot_rel_torso = np.array( [lf.get_roll_deg(), lf.get_pitch_deg(), lf.get_yaw_deg()] ) # foot rotation relative to torso + rf_rot_rel_torso = np.array( [rf.get_roll_deg(), rf.get_pitch_deg(), rf.get_yaw_deg()] ) # foot rotation relative to torso + + # pose + self.obs[23:26] = rel_lankle * (8,8,5) + self.obs[26:29] = rel_rankle * (8,8,5) + self.obs[29:32] = lf_rot_rel_torso / 20 + self.obs[32:35] = rf_rot_rel_torso / 20 + self.obs[35:39] = r.joints_position[14:18] /100 # arms (pitch + roll) + + # velocity + self.obs[39:55] = r.joints_target_last_speed[2:18] # predictions == last action + + ''' + Expected observations for walking state: + Time step R 0 1 2 3 4 5 6 7 0 + Progress 1 0 .14 .28 .43 .57 .71 .86 1 0 + Left leg active T F F F F F F F F T + ''' + + if init: # the walking parameters refer to the last parameters in effect (after a reset, they are pointless) + self.obs[55] = 1 # step progress + self.obs[56] = 1 # 1 if left leg is active + self.obs[57] = 0 # 1 if right leg is active + else: + self.obs[55] = self.step_generator.external_progress # step progress + self.obs[56] = float(self.step_generator.state_is_left_active) # 1 if left leg is active + self.obs[57] = float(not self.step_generator.state_is_left_active) # 1 if right leg is active + + ''' + Create internal target with a smoother variation + ''' + + MAX_LINEAR_DIST = 0.5 + MAX_LINEAR_DIFF = 0.014 # max difference (meters) per step + MAX_ROTATION_DIFF = 1.6 # max difference (degrees) per step + MAX_ROTATION_DIST = 45 + + + if init: + self.internal_rel_orientation = 0 + self.internal_target = np.zeros(2) + + previous_internal_target = np.copy(self.internal_target) + + #---------------------------------------------------------------- compute internal linear target + + rel_raw_target_size = np.linalg.norm(self.walk_rel_target) + + if rel_raw_target_size == 0: + rel_target = self.walk_rel_target + else: + rel_target = self.walk_rel_target / rel_raw_target_size * min(self.walk_distance, MAX_LINEAR_DIST) + + internal_diff = rel_target - self.internal_target + internal_diff_size = np.linalg.norm(internal_diff) + + if internal_diff_size > MAX_LINEAR_DIFF: + self.internal_target += internal_diff * (MAX_LINEAR_DIFF / internal_diff_size) + else: + self.internal_target[:] = rel_target + + #---------------------------------------------------------------- compute internal rotation target + + internal_ori_diff = np.clip( M.normalize_deg( self.walk_rel_orientation - self.internal_rel_orientation ), -MAX_ROTATION_DIFF, MAX_ROTATION_DIFF) + self.internal_rel_orientation = np.clip(M.normalize_deg( self.internal_rel_orientation + internal_ori_diff ), -MAX_ROTATION_DIST, MAX_ROTATION_DIST) + + #----------------------------------------------------------------- observations + + internal_target_vel = self.internal_target - previous_internal_target + + self.obs[58] = self.internal_target[0] / MAX_LINEAR_DIST + self.obs[59] = self.internal_target[1] / MAX_LINEAR_DIST + self.obs[60] = self.internal_rel_orientation / MAX_ROTATION_DIST + self.obs[61] = internal_target_vel[0] / MAX_LINEAR_DIFF + self.obs[62] = internal_target_vel[0] / MAX_LINEAR_DIFF + + return self.obs + + + def execute_ik(self, l_pos, l_rot, r_pos, r_rot): + r = self.world.robot + # Apply IK to each leg + Set joint targets + + # Left leg + indices, self.values_l, error_codes = self.ik.leg(l_pos, l_rot, True, dynamic_pose=False) + + r.set_joints_target_position_direct(indices, self.values_l, harmonize=False) + + # Right leg + indices, self.values_r, error_codes = self.ik.leg(r_pos, r_rot, False, dynamic_pose=False) + + r.set_joints_target_position_direct(indices, self.values_r, harmonize=False) + + + def execute(self, action): + + r = self.world.robot + + # Actions: + # 0,1,2 left ankle pos + # 3,4,5 right ankle pos + # 6,7,8 left foot rotation + # 9,10,11 right foot rotation + # 12,13 left/right arm pitch + # 14,15 left/right arm roll + + internal_dist = np.linalg.norm( self.internal_target ) + action_mult = 1 if internal_dist > 0.2 else (0.7/0.2) * internal_dist + 0.3 + + # exponential moving average + self.act = 0.8 * self.act + 0.2 * action * action_mult * 0.7 + + # execute Step behavior to extract the target positions of each leg (we will override these targets) + lfy,lfz,rfy,rfz = self.step_generator.get_target_positions(self.step_counter == 0, self.STEP_DUR, self.STEP_Z_SPAN, self.leg_length * self.STEP_Z_MAX) + + + # Leg IK + a = self.act + l_ankle_pos = (a[0]*0.02, max(0.01, a[1]*0.02 + lfy), a[2]*0.01 + lfz) # limit y to avoid self collision + r_ankle_pos = (a[3]*0.02, min(a[4]*0.02 + rfy, -0.01), a[5]*0.01 + rfz) # limit y to avoid self collision + l_foot_rot = a[6:9] * (3,3,5) + r_foot_rot = a[9:12] * (3,3,5) + + # Limit leg yaw/pitch + l_foot_rot[2] = max(0,l_foot_rot[2] + 7) + r_foot_rot[2] = min(0,r_foot_rot[2] - 7) + + # Arms actions + arms = np.copy(self.DEFAULT_ARMS) # default arms pose + arm_swing = math.sin(self.step_generator.state_current_ts / self.STEP_DUR * math.pi) * 6 + inv = 1 if self.step_generator.state_is_left_active else -1 + arms[0:4] += a[12:16]*4 + (-arm_swing*inv,arm_swing*inv,0,0) # arms pitch+roll + + # Set target positions + self.execute_ik(l_ankle_pos, l_foot_rot, r_ankle_pos, r_foot_rot) # legs + r.set_joints_target_position_direct( slice(14,22), arms, harmonize=False ) # arms + + self.step_counter += 1 \ No newline at end of file diff --git a/behaviors/custom/Walk/Walk.py b/behaviors/custom/Walk/Walk.py new file mode 100644 index 0000000..01785d2 --- /dev/null +++ b/behaviors/custom/Walk/Walk.py @@ -0,0 +1,83 @@ +from agent.Base_Agent import Base_Agent +from behaviors.custom.Walk.Env import Env +from math_ops.Math_Ops import Math_Ops as M +from math_ops.Neural_Network import run_mlp +import numpy as np +import pickle + +class Walk(): + + def __init__(self, base_agent : Base_Agent) -> None: + self.world = base_agent.world + self.description = "Omnidirectional RL walk" + self.auto_head = True + self.env = Env(base_agent) + self.last_executed = 0 + + with open(M.get_active_directory([ + "/behaviors/custom/Walk/walk_R0.pkl", + "/behaviors/custom/Walk/walk_R1_R3.pkl", + "/behaviors/custom/Walk/walk_R2.pkl", + "/behaviors/custom/Walk/walk_R1_R3.pkl", + "/behaviors/custom/Walk/walk_R4.pkl" + ][self.world.robot.type]), 'rb') as f: + self.model = pickle.load(f) + + + def execute(self, reset, target_2d, is_target_absolute, orientation, is_orientation_absolute, distance): + ''' + Parameters + ---------- + target_2d : array_like + 2D target in absolute or relative coordinates (use is_target_absolute to specify) + is_target_absolute : bool + True if target_2d is in absolute coordinates, False if relative to robot's torso + orientation : float + absolute or relative orientation of torso, in degrees + set to None to go towards the target (is_orientation_absolute is ignored) + is_orientation_absolute : bool + True if orientation is relative to the field, False if relative to the robot's torso + distance : float + distance to final target [0,0.5] (influences walk speed when approaching the final target) + set to None to consider target_2d the final target + ''' + r = self.world.robot + + #------------------------ 0. Override reset (since some behaviors use this as a sub-behavior) + if reset and self.world.time_local_ms - self.last_executed == 20: + reset = False + self.last_executed = self.world.time_local_ms + + #------------------------ 1. Define walk parameters + + if is_target_absolute: # convert to target relative to (head position + torso orientation) + raw_target = target_2d - r.loc_head_position[:2] + self.env.walk_rel_target = M.rotate_2d_vec(raw_target, -r.imu_torso_orientation) + else: + self.env.walk_rel_target = target_2d + + if distance is None: + self.env.walk_distance = np.linalg.norm(self.env.walk_rel_target) + else: + self.env.walk_distance = distance # MAX_LINEAR_DIST = 0.5 + + # Relative orientation values are decreased to avoid overshoot + if orientation is None: + self.env.walk_rel_orientation = M.vector_angle(self.env.walk_rel_target) * 0.3 + elif is_orientation_absolute: + self.env.walk_rel_orientation = M.normalize_deg( orientation - r.imu_torso_orientation ) + else: + self.env.walk_rel_orientation = orientation * 0.3 + + #------------------------ 2. Execute behavior + + obs = self.env.observe(reset) + action = run_mlp(obs, self.model) + self.env.execute(action) + + return False + + + def is_ready(self): + ''' Returns True if Walk Behavior is ready to start under current game/robot conditions ''' + return True \ No newline at end of file diff --git a/behaviors/custom/Walk/walk_R0.pkl b/behaviors/custom/Walk/walk_R0.pkl new file mode 100644 index 0000000..ac7ec3e Binary files /dev/null and b/behaviors/custom/Walk/walk_R0.pkl differ diff --git a/behaviors/custom/Walk/walk_R1_R3.pkl b/behaviors/custom/Walk/walk_R1_R3.pkl new file mode 100644 index 0000000..5e56c0e Binary files /dev/null and b/behaviors/custom/Walk/walk_R1_R3.pkl differ diff --git a/behaviors/custom/Walk/walk_R2.pkl b/behaviors/custom/Walk/walk_R2.pkl new file mode 100644 index 0000000..bde7460 Binary files /dev/null and b/behaviors/custom/Walk/walk_R2.pkl differ diff --git a/behaviors/custom/Walk/walk_R4.pkl b/behaviors/custom/Walk/walk_R4.pkl new file mode 100644 index 0000000..2d86dbc Binary files /dev/null and b/behaviors/custom/Walk/walk_R4.pkl differ diff --git a/behaviors/slot/common/Dive_Left.xml b/behaviors/slot/common/Dive_Left.xml new file mode 100644 index 0000000..e4e239e --- /dev/null +++ b/behaviors/slot/common/Dive_Left.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/common/Dive_Right.xml b/behaviors/slot/common/Dive_Right.xml new file mode 100644 index 0000000..4078aea --- /dev/null +++ b/behaviors/slot/common/Dive_Right.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/common/Flip.xml b/behaviors/slot/common/Flip.xml new file mode 100644 index 0000000..776fa80 --- /dev/null +++ b/behaviors/slot/common/Flip.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/behaviors/slot/common/Squat.xml b/behaviors/slot/common/Squat.xml new file mode 100644 index 0000000..d8893b0 --- /dev/null +++ b/behaviors/slot/common/Squat.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/behaviors/slot/r0/Get_Up_Back.xml b/behaviors/slot/r0/Get_Up_Back.xml new file mode 100644 index 0000000..329871e --- /dev/null +++ b/behaviors/slot/r0/Get_Up_Back.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/r0/Get_Up_Front.xml b/behaviors/slot/r0/Get_Up_Front.xml new file mode 100644 index 0000000..f3203c1 --- /dev/null +++ b/behaviors/slot/r0/Get_Up_Front.xml @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/r0/Kick_Motion.xml b/behaviors/slot/r0/Kick_Motion.xml new file mode 100644 index 0000000..931d8c0 --- /dev/null +++ b/behaviors/slot/r0/Kick_Motion.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/behaviors/slot/r1/Get_Up_Back.xml b/behaviors/slot/r1/Get_Up_Back.xml new file mode 100644 index 0000000..0f0243f --- /dev/null +++ b/behaviors/slot/r1/Get_Up_Back.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/r1/Get_Up_Front.xml b/behaviors/slot/r1/Get_Up_Front.xml new file mode 100644 index 0000000..f2d9ac1 --- /dev/null +++ b/behaviors/slot/r1/Get_Up_Front.xml @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/r1/Kick_Motion.xml b/behaviors/slot/r1/Kick_Motion.xml new file mode 100644 index 0000000..e671362 --- /dev/null +++ b/behaviors/slot/r1/Kick_Motion.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/behaviors/slot/r2/Get_Up_Back.xml b/behaviors/slot/r2/Get_Up_Back.xml new file mode 100644 index 0000000..b3602ce --- /dev/null +++ b/behaviors/slot/r2/Get_Up_Back.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/r2/Get_Up_Front.xml b/behaviors/slot/r2/Get_Up_Front.xml new file mode 100644 index 0000000..2c0c096 --- /dev/null +++ b/behaviors/slot/r2/Get_Up_Front.xml @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/r2/Kick_Motion.xml b/behaviors/slot/r2/Kick_Motion.xml new file mode 100644 index 0000000..8ce996c --- /dev/null +++ b/behaviors/slot/r2/Kick_Motion.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/behaviors/slot/r3/Get_Up_Back.xml b/behaviors/slot/r3/Get_Up_Back.xml new file mode 100644 index 0000000..1ea2a3f --- /dev/null +++ b/behaviors/slot/r3/Get_Up_Back.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/r3/Get_Up_Front.xml b/behaviors/slot/r3/Get_Up_Front.xml new file mode 100644 index 0000000..14b73a4 --- /dev/null +++ b/behaviors/slot/r3/Get_Up_Front.xml @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/r3/Kick_Motion.xml b/behaviors/slot/r3/Kick_Motion.xml new file mode 100644 index 0000000..17b0846 --- /dev/null +++ b/behaviors/slot/r3/Kick_Motion.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/behaviors/slot/r4/Get_Up_Back.xml b/behaviors/slot/r4/Get_Up_Back.xml new file mode 100644 index 0000000..0c36704 --- /dev/null +++ b/behaviors/slot/r4/Get_Up_Back.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/r4/Get_Up_Front.xml b/behaviors/slot/r4/Get_Up_Front.xml new file mode 100644 index 0000000..2c0c096 --- /dev/null +++ b/behaviors/slot/r4/Get_Up_Front.xml @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/behaviors/slot/r4/Kick_Motion.xml b/behaviors/slot/r4/Kick_Motion.xml new file mode 100644 index 0000000..418628f --- /dev/null +++ b/behaviors/slot/r4/Kick_Motion.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundle/bundle.sh b/bundle/bundle.sh new file mode 100755 index 0000000..c57f1cd --- /dev/null +++ b/bundle/bundle.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# Call this script from any directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +# cd to main folder +cd "${SCRIPT_DIR}/.." + +rm -rf ./bundle/build +rm -rf ./bundle/dist + +onefile="--onefile" + +# bundle app, dependencies and data files into single executable + +pyinstaller \ +--add-data './world/commons/robots:world/commons/robots' \ +--add-data './behaviors/slot/common:behaviors/slot/common' \ +--add-data './behaviors/slot/r0:behaviors/slot/r0' \ +--add-data './behaviors/slot/r1:behaviors/slot/r1' \ +--add-data './behaviors/slot/r2:behaviors/slot/r2' \ +--add-data './behaviors/slot/r3:behaviors/slot/r3' \ +--add-data './behaviors/slot/r4:behaviors/slot/r4' \ +--add-data './behaviors/custom/Dribble/*.pkl:behaviors/custom/Dribble' \ +--add-data './behaviors/custom/Walk/*.pkl:behaviors/custom/Walk' \ +--add-data './behaviors/custom/Fall/*.pkl:behaviors/custom/Fall' \ +${onefile} --distpath ./bundle/dist/ --workpath ./bundle/build/ --noconfirm --name fcp Run_Player.py + +# start.sh + +cat > ./bundle/dist/start.sh << EOF +#!/bin/bash +export OMP_NUM_THREADS=1 + +host=\${1:-localhost} +port=\${2:-3100} + +for i in {1..11}; do + ./fcp -i \$host -p \$port -u \$i -t FCPortugal & +done +EOF + +# start_penalty.sh + +cat > ./bundle/dist/start_penalty.sh << EOF +#!/bin/bash +export OMP_NUM_THREADS=1 + +host=\${1:-localhost} +port=\${2:-3100} + +./fcp -i \$host -p \$port -u 1 -t FCPortugal -P 1 & +./fcp -i \$host -p \$port -u 11 -t FCPortugal -P 1 & +EOF + +# start_fat_proxy.sh + +cat > ./bundle/dist/start_fat_proxy.sh << EOF +#!/bin/bash +export OMP_NUM_THREADS=1 + +host=\${1:-localhost} +port=\${2:-3100} + +for i in {1..11}; do + ./fcp -i \$host -p \$port -u \$i -t FCPortugal -F 1 & +done +EOF + +# kill.sh + +cat > ./bundle/dist/kill.sh << EOF +#!/bin/bash +pkill -9 -e fcp +EOF + +# execution permission + +chmod a+x ./bundle/dist/start.sh +chmod a+x ./bundle/dist/start_penalty.sh +chmod a+x ./bundle/dist/start_fat_proxy.sh +chmod a+x ./bundle/dist/kill.sh + diff --git a/communication/Radio.py b/communication/Radio.py new file mode 100644 index 0000000..83adaf6 --- /dev/null +++ b/communication/Radio.py @@ -0,0 +1,306 @@ +from typing import List +from world.commons.Other_Robot import Other_Robot +from world.World import World +import numpy as np + +class Radio(): + ''' + map limits are hardcoded: + teammates/opponents positions (x,y) in ([-16,16],[-11,11]) + ball position (x,y) in ([-15,15],[-10,10]) + known server limitations: + claimed: all ascii from 0x20 to 0x7E except ' ', '(', ')' + bugs: + - ' or " clip the message + - '\' at the end or near another '\' + - ';' at beginning of message + ''' + # map limits are hardcoded: + + # lines, columns, half lines index, half cols index, (lines-1)/x_span, (cols-1)/y_span, combinations, combinations*2states, + TP = 321,221,160,110,10, 10,70941,141882 # teammate position + OP = 201,111,100,55, 6.25,5, 22311,44622 # opponent position + BP = 301,201,150,100,10, 10,60501 # ball position + SYMB = "!#$%&*+,-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~;" + SLEN = len(SYMB) + SYMB_TO_IDX = {ord(s):i for i,s in enumerate(SYMB)} + + + def __init__(self, world : World, commit_announcement) -> None: + self.world = world + self.commit_announcement = commit_announcement + r = world.robot + t = world.teammates + o = world.opponents + self.groups = ( # player team/unum, group has ball?, self in group? + [(t[9],t[10],o[6],o[7],o[8],o[9],o[10]), True ], # 2 teammates, 5 opponents, ball + [(t[0],t[1], t[2],t[3],t[4],t[5],t[6] ), False], # 7 teammates + [(t[7],t[8], o[0],o[1],o[2],o[3],o[4],o[5]), False] # 2 teammates, 6 opponents + ) + for g in self.groups: # add 'self in group?' + g.append(any(i.is_self for i in g[0])) + + def get_player_combination(self, pos, is_unknown, is_down, info): + ''' Returns combination (0-based) and number of possible combinations ''' + + if is_unknown: + return info[7]+1, info[7]+2 # return unknown combination + + x,y = pos[:2] + + if x < -17 or x > 17 or y < -12 or y > 12: + return info[7], info[7]+2 # return out of bounds combination (if it exceeds 1m in any axis) + + # convert to int to avoid overflow later + l = int(np.clip( round(info[4]*x+info[2]), 0, info[0]-1 )) # absorb out of bounds positions (up to 1m in each axis) + c = int(np.clip( round(info[5]*y+info[3]), 0, info[1]-1 )) + + return (l*info[1]+c)+(info[6] if is_down else 0), info[7]+2 # return valid combination + + + def get_ball_combination(self, x, y): + ''' Returns combination (0-based) and number of possible combinations ''' + + # if ball is out of bounds, we force it in + l = int(np.clip( round(Radio.BP[4]*x+Radio.BP[2]), 0, Radio.BP[0]-1 )) + c = int(np.clip( round(Radio.BP[5]*y+Radio.BP[3]), 0, Radio.BP[1]-1 )) + + return l*Radio.BP[1]+c, Radio.BP[6] # return valid combination + + def get_ball_position(self,comb): + + l = comb // Radio.BP[1] + c = comb % Radio.BP[1] + + return np.array([l/Radio.BP[4]-15, c/Radio.BP[5]-10, 0.042]) # assume ball is on ground + + def get_player_position(self,comb, info): + + if comb == info[7]: return -1 # player is out of bounds + if comb == info[7]+1: return -2 # player is in unknown location + + is_down = comb >= info[6] + if is_down: + comb -= info[6] + + l = comb // info[1] + c = comb % info[1] + + return l/info[4]-16, c/info[5]-11, is_down + + + def check_broadcast_requirements(self): + ''' + Check if broadcast group is valid + + Returns + ------- + ready : bool + True if all requirements are met + + Sequence: g0,g1,g2, ig0,ig1,ig2, iig0,iig1,iig2 (whole cycle: 0.36s) + igx means 'incomplete group', where <=1 element can be MIA recently + iigx means 'very incomplete group', where <=2 elements can be MIA recently + Rationale: prevent incomplete messages from monopolizing the broadcast space + + However: + - 1st round: when 0 group members are missing, that group will update 3 times every 0.36s + - 2nd round: when 1 group member is recently missing, that group will update 2 times every 0.36s + - 3rd round: when 2 group members are recently missing, that group will update 1 time every 0.36s + - when >2 group members are recently missing, that group will not be updated + + Players that have never been seen or heard are not considered for the 'recently missing'. + If there is only 1 group member since the beginning, the respective group can be updated, except in the 1st round. + In this way, the 1st round cannot be monopolized by clueless agents, which is important during games with 22 players. + ''' + + w = self.world + r = w.robot + ago40ms = w.time_local_ms - 40 + ago370ms = w.time_local_ms - 370 # maximum delay (up to 2 MIAs) is 360ms because radio has a delay of 20ms (otherwise max delay would be 340ms) + group : List[Other_Robot] + + idx9 = int((w.time_server * 25)+0.1) % 9 # sequence of 9 phases + max_MIA = idx9 // 3 # maximum number of MIA players (based on server time) + group_idx = idx9 % 3 # group number (based on server time) + group, has_ball, is_self_included = self.groups[group_idx] + + #============================================ 0. check if group is valid + + if has_ball and w.ball_abs_pos_last_update < ago40ms: # Ball is included and not up to date + return False + + if is_self_included and r.loc_last_update < ago40ms: # Oneself is included and unable to self-locate + return False + + # Get players that have been previously seen or heard but not recently + MIAs = [not ot.is_self and ot.state_last_update < ago370ms and ot.state_last_update > 0 for ot in group] + self.MIAs = [ot.state_last_update == 0 or MIAs[i] for i,ot in enumerate(group)] # add players that have never been seen + + if sum(MIAs) > max_MIA: # checking if number of recently missing members is not above threshold + return False + + # never seen before players are always ignored except when: + # - this is the 0 MIAs round (see explanation above) + # - all are MIA + if (max_MIA == 0 and any(self.MIAs)) or all(self.MIAs): + return False + + # Check for invalid members. Conditions: + # - Player is other and not MIA and: + # - last update was >40ms ago OR + # - last update did not include the head (head is important to provide state and accurate position) + + if any( + (not ot.is_self and not self.MIAs[i] and + (ot.state_last_update < ago40ms or ot.state_last_update==0 or len(ot.state_abs_pos)<3)# (last update: has no head or is old) + ) for i,ot in enumerate(group) + ): + return False + + return True + + + def broadcast(self): + ''' + Commit messages to teammates if certain conditions are met + Messages contain: positions/states of every moving entity + ''' + + if not self.check_broadcast_requirements(): + return + + w = self.world + ot : Other_Robot + + group_idx = int((w.time_server * 25)+0.1) % 3 # group number based on server time + group, has_ball, _ = self.groups[group_idx] + + #============================================ 1. create combination + + # add message number + combination = group_idx + no_of_combinations = 3 + + # add ball combination + if has_ball: + c, n = self.get_ball_combination(w.ball_abs_pos[0], w.ball_abs_pos[1]) + combination += c * no_of_combinations + no_of_combinations *= n + + + # add group combinations + for i,ot in enumerate(group): + c, n = self.get_player_combination(ot.state_abs_pos, # player position + self.MIAs[i], ot.state_fallen, # is unknown, is down + Radio.TP if ot.is_teammate else Radio.OP) # is teammate + combination += c * no_of_combinations + no_of_combinations *= n + + + assert(no_of_combinations < 9.61e38) # 88*89^19 (first character cannot be ';') + + #============================================ 2. create message + + # 1st msg symbol: ignore ';' due to server bug + msg = Radio.SYMB[combination % (Radio.SLEN-1)] + combination //= (Radio.SLEN-1) + + # following msg symbols + while combination: + msg += Radio.SYMB[combination % Radio.SLEN] + combination //= Radio.SLEN + + #============================================ 3. commit message + + self.commit_announcement(msg.encode()) # commit message + + + def receive(self, msg:bytearray): + w = self.world + r = w.robot + ago40ms = w.time_local_ms - 40 + ago110ms = w.time_local_ms - 110 + msg_time = w.time_local_ms - 20 # message was sent in the last step + + #============================================ 1. get combination + + # read first symbol, which cannot be ';' due to server bug + combination = Radio.SYMB_TO_IDX[msg[0]] + total_combinations = Radio.SLEN-1 + + if len(msg)>1: + for m in msg[1:]: + combination += total_combinations * Radio.SYMB_TO_IDX[m] + total_combinations *= Radio.SLEN + + #============================================ 2. get msg ID + + message_no = combination % 3 + combination //= 3 + group, has_ball, _ = self.groups[message_no] + + #============================================ 3. get data + + if has_ball: + ball_comb = combination % Radio.BP[6] + combination //= Radio.BP[6] + + players_combs = [] + ot : Other_Robot + for ot in group: + info = Radio.TP if ot.is_teammate else Radio.OP + players_combs.append( combination % (info[7]+2) ) + combination //= info[7]+2 + + #============================================ 4. update world + + if has_ball and w.ball_abs_pos_last_update < ago40ms: # update ball if it was not seen + time_diff = (msg_time - w.ball_abs_pos_last_update) / 1000 + ball = self.get_ball_position(ball_comb) + w.ball_abs_vel = (ball - w.ball_abs_pos) / time_diff + w.ball_abs_speed = np.linalg.norm(w.ball_abs_vel) + w.ball_abs_pos_last_update = msg_time # (error: 0-40 ms) + w.ball_abs_pos = ball + w.is_ball_abs_pos_from_vision = False + + for c, ot in zip(players_combs, group): + + # handle oneself case + if ot.is_self: + # the ball's position has a fair amount of noise, whether seen by us or other players + # but our self-locatization mechanism is usually much better than how others perceive us + if r.loc_last_update < ago110ms: # so we wait until we miss 2 visual steps + data = self.get_player_position(c, Radio.TP) + if type(data)==tuple: + x,y,is_down = data + r.loc_head_position[:2] = x,y # z is kept unchanged + r.loc_head_position_last_update = msg_time + r.radio_fallen_state = is_down + r.radio_last_update = msg_time + continue + + # do not update if other robot was recently seen + if ot.state_last_update >= ago40ms: + continue + + info = Radio.TP if ot.is_teammate else Radio.OP + data = self.get_player_position(c, info) + if type(data)==tuple: + x,y,is_down = data + p = np.array([x,y]) + + if ot.state_abs_pos is not None: # update the x & y components of the velocity + time_diff = (msg_time - ot.state_last_update) / 1000 + velocity = np.append( (p - ot.state_abs_pos[:2]) / time_diff, 0) # v.z = 0 + vel_diff = velocity - ot.state_filtered_velocity + if np.linalg.norm(vel_diff) < 4: # otherwise assume it was beamed + ot.state_filtered_velocity /= (ot.vel_decay,ot.vel_decay,1) # neutralize decay (except in the z-axis) + ot.state_filtered_velocity += ot.vel_filter * vel_diff + + ot.state_fallen = is_down + ot.state_last_update = msg_time + ot.state_body_parts_abs_pos = {"head":p} + ot.state_abs_pos = p + ot.state_horizontal_dist = np.linalg.norm(p - r.loc_head_position[:2]) + ot.state_ground_area = (p, 0.3 if is_down else 0.2) # not very precise, but we cannot see the robot \ No newline at end of file diff --git a/communication/Server_Comm.py b/communication/Server_Comm.py new file mode 100644 index 0000000..02d8c17 --- /dev/null +++ b/communication/Server_Comm.py @@ -0,0 +1,285 @@ +from communication.World_Parser import World_Parser +from itertools import count +from select import select +from sys import exit +from world.World import World +import socket +import time + +class Server_Comm(): + monitor_socket = None + + def __init__(self, host:str, agent_port:int, monitor_port:int, unum:int, robot_type:int, team_name:str, + world_parser:World_Parser, world:World, other_players, wait_for_server=True) -> None: + + self.BUFFER_SIZE = 8192 + self.rcv_buff = bytearray(self.BUFFER_SIZE) + self.send_buff = [] + self.world_parser = world_parser + self.unum = unum + + # During initialization, it's not clear whether we are on the left or right side + self._unofficial_beam_msg_left = "(agent (unum " + str(unum) + ") (team Left) (move " + self._unofficial_beam_msg_right = "(agent (unum " + str(unum) + ") (team Right) (move " + self.world = world + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM ) + + if wait_for_server: print("Waiting for server at ", host, ":", agent_port, sep="",end=".",flush=True) + while True: + try: + self.socket.connect((host, agent_port)) + print(end=" ") + break + except ConnectionRefusedError: + if not wait_for_server: + print("Server is down. Closing...") + exit() + time.sleep(1) + print(".",end="",flush=True) + print("Connected agent", unum, self.socket.getsockname()) + + self.send_immediate(b'(scene rsg/agent/nao/nao_hetero.rsg ' + str(robot_type).encode() + b')') + self._receive_async(other_players, True) + + self.send_immediate(b'(init (unum '+ str(unum).encode() + b') (teamname '+ team_name.encode() + b'))') + self._receive_async(other_players, False) + + # Repeat to guarantee that team side information is received + for _ in range(3): + # Eliminate advanced step by changing syn order (rcssserver3d protocol bug, usually seen for player 11) + self.send_immediate(b'(syn)') #if this syn is not needed, it will be discarded by the server + for p in other_players: + p.scom.send_immediate(b'(syn)') + for p in other_players: + p.scom.receive() + self.receive() + + if world.team_side_is_left == None: + print("\nError: server did not return a team side! Check server terminal!") + exit() + + # Monitor socket is shared by all agents on the same thread + if Server_Comm.monitor_socket is None and monitor_port is not None: + print("Connecting to server's monitor port at ", host, ":", monitor_port, sep="",end=".",flush=True) + Server_Comm.monitor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM ) + Server_Comm.monitor_socket.connect((host, monitor_port)) + print("Done!") + + + + def _receive_async(self, other_players, first_pass) -> None: + '''Private function that receives asynchronous information during the initialization''' + + if not other_players: + self.receive() + return + + self.socket.setblocking(0) + if first_pass: print("Async agent",self.unum,"initialization", end="", flush=True) + + while True: + try: + print(".",end="",flush=True) + self.receive() + break + except: + pass + for p in other_players: + p.scom.send_immediate(b'(syn)') + for p in other_players: + p.scom.receive() + + self.socket.setblocking(1) + if not first_pass: print("Done!") + + + def receive(self, update=True): + + for i in count(): # parse all messages and perform value updates, but heavy computation is only done once at the end + try: + if self.socket.recv_into(self.rcv_buff, nbytes=4) != 4: raise ConnectionResetError() + msg_size = int.from_bytes(self.rcv_buff[:4], byteorder='big', signed=False) + if self.socket.recv_into(self.rcv_buff, nbytes=msg_size, flags=socket.MSG_WAITALL) != msg_size: raise ConnectionResetError() + except ConnectionResetError: + print("\nError: socket was closed by rcssserver3d!") + exit() + + self.world_parser.parse(self.rcv_buff[:msg_size]) + if len(select([self.socket],[],[], 0.0)[0]) == 0: break + + if update: + if i==1: self.world.log( "Server_Comm.py: The agent lost 1 packet! Is syncmode enabled?") + if i>1: self.world.log(f"Server_Comm.py: The agent lost {i} consecutive packets! Is syncmode disabled?") + self.world.update() + + if len(select([self.socket],[],[], 0.0)[0]) != 0: + self.world.log("Server_Comm.py: Received a new packet while on world.update()!") + self.receive() + + + def send_immediate(self, msg:bytes) -> None: + ''' Commit and send immediately ''' + try: + self.socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg ) #Add message length in the first 4 bytes + except BrokenPipeError: + print("\nError: socket was closed by rcssserver3d!") + exit() + + + def send(self) -> None: + ''' Send all committed messages ''' + if len(select([self.socket],[],[], 0.0)[0]) == 0: + self.send_buff.append(b'(syn)') + self.send_immediate( b''.join(self.send_buff) ) + else: + self.world.log("Server_Comm.py: Received a new packet while thinking!") + self.send_buff = [] #clear buffer + + def commit(self, msg:bytes) -> None: + assert type(msg) == bytes, "Message must be of type Bytes!" + self.send_buff.append(msg) + + def commit_and_send(self, msg:bytes = b'') -> None: + self.commit(msg) + self.send() + + def clear_buffer(self) -> None: + self.send_buff = [] + + def commit_announcement(self, msg:bytes) -> None: + ''' + Say something to every player on the field. + Maximum 20 characters, ascii between 0x20, 0x7E except ' ', '(', ')' + Accepted: letters+numbers+symbols: !"#$%&'*+,-./:;<=>?@[\]^_`{|}~ + Message range: 50m (the field is 36m diagonally, so ignore this limitation) + A player can only hear a teammate's message every 2 steps (0.04s) + This ability exists independetly for messages from both teams + (i.e. our team cannot spam the other team to block their messages) + Messages from oneself are always heard + ''' + assert len(msg) <= 20 and type(msg) == bytes + self.commit(b'(say ' + msg + b')') + + def commit_pass_command(self) -> None: + ''' + Issue a pass command: + Conditions: + - The current playmode is PlayOn + - The agent is near the ball (default 0.5m) + - No opponents are near the ball (default 1m) + - The ball is stationary (default <0.05m/s) + - A certain amount of time has passed between pass commands + ''' + self.commit(b'(pass)') + + def commit_beam(self, pos2d, rot) -> None: + ''' + Official beam command that can be used in-game + This beam is affected by noise (unless it is disabled in the server configuration) + + Parameters + ---------- + pos2d : array_like + Absolute 2D position (negative X is always our half of the field, no matter our side) + rot : `int`/`float` + Player angle in degrees (0 points forward) + ''' + assert len(pos2d)==2, "The official beam command accepts only 2D positions!" + self.commit( f"(beam {pos2d[0]} {pos2d[1]} {rot})".encode() ) + + + def unofficial_beam(self, pos3d, rot) -> None: + ''' + Unofficial beam - it cannot be used in official matches + + Parameters + ---------- + pos3d : array_like + Absolute 3D position (negative X is always our half of the field, no matter our side) + rot : `int`/`float` + Player angle in degrees (0 points forward) + ''' + assert len(pos3d)==3, "The unofficial beam command accepts only 3D positions!" + + # there is no need to normalize the angle, the server accepts any angle + if self.world.team_side_is_left: + msg = f"{self._unofficial_beam_msg_left }{ pos3d[0]} { pos3d[1]} {pos3d[2]} {rot-90}))".encode() + else: + msg = f"{self._unofficial_beam_msg_right}{-pos3d[0]} {-pos3d[1]} {pos3d[2]} {rot+90}))".encode() + + self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg ) + + def unofficial_kill_sim(self) -> None: + ''' Unofficial kill simulator command ''' + msg = b'(killsim)' + self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg ) + + def unofficial_move_ball(self, pos3d, vel3d=(0,0,0)) -> None: + ''' + Unofficial command to move ball + info: ball radius = 0.042m + + Parameters + ---------- + pos3d : array_like + Absolute 3D position (negative X is always our half of the field, no matter our side) + vel3d : array_like + Absolute 3D velocity (negative X is always our half of the field, no matter our side) + ''' + assert len(pos3d)==3 and len(vel3d)==3, "To move the ball we need a 3D position and velocity" + + if self.world.team_side_is_left: + msg = f"(ball (pos { pos3d[0]} { pos3d[1]} {pos3d[2]}) (vel { vel3d[0]} { vel3d[1]} {vel3d[2]}))".encode() + else: + msg = f"(ball (pos {-pos3d[0]} {-pos3d[1]} {pos3d[2]}) (vel {-vel3d[0]} {-vel3d[1]} {vel3d[2]}))".encode() + + self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg ) + + def unofficial_set_game_time(self, time_in_s : float) -> None: + ''' + Unofficial command to set the game time + e.g. unofficial_set_game_time(68.78) + + Parameters + ---------- + time_in_s : float + Game time in seconds + ''' + msg = f"(time {time_in_s})".encode() + self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg ) + + def unofficial_set_play_mode(self, play_mode : str) -> None: + ''' + Unofficial command to set the play mode + e.g. unofficial_set_play_mode("PlayOn") + + Parameters + ---------- + play_mode : str + Play mode + ''' + msg = f"(playMode {play_mode})".encode() + self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg ) + + def unofficial_kill_player(self, unum : int, team_side_is_left : bool) -> None: + ''' + Unofficial command to kill specific player + + Parameters + ---------- + unum : int + Uniform number + team_side_is_left : bool + True if player to kill belongs to left team + ''' + msg = f"(kill (unum {unum}) (team {'Left' if team_side_is_left else 'Right'}))".encode() + self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg ) + + def close(self, close_monitor_socket = False): + ''' Close agent socket, and optionally the monitor socket (shared by players running on the same thread) ''' + self.socket.close() + if close_monitor_socket and Server_Comm.monitor_socket is not None: + Server_Comm.monitor_socket.close() + Server_Comm.monitor_socket = None + diff --git a/communication/World_Parser.py b/communication/World_Parser.py new file mode 100644 index 0000000..5359a3e --- /dev/null +++ b/communication/World_Parser.py @@ -0,0 +1,430 @@ +from math_ops.Math_Ops import Math_Ops as M +from world.Robot import Robot +from world.World import World +import math +import numpy as np + + +class World_Parser(): + def __init__(self, world:World, hear_callback) -> None: + self.LOG_PREFIX = "World_Parser.py: " + self.world = world + self.hear_callback = hear_callback + self.exp = None + self.depth = None + self.LEFT_SIDE_FLAGS = {b'F2L':(-15,-10,0), + b'F1L':(-15,+10,0), + b'F2R':(+15,-10,0), + b'F1R':(+15,+10,0), + b'G2L':(-15,-1.05,0.8), + b'G1L':(-15,+1.05,0.8), + b'G2R':(+15,-1.05,0.8), + b'G1R':(+15,+1.05,0.8)} #mapping between flag names and their corrected location, when playing on the left side + self.RIGHT_SIDE_FLAGS = {b'F2L':(+15,+10,0), + b'F1L':(+15,-10,0), + b'F2R':(-15,+10,0), + b'F1R':(-15,-10,0), + b'G2L':(+15,+1.05,0.8), + b'G1L':(+15,-1.05,0.8), + b'G2R':(-15,+1.05,0.8), + b'G1R':(-15,-1.05,0.8)} + self.play_mode_to_id = None + self.LEFT_PLAY_MODE_TO_ID = {"KickOff_Left":World.M_OUR_KICKOFF, "KickIn_Left":World.M_OUR_KICK_IN, "corner_kick_left":World.M_OUR_CORNER_KICK, + "goal_kick_left":World.M_OUR_GOAL_KICK, "free_kick_left":World.M_OUR_FREE_KICK, "pass_left":World.M_OUR_PASS, + "direct_free_kick_left": World.M_OUR_DIR_FREE_KICK, "Goal_Left": World.M_OUR_GOAL, "offside_left": World.M_OUR_OFFSIDE, + "KickOff_Right":World.M_THEIR_KICKOFF, "KickIn_Right":World.M_THEIR_KICK_IN, "corner_kick_right":World.M_THEIR_CORNER_KICK, + "goal_kick_right":World.M_THEIR_GOAL_KICK, "free_kick_right":World.M_THEIR_FREE_KICK, "pass_right":World.M_THEIR_PASS, + "direct_free_kick_right": World.M_THEIR_DIR_FREE_KICK, "Goal_Right": World.M_THEIR_GOAL, "offside_right": World.M_THEIR_OFFSIDE, + "BeforeKickOff": World.M_BEFORE_KICKOFF, "GameOver": World.M_GAME_OVER, "PlayOn": World.M_PLAY_ON } + self.RIGHT_PLAY_MODE_TO_ID = {"KickOff_Left":World.M_THEIR_KICKOFF, "KickIn_Left":World.M_THEIR_KICK_IN, "corner_kick_left":World.M_THEIR_CORNER_KICK, + "goal_kick_left":World.M_THEIR_GOAL_KICK, "free_kick_left":World.M_THEIR_FREE_KICK, "pass_left":World.M_THEIR_PASS, + "direct_free_kick_left": World.M_THEIR_DIR_FREE_KICK, "Goal_Left": World.M_THEIR_GOAL, "offside_left": World.M_THEIR_OFFSIDE, + "KickOff_Right":World.M_OUR_KICKOFF, "KickIn_Right":World.M_OUR_KICK_IN, "corner_kick_right":World.M_OUR_CORNER_KICK, + "goal_kick_right":World.M_OUR_GOAL_KICK, "free_kick_right":World.M_OUR_FREE_KICK, "pass_right":World.M_OUR_PASS, + "direct_free_kick_right": World.M_OUR_DIR_FREE_KICK, "Goal_Right": World.M_OUR_GOAL, "offside_right": World.M_OUR_OFFSIDE, + "BeforeKickOff": World.M_BEFORE_KICKOFF, "GameOver": World.M_GAME_OVER, "PlayOn": World.M_PLAY_ON } + + + def find_non_digit(self,start): + while True: + if (self.exp[start] < ord('0') or self.exp[start] > ord('9')) and self.exp[start] != ord('.'): return start + start+=1 + + def find_char(self,start,char): + while True: + if self.exp[start] == char : return start + start+=1 + + def read_float(self, start): + if self.exp[start:start+3] == b'nan': return float('nan'), start+3 #handle nan values (they exist) + end = self.find_non_digit(start+1) #we assume the first one is a digit or minus sign + try: + retval = float(self.exp[start:end]) + except: + self.world.log(f"{self.LOG_PREFIX}String to float conversion failed: {self.exp[start:end]} at msg[{start},{end}], \nMsg: {self.exp.decode()}") + retval = 0 + return retval, end + + def read_int(self, start): + end = self.find_non_digit(start+1) #we assume the first one is a digit or minus sign + return int(self.exp[start:end]), end + + def read_bytes(self, start): + end = start + while True: + if self.exp[end] == ord(' ') or self.exp[end] == ord(')'): break + end+=1 + + return self.exp[start:end], end + + def read_str(self, start): + b, end = self.read_bytes(start) + return b.decode(), end + + def get_next_tag(self, start): + min_depth = self.depth + while True: + if self.exp[start] == ord(")") : #monitor xml element depth + self.depth -= 1 + if min_depth > self.depth: min_depth = self.depth + elif self.exp[start] == ord("(") : break + start+=1 + if start >= len(self.exp): return None, start, 0 + + self.depth += 1 + start += 1 + end = self.find_char(start, ord(" ")) + return self.exp[start:end], end, min_depth + + + def parse(self, exp): + + self.exp = exp #used by other member functions + self.depth = 0 #xml element depth + self.world.step += 1 + self.world.line_count = 0 + self.world.robot.frp = dict() + self.world.flags_posts = dict() + self.world.flags_corners = dict() + self.world.vision_is_up_to_date = False + self.world.ball_is_visible = False + self.world.robot.feet_toes_are_touching = dict.fromkeys(self.world.robot.feet_toes_are_touching, False) + self.world.time_local_ms += World.STEPTIME_MS + + for p in self.world.teammates: p.is_visible = False + for p in self.world.opponents: p.is_visible = False + + tag, end, _ = self.get_next_tag(0) + + while end < len(exp): + + if tag==b'time': + while True: + tag, end, min_depth = self.get_next_tag(end) + if min_depth == 0: break + + if tag==b'now': + #last_time = self.world.time_server + self.world.time_server, end = self.read_float(end+1) + + #Test server time reliability + #increment = self.world.time_server - last_time + #if increment < 0.019: print ("down",last_time,self.world.time_server) + #if increment > 0.021: print ("up",last_time,self.world.time_server) + else: + self.world.log(f"{self.LOG_PREFIX}Unknown tag inside 'time': {tag} at {end}, \nMsg: {exp.decode()}") + + + elif tag==b'GS': + while True: + tag, end, min_depth = self.get_next_tag(end) + if min_depth == 0: break + + if tag==b'unum': + _, end = self.read_int(end+1) #We already know our unum + elif tag==b'team': + aux, end = self.read_str(end+1) + is_left = bool(aux == "left") + if self.world.team_side_is_left != is_left: + self.world.team_side_is_left = is_left + self.play_mode_to_id = self.LEFT_PLAY_MODE_TO_ID if is_left else self.RIGHT_PLAY_MODE_TO_ID + self.world.draw.set_team_side(not is_left) + self.world.team_draw.set_team_side(not is_left) + elif tag==b'sl': + if self.world.team_side_is_left: + self.world.goals_scored, end = self.read_int(end+1) + else: + self.world.goals_conceded, end = self.read_int(end+1) + elif tag==b'sr': + if self.world.team_side_is_left: + self.world.goals_conceded, end = self.read_int(end+1) + else: + self.world.goals_scored, end = self.read_int(end+1) + elif tag==b't': + self.world.time_game, end = self.read_float(end+1) + elif tag==b'pm': + aux, end = self.read_str(end+1) + if self.play_mode_to_id is not None: + self.world.play_mode = self.play_mode_to_id[aux] + else: + self.world.log(f"{self.LOG_PREFIX}Unknown tag inside 'GS': {tag} at {end}, \nMsg: {exp.decode()}") + + + elif tag==b'GYR': + while True: + tag, end, min_depth = self.get_next_tag(end) + if min_depth == 0: break + + ''' + The gyroscope measures the robot's torso angular velocity (rotation rate vector) + The angular velocity's orientation is given by the right-hand rule. + + Original reference frame: + X:left(-)/right(+) Y:back(-)/front(+) Z:down(-)/up(+) + + New reference frame: + X:back(-)/front(+) Y:right(-)/left(+) Z:down(-)/up(+) + + ''' + + if tag==b'n': + pass + elif tag==b'rt': + self.world.robot.gyro[1], end = self.read_float(end+1) + self.world.robot.gyro[0], end = self.read_float(end+1) + self.world.robot.gyro[2], end = self.read_float(end+1) + self.world.robot.gyro[1] *= -1 + else: + self.world.log(f"{self.LOG_PREFIX}Unknown tag inside 'GYR': {tag} at {end}, \nMsg: {exp.decode()}") + + + elif tag==b'ACC': + while True: + tag, end, min_depth = self.get_next_tag(end) + if min_depth == 0: break + + ''' + The accelerometer measures the acceleration relative to freefall. It will read zero during any type of free fall. + When at rest relative to the Earth's surface, it will indicate an upwards acceleration of 9.81m/s^2 (in SimSpark). + + Original reference frame: + X:left(-)/right(+) Y:back(-)/front(+) Z:down(-)/up(+) + + New reference frame: + X:back(-)/front(+) Y:right(-)/left(+) Z:down(-)/up(+) + ''' + + if tag==b'n': + pass + elif tag==b'a': + self.world.robot.acc[1], end = self.read_float(end+1) + self.world.robot.acc[0], end = self.read_float(end+1) + self.world.robot.acc[2], end = self.read_float(end+1) + self.world.robot.acc[1] *= -1 + else: + self.world.log(f"{self.LOG_PREFIX}Unknown tag inside 'ACC': {tag} at {end}, \nMsg: {exp.decode()}") + + + elif tag==b'HJ': + while True: + tag, end, min_depth = self.get_next_tag(end) + if min_depth == 0: break + + if tag==b'n': + joint_name, end = self.read_str(end+1) + joint_index = Robot.MAP_PERCEPTOR_TO_INDEX[joint_name] + elif tag==b'ax': + joint_angle, end = self.read_float(end+1) + + #Fix symmetry issues 2/4 (perceptors) + if joint_name in Robot.FIX_PERCEPTOR_SET: joint_angle = -joint_angle + + old_angle = self.world.robot.joints_position[joint_index] + self.world.robot.joints_speed[joint_index] = (joint_angle - old_angle) / World.STEPTIME * math.pi / 180 + self.world.robot.joints_position[joint_index] = joint_angle + else: + self.world.log(f"{self.LOG_PREFIX}Unknown tag inside 'HJ': {tag} at {end}, \nMsg: {exp.decode()}") + + elif tag==b'FRP': + while True: + tag, end, min_depth = self.get_next_tag(end) + if min_depth == 0: break + + ''' + The reference frame is used for the contact point and force vector applied to that point + Note: The force vector is applied to the foot, so it usually points up + + Original reference frame: + X:left(-)/right(+) Y:back(-)/front(+) Z:down(-)/up(+) + + New reference frame: + X:back(-)/front(+) Y:right(-)/left(+) Z:down(-)/up(+) + + ''' + + if tag==b'n': + foot_toe_id, end = self.read_str(end+1) + self.world.robot.frp[foot_toe_id] = foot_toe_ref = np.empty(6) + self.world.robot.feet_toes_last_touch[foot_toe_id] = self.world.time_local_ms + self.world.robot.feet_toes_are_touching[foot_toe_id] = True + elif tag==b'c': + foot_toe_ref[1], end = self.read_float(end+1) + foot_toe_ref[0], end = self.read_float(end+1) + foot_toe_ref[2], end = self.read_float(end+1) + foot_toe_ref[1] *= -1 + elif tag==b'f': + foot_toe_ref[4], end = self.read_float(end+1) + foot_toe_ref[3], end = self.read_float(end+1) + foot_toe_ref[5], end = self.read_float(end+1) + foot_toe_ref[4] *= -1 + else: + self.world.log(f"{self.LOG_PREFIX}Unknown tag inside 'FRP': {tag} at {end}, \nMsg: {exp.decode()}") + + + elif tag==b'See': + self.world.vision_is_up_to_date = True + self.world.vision_last_update = self.world.time_local_ms + + while True: + tag, end, min_depth = self.get_next_tag(end) + if min_depth == 0: break + + tag_bytes = bytes(tag) #since bytearray is not hashable, it cannot be used as key for dictionaries + + if tag==b'G1R' or tag==b'G2R' or tag==b'G1L' or tag==b'G2L': + _, end, _ = self.get_next_tag(end) + + c1, end = self.read_float(end+1) + c2, end = self.read_float(end+1) + c3, end = self.read_float(end+1) + + aux = self.LEFT_SIDE_FLAGS[tag_bytes] if self.world.team_side_is_left else self.RIGHT_SIDE_FLAGS[tag_bytes] + self.world.flags_posts[aux] = (c1,c2,c3) + + elif tag==b'F1R' or tag==b'F2R' or tag==b'F1L' or tag==b'F2L': + _, end, _ = self.get_next_tag(end) + + c1, end = self.read_float(end+1) + c2, end = self.read_float(end+1) + c3, end = self.read_float(end+1) + + aux = self.LEFT_SIDE_FLAGS[tag_bytes] if self.world.team_side_is_left else self.RIGHT_SIDE_FLAGS[tag_bytes] + self.world.flags_corners[aux] = (c1,c2,c3) + + elif tag==b'B': + _, end, _ = self.get_next_tag(end) + + self.world.ball_rel_head_sph_pos[0], end = self.read_float(end+1) + self.world.ball_rel_head_sph_pos[1], end = self.read_float(end+1) + self.world.ball_rel_head_sph_pos[2], end = self.read_float(end+1) + self.world.ball_rel_head_cart_pos = M.deg_sph2cart(self.world.ball_rel_head_sph_pos) + self.world.ball_is_visible = True + self.world.ball_last_seen = self.world.time_local_ms + + elif tag==b'mypos': + + self.world.robot.cheat_abs_pos[0], end = self.read_float(end+1) + self.world.robot.cheat_abs_pos[1], end = self.read_float(end+1) + self.world.robot.cheat_abs_pos[2], end = self.read_float(end+1) + + elif tag==b'myorien': + + self.world.robot.cheat_ori, end = self.read_float(end+1) + + elif tag==b'ballpos': + + c1, end = self.read_float(end+1) + c2, end = self.read_float(end+1) + c3, end = self.read_float(end+1) + + self.world.ball_cheat_abs_vel[0] = (c1 - self.world.ball_cheat_abs_pos[0]) / World.VISUALSTEP + self.world.ball_cheat_abs_vel[1] = (c2 - self.world.ball_cheat_abs_pos[1]) / World.VISUALSTEP + self.world.ball_cheat_abs_vel[2] = (c3 - self.world.ball_cheat_abs_pos[2]) / World.VISUALSTEP + + self.world.ball_cheat_abs_pos[0] = c1 + self.world.ball_cheat_abs_pos[1] = c2 + self.world.ball_cheat_abs_pos[2] = c3 + + elif tag==b'P': + + while True: + previous_depth = self.depth + previous_end = end + tag, end, min_depth = self.get_next_tag(end) + if min_depth < 2: #if =1 we are still inside 'See', if =0 we are already outside 'See' + end = previous_end #The "P" tag is special because it's the only variable particle inside 'See' + self.depth = previous_depth + break # we restore the previous tag, and let 'See' handle it + + if tag==b'team': + player_team, end = self.read_str(end+1) + is_teammate = bool(player_team == self.world.team_name) + if self.world.team_name_opponent is None and not is_teammate: #register opponent team name + self.world.team_name_opponent = player_team + elif tag==b'id': + player_id, end = self.read_int(end+1) + player = self.world.teammates[player_id-1] if is_teammate else self.world.opponents[player_id-1] + player.body_parts_cart_rel_pos = dict() #reset seen body parts + player.is_visible = True + elif tag==b'llowerarm' or tag==b'rlowerarm' or tag==b'lfoot' or tag==b'rfoot' or tag==b'head': + tag_str = tag.decode() + _, end, _ = self.get_next_tag(end) + + c1, end = self.read_float(end+1) + c2, end = self.read_float(end+1) + c3, end = self.read_float(end+1) + + if is_teammate: + self.world.teammates[player_id-1].body_parts_sph_rel_pos[tag_str] = (c1,c2,c3) + self.world.teammates[player_id-1].body_parts_cart_rel_pos[tag_str] = M.deg_sph2cart((c1,c2,c3)) + else: + self.world.opponents[player_id-1].body_parts_sph_rel_pos[tag_str] = (c1,c2,c3) + self.world.opponents[player_id-1].body_parts_cart_rel_pos[tag_str] = M.deg_sph2cart((c1,c2,c3)) + else: + self.world.log(f"{self.LOG_PREFIX}Unknown tag inside 'P': {tag} at {end}, \nMsg: {exp.decode()}") + + elif tag==b'L': + l = self.world.lines[self.world.line_count] + + _, end, _ = self.get_next_tag(end) + l[0], end = self.read_float(end+1) + l[1], end = self.read_float(end+1) + l[2], end = self.read_float(end+1) + _, end, _ = self.get_next_tag(end) + l[3], end = self.read_float(end+1) + l[4], end = self.read_float(end+1) + l[5], end = self.read_float(end+1) + + if np.isnan(l).any(): + self.world.log(f"{self.LOG_PREFIX}Received field line with NaNs {l}") + else: + self.world.line_count += 1 #accept field line if there are no NaNs + + else: + self.world.log(f"{self.LOG_PREFIX}Unknown tag inside 'see': {tag} at {end}, \nMsg: {exp.decode()}") + + + elif tag==b'hear': + + team_name, end = self.read_str(end+1) + + if team_name == self.world.team_name: # discard message if it's not from our team + + timestamp, end = self.read_float(end+1) + + if self.exp[end+1] == ord('s'): # this message was sent by oneself + direction, end = "self", end+5 + else: # this message was sent by teammate + direction, end = self.read_float(end+1) + + msg, end = self.read_bytes(end+1) + self.hear_callback(msg, direction, timestamp) + + + tag, end, _ = self.get_next_tag(end) + + + else: + self.world.log(f"{self.LOG_PREFIX}Unknown root tag: {tag} at {end}, \nMsg: {exp.decode()}") + tag, end, min_depth = self.get_next_tag(end) + diff --git a/cpp/a_star/Makefile b/cpp/a_star/Makefile new file mode 100644 index 0000000..d408f5f --- /dev/null +++ b/cpp/a_star/Makefile @@ -0,0 +1,14 @@ +src = $(wildcard *.cpp) +obj = $(src:.c=.o) + +CFLAGS = -O3 -shared -std=c++11 -fPIC -Wall $(PYBIND_INCLUDES) + +all: $(obj) + g++ $(CFLAGS) -o a_star.so $^ + +debug: $(filter-out lib_main.cpp,$(obj)) + g++ -O0 -std=c++14 -Wall -g -o debug.bin debug_main.cc $^ + +.PHONY: clean +clean: + rm -f $(obj) all diff --git a/cpp/a_star/a_star.cpp b/cpp/a_star/a_star.cpp new file mode 100644 index 0000000..969c6b1 --- /dev/null +++ b/cpp/a_star/a_star.cpp @@ -0,0 +1,918 @@ +#include "a_star.h" +#include "expansion_groups.h" +#include +#include +#include +#define SQRT2 1.414213562373095f +#define LINES 321 +#define COLS 221 +#define MAX_RADIUS 5 // obstacle max radius in meters +#define IN_GOAL_LINE 312 // target line when go_to_goal is 'true' (312 -> 15.2m) +using std::chrono::high_resolution_clock; +using std::chrono::duration_cast; +using std::chrono::microseconds; + +/* +Map dimensions: 32m*22m +col 0 ... col 220 + line 0 + --- our goal --- line 1 + | | line 2 + | | line 3 + |--------------| ... + | | line 317 + | | line 318 + -- their goal -- line 319 + line 320 + +[(H)ard wall: -3, (S)oft wall: -2, (E)mpty: 0, 0 < Cost < inf] +*/ + +// build board cost statically +#define H27 -3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3 +#define S11 -2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2 +#define S19 S11,-2,-2,-2,-2,-2,-2,-2,-2 +#define S97 S11,S11,S11,S11,S11,S11,S11,S11,-2,-2,-2,-2,-2,-2,-2,-2,-2 +#define S98 S11,S11,S11,S11,S11,S11,S11,S11,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2 +#define S221 S98,S98,S11,S11,-2,-2,-2 +#define E19 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +#define E197 E19,E19,E19,E19,E19,E19,E19,E19,E19,E19,0,0,0,0,0,0,0 +#define L0 S221 // Line 0: soft W +#define L0_1 L0,L0 +#define L2 S97,H27,S97 // Line 2: soft W, (goal post, back net, goal post), soft W +#define L2_5 L2,L2,L2,L2 +#define L6 S98,-3,-3,-3,S19,-3,-3,-3,S98 // Line 6: soft W, goal post, soft W, goal post, soft W +#define L6_10 L6,L6,L6,L6,L6 +#define L11 S98,-2,-3,-3,S19,-3,-3,-2,S98 // Line 11:soft W, empty field, goal post, soft W, goal post,empty field, soft W +#define L12 S11,-2,E197,-2,S11 // Line 12:soft W, empty field, soft W + +#define L12x33 L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12,L12 +#define LIN12_308 L12x33,L12x33,L12x33,L12x33,L12x33,L12x33,L12x33,L12x33,L12x33 + +#define L309 S98,-2,-3,-3,E19,-3,-3,-2,S98 // Line 309: soft W, empty field, goal post, empty field, goal post,empty field, soft W +#define L310 S98,-3,-3,-3,E19,-3,-3,-3,S98 // Line 310: soft W, goal post, inside goal, goal post, soft W +#define L310_314 L310,L310,L310,L310,L310 + +using std::min; +using std::max; + + +#define MIN min_node +Node* min_node; // non-expanded node with lowest predicted total cost (f) + +namespace open{ + + Node* insert(Node* new_node, Node* root) { + + new_node->left = nullptr; + new_node->right = nullptr; + + // Empty BST, return without saving min_node + if(root == nullptr){ + new_node->up = nullptr; + MIN = new_node; // save min node for fast access + return new_node; + } + + // If new_node is the new min node + if(new_node->f < MIN->f){ + MIN->left = new_node; + new_node->up = MIN; + MIN = new_node; + return root; + } + + Node* node = root; + float key = new_node->f; + + while(true){ + if (key < node->f) + if(node->left == nullptr){ + node->left = new_node; + break; + }else{ + node = node->left; + } + else{ + if(node->right == nullptr){ + node->right = new_node; + break; + }else{ + node = node->right; + } + } + } + + new_node->up = node; + return root; + } + + + // Remove min node + Node* pop(Node* root) { + + // Minimum node can have right child, but not left child + if (MIN->right == nullptr){ + if(MIN == root){ //------(A)------ min node is root and has no children + return nullptr; // BST is empty + } + MIN->up->left = nullptr; //------(B)------ min node has no children but has parent + MIN = MIN->up; + }else{ + if(MIN == root){ //------(C)------ min node is root and has right child + MIN = MIN->right; + root = MIN; + root->up = nullptr; + + while(MIN->left != nullptr){ // update new min node + MIN = MIN->left; + } + + return root; // right child is now root + } + MIN->right->up = MIN->up; //------(D)------ min node has right child and parent + MIN->up->left = MIN->right; + + MIN = MIN->right; + while(MIN->left != nullptr){ // update new min node + MIN = MIN->left; + } + } + + return root; + } + + + // Remove specific node + Node* delete_node(Node* node, Node* root) { + + if(node == MIN){ // remove min node + return pop(root); + } + + if(node->left==nullptr and node->right==nullptr){ //------(A)------ node has no children (it can't be root, otherwise it would be min node) + // Redirect incoming connection + if(node->up->left == node){ node->up->left = nullptr; } + else{ node->up->right = nullptr; } + + }else if(node->left==nullptr){ //------(B)------ node has right child (it can't be root, otherwise it would be min node) + // Redirect incoming connections + node->right->up = node->up; + if(node->up->left == node){ node->up->left = node->right; } + else{ node->up->right = node->right; } + + }else if(node->right==nullptr){ //------(C)------ node has left child + if(node == root){ + node->left->up = nullptr; + return node->left; // left child becomes root + } + + // Redirect incoming connections (if not root) + node->left->up = node->up; + if(node->up->left == node){ node->up->left = node->left; } + else{ node->up->right = node->left; } + + }else{ //------(D)------ node has 2 children + Node *successor = node->right; + if(successor->left == nullptr){ //----- if successor is the node's right child (successor has no left child) + + //-------------- successor replaces node + + // Outgoing connections (successor's right child is not changed) + successor->left = node->left; + successor->up = node->up; // if node is root this is also ok + + // Incoming connections + node->left->up = successor; + + if(node == root){ return successor; } // successor becomes root + + // Incoming connections (if not root) + if(node->up->left == node){ node->up->left = successor; } + else{ node->up->right = successor; } + + }else{ //------ if successor is deeper (successor has no left child, and is itself a left child) + do{ + successor = successor->left; + }while(successor->left != nullptr); + + //-------------- Remove successor by redirecting its incoming connections + if(successor->right==nullptr){ // no children + successor->up->left = nullptr; + }else{ + successor->up->left = successor->right; + successor->right->up = successor->up; + } + + //-------------- successor replaces node + + // Outgoing connections + successor->left = node->left; + successor->right = node->right; + successor->up = node->up; // if node is root this is also ok + + // Incoming connections + node->left->up = successor; + node->right->up = successor; + + if(node == root){ return successor; } // successor becomes root + + // Incoming connections (if not root) + if(node->up->left == node){ node->up->left = successor; } + else{ node->up->right = successor; } + } + } + + return root; + } + + // Inorder Traversal + // void inorder(Node* root, Node* board) { + // if (root != nullptr) { + // // Traverse left + // inorder(root->left, board); + + // // Traverse root + // std::cout << (root-board)/COLS << " " << (root-board)%COLS << " -> "; + + // // Traverse right + // inorder(root->right, board); + + // } + // return; + // } +} + + + + +inline int x_to_line(float x){ + return int(fmaxf(0.f, fminf(10*x+160, 320.f)) + 0.5f); +} + +inline int y_to_col(float y){ + return int(fmaxf(0.f, fminf(10*y+110, 220.f)) + 0.5f); +} + +inline float diagonal_distance(bool go_to_goal, int line, int col, int end_l, int end_c){ + // diagonal distance - adapted from http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html + int dl, dc; + if( go_to_goal ){ + dl = abs(IN_GOAL_LINE - line); + if (col>119) { dc = col-119; } + else if (col<101) { dc = 101-col; } + else { dc = 0; } + }else{ + dl = abs(line - end_l); + dc = abs(col - end_c); + } + return (dl + dc) - 0.585786437626905f * min(dl,dc); +} + +inline Node* expand_child(Node* open_root, float cost, float wall_index, Node* curr_node, Node* board, int pos, int state, + bool go_to_goal, int line, int col, int end_l, int end_c, unsigned int* node_state, float extra ){ + // child can be as inaccessible as current pos (but there is a cost penalty to avoid inaccessible paths) + if(cost <= wall_index){ + cost = 100.f; + } + + // g (min cost from start to n) + float g = curr_node->g + extra + std::fmaxf(0.f,cost); // current cost + child distance + + Node* child = &board[pos]; + + // if child is already in the open set + if (state){ + if (g >= child->g){ + return open_root; // if not an improvement, we discard the new child + }else{ + open_root = open::delete_node(child, open_root); // if it is an improvement: remove reference, update it, add it again in correct order + } + }else{ + node_state[pos] = 1; + } + + // f (prediction of min total cost passing through n) + float f = g + diagonal_distance(go_to_goal,line,col,end_l,end_c); + + child->g = g; + child->f = f; + child->parent = curr_node; + return open::insert(child, open_root); +} + + +float final_path[2050]; +int final_path_size; + +inline void build_final_path(Node* const best_node, const Node* board, float status, const bool override_end=false, const float end_x=0, const float end_y=0){ + // Node* pt = best_node; + // while( pt != nullptr ){ + // int pos = pt - board; + // board_cost[pos] = -4; + // pt = pt->parent; + // } + // std::cout << "\n"; + // for(int l=l_min; l<=l_max; l++){ + // for(int c=c_min; c<=c_max; c++){ + // //[(H)ard wall: -3, (S)oft wall: -2, (E)mpty: 0, 0 < Cost < inf] + // //if (board[l][c].closed) std::cout << "o"; + // if (board_cost[l*COLS+c] == -3) std::cout << "h"; + // else if (board_cost[l*COLS+c] == -2) std::cout << "s"; + // else if (board_cost[l*COLS+c] == -4) std::cout << "."; + // else if (board_cost[l*COLS+c] == -1) std::cout << "g"; + // else if (board_cost[l*COLS+c] == 0 and node_state[l*COLS+c]==2) std::cout << "o"; + // else if (board_cost[l*COLS+c] == 0) std::cout << " "; + // //ele ainda nao sabe ler hard walls + // else std::cout << int(board_cost[l*COLS+c]+0.5f); + // } + // std::cout << "\n"; + // } + + // Using 'current_node' would suffice if A* reaches the objective (but 'best_node' works with impossible paths or timeout) + Node* ptr = best_node; + int counter=0; + do{ + ptr = ptr->parent; + counter++; + }while( ptr != nullptr ); + + final_path_size = min(counter*2,2048); + + ptr = best_node; + int i = final_path_size-1; + + // if enabled, replace end point with correct coordinates instead of discrete version + if(override_end){ + final_path[i--] = end_y; + final_path[i--] = end_x; + ptr = ptr->parent; + } + + for(; i>0;){ + final_path[i--] = ((ptr-board) % COLS)/10.f-11.f; // y + final_path[i--] = ((ptr-board) / COLS)/10.f-16.f; // x + ptr = ptr->parent; + } + + // add status (& increment path size) + final_path[final_path_size++] = status; // 0-success, 1-timeout, 2-impossible, 3-no obstacles(this one is not done in this function) + + // add cost (& increment path size) + final_path[final_path_size++] = best_node->g / 10.f; // min. A* cost from start to best_node + +} + + +/** + * @brief Returns true if line segment 'ab' intersects either goal (considering the unreachable area) + * - This function assumes that 'a' and 'b' are two points outside the unreachable goal area + * - Therefore, 'ab' must enter and exit the goal unreachable area + * - To detect this, we consider only the intersection of 'ab' and the goal outer borders (back+sides) + * - The front should already be covered by independent goal posts checks + */ +inline bool does_intersect_any_goal(float a_x, float a_y, float b_x, float b_y){ + + float ab_x = b_x - a_x; + float ab_y = b_y - a_y; + float k; + + if(ab_x != 0){ // Check if 'ab' and goal back is noncollinear (collinear intersections are ignored) + + k = (15.75-a_x) / ab_x; // a_x + ab_x*k = 15.75 + if (k >= 0 and k <= 1 and fabsf(a_y + ab_y * k) <= 1.25){ // collision (intersection_y = a_y + ab_y*k) + return true; + } + + k = (-15.75-a_x) / ab_x; // a_x + ab_x*k = -15.75 + if (k >= 0 and k <= 1 and fabsf(a_y + ab_y * k) <= 1.25){ // collision (intersection_y = a_y + ab_y*k) + return true; + } + } + + if(ab_y != 0){ // Check if 'ab' and goal sides are noncollinear (collinear intersections are ignored) + + k = (1.25-a_y) / ab_y; // a_y + ab_y*k = 1.25 + if( k >= 0 and k <= 1){ + float intersection_x_abs = fabsf(a_x + ab_x * k); + if( intersection_x_abs >= 15 and intersection_x_abs <= 15.75 ){ // check one side for both goals at same time + return true; + } + } + + k = (-1.25-a_y) / ab_y; // a_y + ab_y*k = -1.25 + if( k >= 0 and k <= 1){ + float intersection_x_abs = fabsf(a_x + ab_x * k); + if( intersection_x_abs >= 15 and intersection_x_abs <= 15.75 ){ // check one side for both goals at same time + return true; + } + } + } + + return false; +} + + +/** + * @brief Add space cushion near the midlines and endlines + */ +inline void add_space_cushion(float board_cost[]){ + + #define CUSHION_WIDTH 6 + + // opponent goal line + for(int i=0; i 0)){ + return true; + } + + + if (go_to_goal){ // This is a safe target. If it generates a collision with any goal post, we use A* instead. + end_x = 15.2; + end_y = max(-0.8f, min(start_y, 0.8f)); + }else{ // Restrict end coordinates to map + end_x = max(-16.f, min(end_x, 16.f)); + end_y = max(-11.f, min(end_y, 11.f)); + + // Let A* handle it if the end position is unreachable or (is nearly out of bounds && out of bounds is not allowed && not near end) + if(e_cost <= wall_index or (!is_near and e_cost > 0)){ + return true; + } + } + + + /** + * Check if path intersects either goal (considering the unreachable area) + * - at this point we know that 'start' and 'end' are reachable + * - Therefore, the path must enter and exit the goal unreachable area for an intersection to exist + * - To detect this, we consider only the intersection of the path and the goal outer borders (back+sides) + * - The front is covered next by goal posts checks + */ + if (does_intersect_any_goal(start_x, start_y, end_x, end_y)) { + return true; + } + + /** + * ----------------------- List all obstacles: given obstacles + goal posts + * note that including the goal posts in the given obstacles is not a bad idea since the default + * goal posts we add here provide no space cushion (which may be needed if the robot is not precise) + * values explanation: + * - goal post location (tested in simulator, collision with robot, robot is sideways to the goal post and arms are close to body) + * - hard radius (tested in the same way, robot collides when closer than 0.15m from goal post border) + * post radius 0.02 + agent radius 0.15 = 0.17 + * - largest radius (equivalent to hard radius, since there is no soft radius) + */ + + float obst[given_obst_size*4/5+16] = { + 15.02, 1.07, 0.17, 0.17, + 15.02, -1.07, 0.17, 0.17, + -15.02, 1.07, 0.17, 0.17, + -15.02, -1.07, 0.17, 0.17 + }; // x, y, hard radius, largest radius + + + int obst_size = 16; + + for(int i=0; istart center<->end + return true; + } + } + }else{ + + //-------------------- Normal case (start !~= end): the path is obstructed if it intersects any hard or soft circumference + + // for each obstacle: check if circle intersects line segment (start-end) + for(int ob=0; obcenter onto start->end + float sc_proj_y = se_y * sc_proj_scale; // projection of start->center onto start->end + + // check if projection falls on top of trajectory (start->projection = k * start->end) + float k = abs(se_x)>abs(se_y) ? sc_proj_x/se_x : sc_proj_y/se_y; // we use the largest dimension of start->end to avoid division by 0 + + if(k <= 0){ + if(sc_x*sc_x + sc_y*sc_y <= r_sq){ // check distance: center<->start + return true; + } + }else if(k >= 1){ + float ec_x = c_x - end_x; + float ec_y = c_y - end_y; + if(ec_x*ec_x + ec_y*ec_y <= r_sq){ // check distance: center<->end + return true; + } + }else{ + float proj_c_x = c_x - (sc_proj_x + start_x); + float proj_c_y = c_y - (sc_proj_y + start_y); + if(proj_c_x*proj_c_x + proj_c_y*proj_c_y <= r_sq){ // check distance: center<->projection + return true; + } + } + } + } + + float path_x = end_x - start_x; + float path_y = end_y - start_y; + + final_path_size = 6; + final_path[0] = start_x; + final_path[1] = start_y; + final_path[2] = end_x; + final_path[3] = end_y; + final_path[4] = 3; // status: 3-no obstacles + final_path[5] = sqrtf(path_x*path_x+path_y*path_y) + max(0.f, e_cost/10.f); // min. A* cost from start to end (e_cost is added even if start==end to help debug cell costs) + + return false; // no obstruction was found +} + +// opponent players + active player + restricted areas (from referee) +// data: +// [start x][start y] +// [allow out of bounds?][go to goal?] +// [optional target x][optional target y] +// [timeout] +// [x][y][hard radius][soft radius][force] +void astar(float params[], int params_size){ + + auto t1 = high_resolution_clock::now(); + + const float s_x = params[0]; // start x + const float s_y = params[1]; // start y + const bool allow_out_of_bounds = params[2]; + const int wall_index = allow_out_of_bounds ? -3 : -2; // (cost <= wall_index) means 'unreachable' + const bool go_to_goal = params[3]; + const float opt_t_x = params[4]; // optional target x + const float opt_t_y = params[5]; // optional target y + const int timeout_us = params[6]; + float* obstacles = ¶ms[7]; + int obst_size = params_size-7; // size of obstacles array + + //======================================================== Populate board 0: add field layout + float board_cost[LINES*COLS] = {L0_1,L2_5,L6_10,L11,LIN12_308,L309,L310_314,L2_5,L0_1}; + + if (!allow_out_of_bounds){ // add cost to getting near sideline or endline (except near goal) + add_space_cushion(board_cost); + } + + //======================================================== Check if path is obstructed + + if (!is_path_obstructed(s_x, s_y, opt_t_x, opt_t_y, obstacles, obst_size, go_to_goal, wall_index, board_cost)){ + return; // return if path is not obstructed + } + + //======================================================== Define board basics (start, end, limits) + + // if the start point is out of field, it is brought in + const int start_l = x_to_line(s_x); + const int start_c = y_to_col(s_y); + const int start_pos = start_l * COLS + start_c; + + // define objective (go to goal or a specific point) + int end_l, end_c; + if(!go_to_goal){ + end_l = x_to_line(opt_t_x); + end_c = y_to_col(opt_t_y); + }else{ + end_l = IN_GOAL_LINE; + } + + // define board limits considering the initial and final positions (and obstacles in the next section, and goals after that) + int l_min = min(start_l, end_l); + int l_max = max(start_l, end_l); + int c_min, c_max; + if(go_to_goal){ + c_min = min(start_c,119); + c_max = max(start_c,101); + }else{ + c_min = min(start_c, end_c); + c_max = max(start_c, end_c); + } + + if (!allow_out_of_bounds){ // workspace must contain a bit of empty field if out of bounds is not allowed + l_min = min(l_min, 306); + l_max = max(14, l_max); + c_min = min(c_min, 206); + c_max = max(14, c_max); + } + + //======================================================== Initialize A* + + Node* open_root = nullptr; + Node board[LINES*COLS]; + unsigned int node_state[LINES*COLS] = {0}; //0-unknown, 1-open, 2-closed + + //======================================================== Populate board 1: convert obstacles to cost + for(int ob=0; ob=0 and c>=0 and l=0 and c>=0 and l wall_index and cost < fr){ + board_cost[p] = fr; + } + } + } + + // adjust board limits if working area overlaps goal area (which includes walking margin) + if (c_max > 96 and c_min < 124){ // Otherwise it does not overlap any goal + if (l_max > 1 and l_min < 12 ){ // Overlaps our goal + l_max = max(12,l_max); // Extend working area to include our goal + l_min = min(l_min,1); + c_max = max(124,c_max); + c_min = min(c_min,96); + } + if (l_max > 308 and l_min < 319 ){ // Overlaps their goal + l_max = max(319,l_max); // Extend working area to include their goal + l_min = min(l_min,308); + c_max = max(124,c_max); + c_min = min(c_min,96); + } + } + + + //======================================================== Populate board 2: add objective + // Explanation: if objective is not accessible we do not add it to the map, although its reference still exists. + // Therefore, we know how far we are from that reference, but it cannot be reached. + // Even if we are on top of the objective, the idea is to get away, to the nearest valid position. + if(!go_to_goal){ + float *end_cost = &board_cost[end_l*COLS+end_c]; + if(*end_cost > wall_index){ + *end_cost = -1; + } + }else{ + for(int i=IN_GOAL_LINE*COLS+101; i<=IN_GOAL_LINE*COLS+119; i++){ + if(board_cost[i] > wall_index){ + board_cost[i] = -1; + } + } + } + + // add board limits as an additional restriction to workspace + l_min = max(0, l_min); + l_max = min(l_max, 320); + c_min = max(0, c_min); + c_max = min(c_max, 220); + + // add start node to open list (it will be closed right away, so there is not need to set it as open) + board[start_pos].g = 0; // This is needed to compute the cost of child nodes, but f is not needed because there are no comparisons with other nodes in the open BST + board[start_pos].parent = nullptr; //This is where the path ends + open_root = open::insert(&board[start_pos], open_root); + int measure_timeout=0; + Node* best_node = &board[start_pos]; // save best node based on distance to goal (useful if impossible/timeout to get best path) + + float best_node_dist = std::numeric_limits::max(); // infinite distance if start is itself unreachable + if(board_cost[start_pos] > wall_index){ + best_node_dist = diagonal_distance(go_to_goal,start_l,start_c,end_l,end_c); + } + + + + //======================================================== A* algorithm + while (open_root != nullptr){ + + // Get next best node (lowest predicted total cost (f)) + Node* curr_node = min_node; + const int curr_pos = curr_node - board; + const int curr_line = curr_pos / COLS; + const int curr_col = curr_pos % COLS; + const float curr_cost = board_cost[curr_pos]; + measure_timeout = (measure_timeout+1) & 31; // check timeout at every 32 iterations + + // save best node based on distance to goal (useful if impossible/timeout) + if(curr_cost > wall_index){ + float dd = diagonal_distance(go_to_goal,curr_line,curr_col,end_l,end_c); + if(best_node_dist > dd){ + best_node = curr_node; + best_node_dist = dd; + } + } + + + open_root = open::pop(open_root); + node_state[curr_pos] = 2; + + // Check if we reached objective + if( curr_cost == -1 ){ + // replace end point with correct coordinates instead of discrete version if the optional target was defined (not going to goal) + build_final_path(best_node, board, 0, !go_to_goal, opt_t_x, opt_t_y); + return; + } + if( measure_timeout==0 and duration_cast(high_resolution_clock::now() - t1).count() > timeout_us ){ + build_final_path(best_node, board, 1); + return; + } + + // Expand child nodes + bool rcol_ok = curr_col < c_max; + bool lcol_ok = curr_col > c_min; + + if(curr_line > l_min){ + int line = curr_line - 1; + int col = curr_col - 1; + int pos = curr_pos - COLS - 1; + float cost = board_cost[pos]; + int state = node_state[pos]; + + // check if not an obstacle and if node is not closed (child can be as inaccessible as current pos) + if (state!=2 and !(cost <= wall_index and cost < curr_cost) and lcol_ok){ + open_root = expand_child(open_root,cost,wall_index,curr_node,board,pos,state,go_to_goal,line,col,end_l,end_c,node_state,SQRT2); + } + + col++; + pos++; + cost = board_cost[pos]; + state = node_state[pos]; + + // check if not an obstacle and if node is not closed (child can be as inaccessible as current pos) + if (state!=2 and !(cost <= wall_index and cost < curr_cost)){ + open_root = expand_child(open_root,cost,wall_index,curr_node,board,pos,state,go_to_goal,line,col,end_l,end_c,node_state,1); + } + + col++; + pos++; + cost = board_cost[pos]; + state = node_state[pos]; + + // check if not an obstacle and if node is not closed (child can be as inaccessible as current pos) + if (state!=2 and !(cost <= wall_index and cost < curr_cost) and rcol_ok){ + open_root = expand_child(open_root,cost,wall_index,curr_node,board,pos,state,go_to_goal,line,col,end_l,end_c,node_state,SQRT2); + } + + } + + if(curr_line < l_max){ + int line = curr_line + 1; + int col = curr_col - 1; + int pos = curr_pos + COLS - 1; + float cost = board_cost[pos]; + int state = node_state[pos]; + + // check if not an obstacle and if node is not closed (child can be as inaccessible as current pos) + if (state!=2 and !(cost <= wall_index and cost < curr_cost) and lcol_ok){ + open_root = expand_child(open_root,cost,wall_index,curr_node,board,pos,state,go_to_goal,line,col,end_l,end_c,node_state,SQRT2); + } + + col++; + pos++; + cost = board_cost[pos]; + state = node_state[pos]; + + // check if not an obstacle and if node is not closed (child can be as inaccessible as current pos) + if (state!=2 and !(cost <= wall_index and cost < curr_cost)){ + open_root = expand_child(open_root,cost,wall_index,curr_node,board,pos,state,go_to_goal,line,col,end_l,end_c,node_state,1); + } + + col++; + pos++; + cost = board_cost[pos]; + state = node_state[pos]; + + // check if not an obstacle and if node is not closed (child can be as inaccessible as current pos) + if (state!=2 and !(cost <= wall_index and cost < curr_cost) and rcol_ok){ + open_root = expand_child(open_root,cost,wall_index,curr_node,board,pos,state,go_to_goal,line,col,end_l,end_c,node_state,SQRT2); + } + } + + + { + int col = curr_col - 1; + int pos = curr_pos - 1; + float cost = board_cost[pos]; + int state = node_state[pos]; + + // check if not an obstacle and if node is not closed (child can be as inaccessible as current pos) + if (state!=2 and !(cost <= wall_index and cost < curr_cost) and lcol_ok){ + open_root = expand_child(open_root,cost,wall_index,curr_node,board,pos,state,go_to_goal,curr_line,col,end_l,end_c,node_state,1); + } + + col+=2; + pos+=2; + cost = board_cost[pos]; + state = node_state[pos]; + + + // check if not an obstacle and if node is not closed (child can be as inaccessible as current pos) + if (state!=2 and !(cost <= wall_index and cost < curr_cost) and rcol_ok){ + open_root = expand_child(open_root,cost,wall_index,curr_node,board,pos,state,go_to_goal,curr_line,col,end_l,end_c,node_state,1); + } + } + + + } + + build_final_path(best_node, board, 2); + return; +} \ No newline at end of file diff --git a/cpp/a_star/a_star.h b/cpp/a_star/a_star.h new file mode 100644 index 0000000..16d0974 --- /dev/null +++ b/cpp/a_star/a_star.h @@ -0,0 +1,26 @@ +#pragma once + +/** + * FILENAME: a_star.h + * DESCRIPTION: custom A* pathfinding implementation, optimized for the soccer environment + * AUTHOR: Miguel Abreu (m.abreu@fe.up.pt) + * DATE: 2022 + */ + +struct Node{ + + //------------- BST parameters + Node* left; + Node* right; + Node* up; + + //------------- A* parameters + Node* parent; + float g; + float f; + +}; + +extern void astar(float params[], int params_size); +extern float final_path[2050]; +extern int final_path_size; \ No newline at end of file diff --git a/cpp/a_star/debug_main.cc b/cpp/a_star/debug_main.cc new file mode 100644 index 0000000..1cd5f83 --- /dev/null +++ b/cpp/a_star/debug_main.cc @@ -0,0 +1,37 @@ +#include "a_star.h" +#include +#include + +using std::chrono::high_resolution_clock; +using std::chrono::duration_cast; +using std::chrono::microseconds; + +std::chrono::_V2::system_clock::time_point t1,t2; + +float params[] = { + 15.78,-0.07, //start + 1,1, //out of bounds? go to goal? + 0,0, //target (if not go to goal) + 500000, // timeout + -10,0,1,5,5, + -10,1,1,7,10, + -10,-7,0,5,1 +}; +int params_size = sizeof(params)/sizeof(params[0]); + + +int main(){ + + t1 = high_resolution_clock::now(); + astar(params, params_size); + t2 = high_resolution_clock::now(); + + std::cout << duration_cast(t2 - t1).count() << "us (includes initialization)\n"; + + t1 = high_resolution_clock::now(); + astar(params, params_size); + t2 = high_resolution_clock::now(); + + std::cout << duration_cast(t2 - t1).count() << "us\n"; + +} \ No newline at end of file diff --git a/cpp/a_star/expansion_groups.h b/cpp/a_star/expansion_groups.h new file mode 100644 index 0000000..d8350b5 --- /dev/null +++ b/cpp/a_star/expansion_groups.h @@ -0,0 +1,6 @@ +const int expansion_positions_no = 7845; +const float expansion_pos_dist[7845] = {0.0,0.1,0.1,0.1,0.1,0.14142135623730953,0.14142135623730953,0.14142135623730953,0.14142135623730953,0.2,0.2,0.2,0.2,0.223606797749979,0.223606797749979,0.223606797749979,0.223606797749979,0.223606797749979,0.223606797749979,0.223606797749979,0.223606797749979,0.28284271247461906,0.28284271247461906,0.28284271247461906,0.28284271247461906,0.30000000000000004,0.30000000000000004,0.30000000000000004,0.30000000000000004,0.316227766016838,0.316227766016838,0.316227766016838,0.316227766016838,0.316227766016838,0.316227766016838,0.316227766016838,0.316227766016838,0.36055512754639896,0.36055512754639896,0.36055512754639896,0.36055512754639896,0.36055512754639896,0.36055512754639896,0.36055512754639896,0.36055512754639896,0.4,0.4,0.4,0.4,0.41231056256176607,0.41231056256176607,0.41231056256176607,0.41231056256176607,0.41231056256176607,0.41231056256176607,0.41231056256176607,0.41231056256176607,0.4242640687119285,0.4242640687119285,0.4242640687119285,0.4242640687119285,0.447213595499958,0.447213595499958,0.447213595499958,0.447213595499958,0.447213595499958,0.447213595499958,0.447213595499958,0.447213595499958,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5099019513592785,0.5099019513592785,0.5099019513592785,0.5099019513592785,0.5099019513592785,0.5099019513592785,0.5099019513592785,0.5099019513592785,0.5385164807134504,0.5385164807134504,0.5385164807134504,0.5385164807134504,0.5385164807134504,0.5385164807134504,0.5385164807134504,0.5385164807134504,0.5656854249492381,0.5656854249492381,0.5656854249492381,0.5656854249492381,0.5830951894845301,0.5830951894845301,0.5830951894845301,0.5830951894845301,0.5830951894845301,0.5830951894845301,0.5830951894845301,0.5830951894845301,0.6000000000000001,0.6000000000000001,0.6000000000000001,0.6000000000000001,0.608276253029822,0.608276253029822,0.608276253029822,0.608276253029822,0.608276253029822,0.608276253029822,0.608276253029822,0.608276253029822,0.632455532033676,0.632455532033676,0.632455532033676,0.632455532033676,0.632455532033676,0.632455532033676,0.632455532033676,0.632455532033676,0.6403124237432849,0.6403124237432849,0.6403124237432849,0.6403124237432849,0.6403124237432849,0.6403124237432849,0.6403124237432849,0.6403124237432849,0.670820393249937,0.670820393249937,0.670820393249937,0.670820393249937,0.670820393249937,0.670820393249937,0.670820393249937,0.670820393249937,0.7000000000000001,0.7000000000000001,0.7000000000000001,0.7000000000000001,0.7071067811865476,0.7071067811865476,0.7071067811865476,0.7071067811865476,0.7071067811865476,0.7071067811865476,0.7071067811865476,0.7071067811865476,0.7071067811865476,0.7071067811865476,0.7071067811865476,0.7071067811865476,0.7211102550927979,0.7211102550927979,0.7211102550927979,0.7211102550927979,0.7211102550927979,0.7211102550927979,0.7211102550927979,0.7211102550927979,0.7280109889280518,0.7280109889280518,0.7280109889280518,0.7280109889280518,0.7280109889280518,0.7280109889280518,0.7280109889280518,0.7280109889280518,0.7615773105863909,0.7615773105863909,0.7615773105863909,0.7615773105863909,0.7615773105863909,0.7615773105863909,0.7615773105863909,0.7615773105863909,0.7810249675906654,0.7810249675906654,0.7810249675906654,0.7810249675906654,0.7810249675906654,0.7810249675906654,0.7810249675906654,0.7810249675906654,0.8,0.8,0.8,0.8,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8062257748298549,0.8246211251235321,0.8246211251235321,0.8246211251235321,0.8246211251235321,0.8246211251235321,0.8246211251235321,0.8246211251235321,0.8246211251235321,0.848528137423857,0.848528137423857,0.848528137423857,0.848528137423857,0.8544003745317531,0.8544003745317531,0.8544003745317531,0.8544003745317531,0.8544003745317531,0.8544003745317531,0.8544003745317531,0.8544003745317531,0.8602325267042628,0.8602325267042628,0.8602325267042628,0.8602325267042628,0.8602325267042628,0.8602325267042628,0.8602325267042628,0.8602325267042628,0.894427190999916,0.894427190999916,0.894427190999916,0.894427190999916,0.894427190999916,0.894427190999916,0.894427190999916,0.894427190999916,0.9,0.9,0.9,0.9,0.9055385138137417,0.9055385138137417,0.9055385138137417,0.9055385138137417,0.9055385138137417,0.9055385138137417,0.9055385138137417,0.9055385138137417,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9219544457292888,0.9433981132056604,0.9433981132056604,0.9433981132056604,0.9433981132056604,0.9433981132056604,0.9433981132056604,0.9433981132056604,0.9433981132056604,0.9486832980505139,0.9486832980505139,0.9486832980505139,0.9486832980505139,0.9486832980505139,0.9486832980505139,0.9486832980505139,0.9486832980505139,0.9848857801796105,0.9848857801796105,0.9848857801796105,0.9848857801796105,0.9848857801796105,0.9848857801796105,0.9848857801796105,0.9848857801796105,0.9899494936611666,0.9899494936611666,0.9899494936611666,0.9899494936611666,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.004987562112089,1.004987562112089,1.004987562112089,1.004987562112089,1.004987562112089,1.004987562112089,1.004987562112089,1.004987562112089,1.019803902718557,1.019803902718557,1.019803902718557,1.019803902718557,1.019803902718557,1.019803902718557,1.019803902718557,1.019803902718557,1.0295630140987002,1.0295630140987002,1.0295630140987002,1.0295630140987002,1.0295630140987002,1.0295630140987002,1.0295630140987002,1.0295630140987002,1.044030650891055,1.044030650891055,1.044030650891055,1.044030650891055,1.044030650891055,1.044030650891055,1.044030650891055,1.044030650891055,1.063014581273465,1.063014581273465,1.063014581273465,1.063014581273465,1.063014581273465,1.063014581273465,1.063014581273465,1.063014581273465,1.0770329614269007,1.0770329614269007,1.0770329614269007,1.0770329614269007,1.0770329614269007,1.0770329614269007,1.0770329614269007,1.0770329614269007,1.0816653826391969,1.0816653826391969,1.0816653826391969,1.0816653826391969,1.0816653826391969,1.0816653826391969,1.0816653826391969,1.0816653826391969,1.1,1.1,1.1,1.1,1.104536101718726,1.104536101718726,1.104536101718726,1.104536101718726,1.104536101718726,1.104536101718726,1.104536101718726,1.104536101718726,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.118033988749895,1.1313708498984762,1.1313708498984762,1.1313708498984762,1.1313708498984762,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.140175425099138,1.1661903789690602,1.1661903789690602,1.1661903789690602,1.1661903789690602,1.1661903789690602,1.1661903789690602,1.1661903789690602,1.1661903789690602,1.1704699910719627,1.1704699910719627,1.1704699910719627,1.1704699910719627,1.1704699910719627,1.1704699910719627,1.1704699910719627,1.1704699910719627,1.2000000000000002,1.2000000000000002,1.2000000000000002,1.2000000000000002,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2041594578792296,1.2083045973594573,1.2083045973594573,1.2083045973594573,1.2083045973594573,1.2083045973594573,1.2083045973594573,1.2083045973594573,1.2083045973594573,1.216552506059644,1.216552506059644,1.216552506059644,1.216552506059644,1.216552506059644,1.216552506059644,1.216552506059644,1.216552506059644,1.2206555615733703,1.2206555615733703,1.2206555615733703,1.2206555615733703,1.2206555615733703,1.2206555615733703,1.2206555615733703,1.2206555615733703,1.2369316876852983,1.2369316876852983,1.2369316876852983,1.2369316876852983,1.2369316876852983,1.2369316876852983,1.2369316876852983,1.2369316876852983,1.252996408614167,1.252996408614167,1.252996408614167,1.252996408614167,1.252996408614167,1.252996408614167,1.252996408614167,1.252996408614167,1.264911064067352,1.264911064067352,1.264911064067352,1.264911064067352,1.264911064067352,1.264911064067352,1.264911064067352,1.264911064067352,1.2727922061357857,1.2727922061357857,1.2727922061357857,1.2727922061357857,1.2806248474865698,1.2806248474865698,1.2806248474865698,1.2806248474865698,1.2806248474865698,1.2806248474865698,1.2806248474865698,1.2806248474865698,1.3,1.3,1.3,1.3,1.3,1.3,1.3,1.3,1.3,1.3,1.3,1.3,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.30384048104053,1.3152946437965907,1.3152946437965907,1.3152946437965907,1.3152946437965907,1.3152946437965907,1.3152946437965907,1.3152946437965907,1.3152946437965907,1.3341664064126335,1.3341664064126335,1.3341664064126335,1.3341664064126335,1.3341664064126335,1.3341664064126335,1.3341664064126335,1.3341664064126335,1.341640786499874,1.341640786499874,1.341640786499874,1.341640786499874,1.341640786499874,1.341640786499874,1.341640786499874,1.341640786499874,1.3453624047073711,1.3453624047073711,1.3453624047073711,1.3453624047073711,1.3453624047073711,1.3453624047073711,1.3453624047073711,1.3453624047073711,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3601470508735445,1.3892443989449805,1.3892443989449805,1.3892443989449805,1.3892443989449805,1.3892443989449805,1.3892443989449805,1.3892443989449805,1.3892443989449805,1.392838827718412,1.392838827718412,1.392838827718412,1.392838827718412,1.392838827718412,1.392838827718412,1.392838827718412,1.392838827718412,1.4000000000000001,1.4000000000000001,1.4000000000000001,1.4000000000000001,1.40356688476182,1.40356688476182,1.40356688476182,1.40356688476182,1.40356688476182,1.40356688476182,1.40356688476182,1.40356688476182,1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4212670403551897,1.4212670403551897,1.4212670403551897,1.4212670403551897,1.4212670403551897,1.4212670403551897,1.4212670403551897,1.4212670403551897,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4317821063276355,1.4422205101855958,1.4422205101855958,1.4422205101855958,1.4422205101855958,1.4422205101855958,1.4422205101855958,1.4422205101855958,1.4422205101855958,1.4560219778561037,1.4560219778561037,1.4560219778561037,1.4560219778561037,1.4560219778561037,1.4560219778561037,1.4560219778561037,1.4560219778561037,1.47648230602334,1.47648230602334,1.47648230602334,1.47648230602334,1.47648230602334,1.47648230602334,1.47648230602334,1.47648230602334,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.4866068747318506,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.503329637837291,1.503329637837291,1.503329637837291,1.503329637837291,1.503329637837291,1.503329637837291,1.503329637837291,1.503329637837291,1.5132745950421558,1.5132745950421558,1.5132745950421558,1.5132745950421558,1.5132745950421558,1.5132745950421558,1.5132745950421558,1.5132745950421558,1.5231546211727818,1.5231546211727818,1.5231546211727818,1.5231546211727818,1.5231546211727818,1.5231546211727818,1.5231546211727818,1.5231546211727818,1.5264337522473748,1.5264337522473748,1.5264337522473748,1.5264337522473748,1.5264337522473748,1.5264337522473748,1.5264337522473748,1.5264337522473748,1.5297058540778357,1.5297058540778357,1.5297058540778357,1.5297058540778357,1.5297058540778357,1.5297058540778357,1.5297058540778357,1.5297058540778357,1.5524174696260025,1.5524174696260025,1.5524174696260025,1.5524174696260025,1.5524174696260025,1.5524174696260025,1.5524174696260025,1.5524174696260025,1.5556349186104046,1.5556349186104046,1.5556349186104046,1.5556349186104046,1.5620499351813308,1.5620499351813308,1.5620499351813308,1.5620499351813308,1.5620499351813308,1.5620499351813308,1.5620499351813308,1.5620499351813308,1.565247584249853,1.565247584249853,1.565247584249853,1.565247584249853,1.565247584249853,1.565247584249853,1.565247584249853,1.565247584249853,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.5811388300841898,1.6,1.6,1.6,1.6,1.6031219541881399,1.6031219541881399,1.6031219541881399,1.6031219541881399,1.6031219541881399,1.6031219541881399,1.6031219541881399,1.6031219541881399,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6124515496597098,1.6155494421403511,1.6155494421403511,1.6155494421403511,1.6155494421403511,1.6155494421403511,1.6155494421403511,1.6155494421403511,1.6155494421403511,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6278820596099708,1.6401219466856727,1.6401219466856727,1.6401219466856727,1.6401219466856727,1.6401219466856727,1.6401219466856727,1.6401219466856727,1.6401219466856727,1.6492422502470643,1.6492422502470643,1.6492422502470643,1.6492422502470643,1.6492422502470643,1.6492422502470643,1.6492422502470643,1.6492422502470643,1.6552945357246849,1.6552945357246849,1.6552945357246849,1.6552945357246849,1.6552945357246849,1.6552945357246849,1.6552945357246849,1.6552945357246849,1.6643316977093239,1.6643316977093239,1.6643316977093239,1.6643316977093239,1.6643316977093239,1.6643316977093239,1.6643316977093239,1.6643316977093239,1.6763054614240211,1.6763054614240211,1.6763054614240211,1.6763054614240211,1.6763054614240211,1.6763054614240211,1.6763054614240211,1.6763054614240211,1.697056274847714,1.697056274847714,1.697056274847714,1.697056274847714,1.7000000000000002,1.7000000000000002,1.7000000000000002,1.7000000000000002,1.7000000000000002,1.7000000000000002,1.7000000000000002,1.7000000000000002,1.7000000000000002,1.7000000000000002,1.7000000000000002,1.7000000000000002,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7029386365926404,1.7088007490635062,1.7088007490635062,1.7088007490635062,1.7088007490635062,1.7088007490635062,1.7088007490635062,1.7088007490635062,1.7088007490635062,1.711724276862369,1.711724276862369,1.711724276862369,1.711724276862369,1.711724276862369,1.711724276862369,1.711724276862369,1.711724276862369,1.7204650534085255,1.7204650534085255,1.7204650534085255,1.7204650534085255,1.7204650534085255,1.7204650534085255,1.7204650534085255,1.7204650534085255,1.7262676501632068,1.7262676501632068,1.7262676501632068,1.7262676501632068,1.7262676501632068,1.7262676501632068,1.7262676501632068,1.7262676501632068,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.746424919657298,1.7492855684535902,1.7492855684535902,1.7492855684535902,1.7492855684535902,1.7492855684535902,1.7492855684535902,1.7492855684535902,1.7492855684535902,1.7691806012954132,1.7691806012954132,1.7691806012954132,1.7691806012954132,1.7691806012954132,1.7691806012954132,1.7691806012954132,1.7691806012954132,1.772004514666935,1.772004514666935,1.772004514666935,1.772004514666935,1.772004514666935,1.772004514666935,1.772004514666935,1.772004514666935,1.780449381476486,1.780449381476486,1.780449381476486,1.780449381476486,1.780449381476486,1.780449381476486,1.780449381476486,1.780449381476486,1.788854381999832,1.788854381999832,1.788854381999832,1.788854381999832,1.788854381999832,1.788854381999832,1.788854381999832,1.788854381999832,1.8,1.8,1.8,1.8,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8027756377319948,1.8110770276274835,1.8110770276274835,1.8110770276274835,1.8110770276274835,1.8110770276274835,1.8110770276274835,1.8110770276274835,1.8110770276274835,1.824828759089466,1.824828759089466,1.824828759089466,1.824828759089466,1.824828759089466,1.824828759089466,1.824828759089466,1.824828759089466,1.8357559750685821,1.8357559750685821,1.8357559750685821,1.8357559750685821,1.8357559750685821,1.8357559750685821,1.8357559750685821,1.8357559750685821,1.8384776310850235,1.8384776310850235,1.8384776310850235,1.8384776310850235,1.8384776310850235,1.8384776310850235,1.8384776310850235,1.8384776310850235,1.8384776310850235,1.8384776310850235,1.8384776310850235,1.8384776310850235,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8439088914585775,1.8601075237738276,1.8601075237738276,1.8601075237738276,1.8601075237738276,1.8601075237738276,1.8601075237738276,1.8601075237738276,1.8601075237738276,1.8681541692269406,1.8681541692269406,1.8681541692269406,1.8681541692269406,1.8681541692269406,1.8681541692269406,1.8681541692269406,1.8681541692269406,1.8788294228055937,1.8788294228055937,1.8788294228055937,1.8788294228055937,1.8788294228055937,1.8788294228055937,1.8788294228055937,1.8788294228055937,1.8867962264113207,1.8867962264113207,1.8867962264113207,1.8867962264113207,1.8867962264113207,1.8867962264113207,1.8867962264113207,1.8867962264113207,1.8973665961010278,1.8973665961010278,1.8973665961010278,1.8973665961010278,1.8973665961010278,1.8973665961010278,1.8973665961010278,1.8973665961010278,1.9000000000000001,1.9000000000000001,1.9000000000000001,1.9000000000000001,1.9026297590440446,1.9026297590440446,1.9026297590440446,1.9026297590440446,1.9026297590440446,1.9026297590440446,1.9026297590440446,1.9026297590440446,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.91049731745428,1.9209372712298547,1.9209372712298547,1.9209372712298547,1.9209372712298547,1.9209372712298547,1.9209372712298547,1.9209372712298547,1.9209372712298547,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9235384061671343,1.9313207915827968,1.9313207915827968,1.9313207915827968,1.9313207915827968,1.9313207915827968,1.9313207915827968,1.9313207915827968,1.9313207915827968,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.94164878389476,1.96468827043885,1.96468827043885,1.96468827043885,1.96468827043885,1.96468827043885,1.96468827043885,1.96468827043885,1.96468827043885,1.969771560359221,1.969771560359221,1.969771560359221,1.969771560359221,1.969771560359221,1.969771560359221,1.969771560359221,1.969771560359221,1.9723082923316022,1.9723082923316022,1.9723082923316022,1.9723082923316022,1.9723082923316022,1.9723082923316022,1.9723082923316022,1.9723082923316022,1.9798989873223332,1.9798989873223332,1.9798989873223332,1.9798989873223332,1.9849433241279208,1.9849433241279208,1.9849433241279208,1.9849433241279208,1.9849433241279208,1.9849433241279208,1.9849433241279208,1.9849433241279208,1.9924858845171276,1.9924858845171276,1.9924858845171276,1.9924858845171276,1.9924858845171276,1.9924858845171276,1.9924858845171276,1.9924858845171276,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0024984394500787,2.0024984394500787,2.0024984394500787,2.0024984394500787,2.0024984394500787,2.0024984394500787,2.0024984394500787,2.0024984394500787,2.009975124224178,2.009975124224178,2.009975124224178,2.009975124224178,2.009975124224178,2.009975124224178,2.009975124224178,2.009975124224178,2.012461179749811,2.012461179749811,2.012461179749811,2.012461179749811,2.012461179749811,2.012461179749811,2.012461179749811,2.012461179749811,2.0223748416156684,2.0223748416156684,2.0223748416156684,2.0223748416156684,2.0223748416156684,2.0223748416156684,2.0223748416156684,2.0223748416156684,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.024845673131659,2.039607805437114,2.039607805437114,2.039607805437114,2.039607805437114,2.039607805437114,2.039607805437114,2.039607805437114,2.039607805437114,2.0518284528683193,2.0518284528683193,2.0518284528683193,2.0518284528683193,2.0518284528683193,2.0518284528683193,2.0518284528683193,2.0518284528683193,2.0591260281974004,2.0591260281974004,2.0591260281974004,2.0591260281974004,2.0591260281974004,2.0591260281974004,2.0591260281974004,2.0591260281974004,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0615528128088303,2.0808652046684815,2.0808652046684815,2.0808652046684815,2.0808652046684815,2.0808652046684815,2.0808652046684815,2.0808652046684815,2.0808652046684815,2.08806130178211,2.08806130178211,2.08806130178211,2.08806130178211,2.08806130178211,2.08806130178211,2.08806130178211,2.08806130178211,2.1,2.1,2.1,2.1,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.102379604162864,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.109502310972899,2.118962010041709,2.118962010041709,2.118962010041709,2.118962010041709,2.118962010041709,2.118962010041709,2.118962010041709,2.118962010041709,2.121320343559643,2.121320343559643,2.121320343559643,2.121320343559643,2.121320343559643,2.121320343559643,2.121320343559643,2.121320343559643,2.121320343559643,2.121320343559643,2.121320343559643,2.121320343559643,2.12602916254693,2.12602916254693,2.12602916254693,2.12602916254693,2.12602916254693,2.12602916254693,2.12602916254693,2.12602916254693,2.137755832643195,2.137755832643195,2.137755832643195,2.137755832643195,2.137755832643195,2.137755832643195,2.137755832643195,2.137755832643195,2.1400934559032696,2.1400934559032696,2.1400934559032696,2.1400934559032696,2.1400934559032696,2.1400934559032696,2.1400934559032696,2.1400934559032696,2.147091055358389,2.147091055358389,2.147091055358389,2.147091055358389,2.147091055358389,2.147091055358389,2.147091055358389,2.147091055358389,2.1540659228538015,2.1540659228538015,2.1540659228538015,2.1540659228538015,2.1540659228538015,2.1540659228538015,2.1540659228538015,2.1540659228538015,2.1587033144922905,2.1587033144922905,2.1587033144922905,2.1587033144922905,2.1587033144922905,2.1587033144922905,2.1587033144922905,2.1587033144922905,2.1633307652783937,2.1633307652783937,2.1633307652783937,2.1633307652783937,2.1633307652783937,2.1633307652783937,2.1633307652783937,2.1633307652783937,2.1840329667841556,2.1840329667841556,2.1840329667841556,2.1840329667841556,2.1840329667841556,2.1840329667841556,2.1840329667841556,2.1840329667841556,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.193171219946131,2.195449840010015,2.195449840010015,2.195449840010015,2.195449840010015,2.195449840010015,2.195449840010015,2.195449840010015,2.195449840010015,2.2,2.2,2.2,2.2,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.202271554554524,2.209072203437452,2.209072203437452,2.209072203437452,2.209072203437452,2.209072203437452,2.209072203437452,2.209072203437452,2.209072203437452,2.2135943621178655,2.2135943621178655,2.2135943621178655,2.2135943621178655,2.2135943621178655,2.2135943621178655,2.2135943621178655,2.2135943621178655,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.220360331117452,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.23606797749979,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.247220505424423,2.2561028345356955,2.2561028345356955,2.2561028345356955,2.2561028345356955,2.2561028345356955,2.2561028345356955,2.2561028345356955,2.2561028345356955,2.2627416997969525,2.2627416997969525,2.2627416997969525,2.2627416997969525,2.267156809750927,2.267156809750927,2.267156809750927,2.267156809750927,2.267156809750927,2.267156809750927,2.267156809750927,2.267156809750927,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.280350850198276,2.2825424421026654,2.2825424421026654,2.2825424421026654,2.2825424421026654,2.2825424421026654,2.2825424421026654,2.2825424421026654,2.2825424421026654,2.2847319317591728,2.2847319317591728,2.2847319317591728,2.2847319317591728,2.2847319317591728,2.2847319317591728,2.2847319317591728,2.2847319317591728,2.3000000000000003,2.3000000000000003,2.3000000000000003,2.3000000000000003,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3021728866442674,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3086792761230392,2.3194827009486403,2.3194827009486403,2.3194827009486403,2.3194827009486403,2.3194827009486403,2.3194827009486403,2.3194827009486403,2.3194827009486403,2.3259406699226015,2.3259406699226015,2.3259406699226015,2.3259406699226015,2.3259406699226015,2.3259406699226015,2.3259406699226015,2.3259406699226015,2.3323807579381204,2.3323807579381204,2.3323807579381204,2.3323807579381204,2.3323807579381204,2.3323807579381204,2.3323807579381204,2.3323807579381204,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3345235059857505,2.3409399821439254,2.3409399821439254,2.3409399821439254,2.3409399821439254,2.3409399821439254,2.3409399821439254,2.3409399821439254,2.3409399821439254,2.3430749027719964,2.3430749027719964,2.3430749027719964,2.3430749027719964,2.3430749027719964,2.3430749027719964,2.3430749027719964,2.3430749027719964,2.353720459187964,2.353720459187964,2.353720459187964,2.353720459187964,2.353720459187964,2.353720459187964,2.353720459187964,2.353720459187964,2.3600847442411896,2.3600847442411896,2.3600847442411896,2.3600847442411896,2.3600847442411896,2.3600847442411896,2.3600847442411896,2.3600847442411896,2.3706539182259396,2.3706539182259396,2.3706539182259396,2.3706539182259396,2.3706539182259396,2.3706539182259396,2.3706539182259396,2.3706539182259396,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3769728648009427,2.3853720883753127,2.3853720883753127,2.3853720883753127,2.3853720883753127,2.3853720883753127,2.3853720883753127,2.3853720883753127,2.3853720883753127,2.4000000000000004,2.4000000000000004,2.4000000000000004,2.4000000000000004,2.4020824298928627,2.4020824298928627,2.4020824298928627,2.4020824298928627,2.4020824298928627,2.4020824298928627,2.4020824298928627,2.4020824298928627,2.4041630560342617,2.4041630560342617,2.4041630560342617,2.4041630560342617,2.4041630560342617,2.4041630560342617,2.4041630560342617,2.4041630560342617,2.4041630560342617,2.4041630560342617,2.4041630560342617,2.4041630560342617,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4083189157584592,2.4166091947189146,2.4166091947189146,2.4166091947189146,2.4166091947189146,2.4166091947189146,2.4166091947189146,2.4166091947189146,2.4166091947189146,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.4186773244895647,2.420743687382041,2.420743687382041,2.420743687382041,2.420743687382041,2.420743687382041,2.420743687382041,2.420743687382041,2.420743687382041,2.433105012119288,2.433105012119288,2.433105012119288,2.433105012119288,2.433105012119288,2.433105012119288,2.433105012119288,2.433105012119288,2.4351591323771844,2.4351591323771844,2.4351591323771844,2.4351591323771844,2.4351591323771844,2.4351591323771844,2.4351591323771844,2.4351591323771844,2.4413111231467406,2.4413111231467406,2.4413111231467406,2.4413111231467406,2.4413111231467406,2.4413111231467406,2.4413111231467406,2.4413111231467406,2.451530134426253,2.451530134426253,2.451530134426253,2.451530134426253,2.451530134426253,2.451530134426253,2.451530134426253,2.451530134426253,2.459674775249769,2.459674775249769,2.459674775249769,2.459674775249769,2.459674775249769,2.459674775249769,2.459674775249769,2.459674775249769,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4698178070456938,2.4738633753705965,2.4738633753705965,2.4738633753705965,2.4738633753705965,2.4738633753705965,2.4738633753705965,2.4738633753705965,2.4738633753705965,2.4758836806279896,2.4758836806279896,2.4758836806279896,2.4758836806279896,2.4758836806279896,2.4758836806279896,2.4758836806279896,2.4758836806279896,2.4839484696748446,2.4839484696748446,2.4839484696748446,2.4839484696748446,2.4839484696748446,2.4839484696748446,2.4839484696748446,2.4839484696748446,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5019992006393608,2.5019992006393608,2.5019992006393608,2.5019992006393608,2.5019992006393608,2.5019992006393608,2.5019992006393608,2.5019992006393608,2.505992817228334,2.505992817228334,2.505992817228334,2.505992817228334,2.505992817228334,2.505992817228334,2.505992817228334,2.505992817228334,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.507987240796891,2.5179356624028344,2.5179356624028344,2.5179356624028344,2.5179356624028344,2.5179356624028344,2.5179356624028344,2.5179356624028344,2.5179356624028344,2.5238858928247927,2.5238858928247927,2.5238858928247927,2.5238858928247927,2.5238858928247927,2.5238858928247927,2.5238858928247927,2.5238858928247927,2.529822128134704,2.529822128134704,2.529822128134704,2.529822128134704,2.529822128134704,2.529822128134704,2.529822128134704,2.529822128134704,2.5317977802344327,2.5317977802344327,2.5317977802344327,2.5317977802344327,2.5317977802344327,2.5317977802344327,2.5317977802344327,2.5317977802344327,2.5455844122715714,2.5455844122715714,2.5455844122715714,2.5455844122715714,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5495097567963927,2.5553864678361276,2.5553864678361276,2.5553864678361276,2.5553864678361276,2.5553864678361276,2.5553864678361276,2.5553864678361276,2.5553864678361276,2.5612496949731396,2.5612496949731396,2.5612496949731396,2.5612496949731396,2.5612496949731396,2.5612496949731396,2.5612496949731396,2.5612496949731396,2.5632011235952596,2.5632011235952596,2.5632011235952596,2.5632011235952596,2.5632011235952596,2.5632011235952596,2.5632011235952596,2.5632011235952596,2.5709920264364885,2.5709920264364885,2.5709920264364885,2.5709920264364885,2.5709920264364885,2.5709920264364885,2.5709920264364885,2.5709920264364885,2.5806975801127883,2.5806975801127883,2.5806975801127883,2.5806975801127883,2.5806975801127883,2.5806975801127883,2.5806975801127883,2.5806975801127883,2.5942243542145693,2.5942243542145693,2.5942243542145693,2.5942243542145693,2.5942243542145693,2.5942243542145693,2.5942243542145693,2.5942243542145693,2.5961509971494343,2.5961509971494343,2.5961509971494343,2.5961509971494343,2.5961509971494343,2.5961509971494343,2.5961509971494343,2.5961509971494343,2.6,2.6,2.6,2.6,2.6,2.6,2.6,2.6,2.6,2.6,2.6,2.6,2.6019223662515376,2.6019223662515376,2.6019223662515376,2.6019223662515376,2.6019223662515376,2.6019223662515376,2.6019223662515376,2.6019223662515376,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.60768096208106,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6172504656604803,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6248809496813377,2.6305892875931813,2.6305892875931813,2.6305892875931813,2.6305892875931813,2.6305892875931813,2.6305892875931813,2.6305892875931813,2.6305892875931813,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6400757564888173,2.6419689627245813,2.6419689627245813,2.6419689627245813,2.6419689627245813,2.6419689627245813,2.6419689627245813,2.6419689627245813,2.6419689627245813,2.6476404589747453,2.6476404589747453,2.6476404589747453,2.6476404589747453,2.6476404589747453,2.6476404589747453,2.6476404589747453,2.6476404589747453,2.657066051117285,2.657066051117285,2.657066051117285,2.657066051117285,2.657066051117285,2.657066051117285,2.657066051117285,2.657066051117285,2.6627053911388696,2.6627053911388696,2.6627053911388696,2.6627053911388696,2.6627053911388696,2.6627053911388696,2.6627053911388696,2.6627053911388696,2.668332812825267,2.668332812825267,2.668332812825267,2.668332812825267,2.668332812825267,2.668332812825267,2.668332812825267,2.668332812825267,2.683281572999748,2.683281572999748,2.683281572999748,2.683281572999748,2.683281572999748,2.683281572999748,2.683281572999748,2.683281572999748,2.6870057685088806,2.6870057685088806,2.6870057685088806,2.6870057685088806,2.6907248094147422,2.6907248094147422,2.6907248094147422,2.6907248094147422,2.6907248094147422,2.6907248094147422,2.6907248094147422,2.6907248094147422,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.6925824035672523,2.7,2.7,2.7,2.7,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.701851217221259,2.707397274136177,2.707397274136177,2.707397274136177,2.707397274136177,2.707397274136177,2.707397274136177,2.707397274136177,2.707397274136177,2.716615541441225,2.716615541441225,2.716615541441225,2.716615541441225,2.716615541441225,2.716615541441225,2.716615541441225,2.716615541441225,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.720294101747089,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7294688127912363,2.7313000567495327,2.7313000567495327,2.7313000567495327,2.7313000567495327,2.7313000567495327,2.7313000567495327,2.7313000567495327,2.7313000567495327,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.7459060435491964,2.751363298439521,2.751363298439521,2.751363298439521,2.751363298439521,2.751363298439521,2.751363298439521,2.751363298439521,2.751363298439521,2.7586228448267445,2.7586228448267445,2.7586228448267445,2.7586228448267445,2.7586228448267445,2.7586228448267445,2.7586228448267445,2.7586228448267445,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7658633371878665,2.7730849247724096,2.7730849247724096,2.7730849247724096,2.7730849247724096,2.7730849247724096,2.7730849247724096,2.7730849247724096,2.7730849247724096,2.778488797889961,2.778488797889961,2.778488797889961,2.778488797889961,2.778488797889961,2.778488797889961,2.778488797889961,2.778488797889961,2.780287754891569,2.780287754891569,2.780287754891569,2.780287754891569,2.780287754891569,2.780287754891569,2.780287754891569,2.780287754891569,2.785677655436824,2.785677655436824,2.785677655436824,2.785677655436824,2.785677655436824,2.785677655436824,2.785677655436824,2.785677655436824,2.7892651361962706,2.7892651361962706,2.7892651361962706,2.7892651361962706,2.7892651361962706,2.7892651361962706,2.7892651361962706,2.7892651361962706,2.8000000000000003,2.8000000000000003,2.8000000000000003,2.8000000000000003,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.8017851452243803,2.80713376952364,2.80713376952364,2.80713376952364,2.80713376952364,2.80713376952364,2.80713376952364,2.80713376952364,2.80713376952364,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.816025568065745,2.8178005607210745,2.8178005607210745,2.8178005607210745,2.8178005607210745,2.8178005607210745,2.8178005607210745,2.8178005607210745,2.8178005607210745,2.823118842698621,2.823118842698621,2.823118842698621,2.823118842698621,2.823118842698621,2.823118842698621,2.823118842698621,2.823118842698621,2.8284271247461903,2.8284271247461903,2.8284271247461903,2.8284271247461903,2.8284271247461903,2.8284271247461903,2.8284271247461903,2.8284271247461903,2.8284271247461903,2.8284271247461903,2.8284271247461903,2.8284271247461903,2.8301943396169813,2.8301943396169813,2.8301943396169813,2.8301943396169813,2.8301943396169813,2.8301943396169813,2.8301943396169813,2.8301943396169813,2.8319604517012595,2.8319604517012595,2.8319604517012595,2.8319604517012595,2.8319604517012595,2.8319604517012595,2.8319604517012595,2.8319604517012595,2.8425340807103794,2.8425340807103794,2.8425340807103794,2.8425340807103794,2.8425340807103794,2.8425340807103794,2.8425340807103794,2.8425340807103794,2.8442925306655784,2.8442925306655784,2.8442925306655784,2.8442925306655784,2.8442925306655784,2.8442925306655784,2.8442925306655784,2.8442925306655784,2.8460498941515415,2.8460498941515415,2.8460498941515415,2.8460498941515415,2.8460498941515415,2.8460498941515415,2.8460498941515415,2.8460498941515415,2.8600699292150185,2.8600699292150185,2.8600699292150185,2.8600699292150185,2.8600699292150185,2.8600699292150185,2.8600699292150185,2.8600699292150185,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.863564212655271,2.8653097563788807,2.8653097563788807,2.8653097563788807,2.8653097563788807,2.8653097563788807,2.8653097563788807,2.8653097563788807,2.8653097563788807,2.879236009777594,2.879236009777594,2.879236009777594,2.879236009777594,2.879236009777594,2.879236009777594,2.879236009777594,2.879236009777594,2.8844410203711917,2.8844410203711917,2.8844410203711917,2.8844410203711917,2.8844410203711917,2.8844410203711917,2.8844410203711917,2.8844410203711917,2.8861739379323623,2.8861739379323623,2.8861739379323623,2.8861739379323623,2.8861739379323623,2.8861739379323623,2.8861739379323623,2.8861739379323623,2.9000000000000004,2.9000000000000004,2.9000000000000004,2.9000000000000004,2.9000000000000004,2.9000000000000004,2.9000000000000004,2.9000000000000004,2.9000000000000004,2.9000000000000004,2.9000000000000004,2.9000000000000004,2.9017236257093817,2.9017236257093817,2.9017236257093817,2.9017236257093817,2.9017236257093817,2.9017236257093817,2.9017236257093817,2.9017236257093817,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.906888370749727,2.9120439557122073,2.9120439557122073,2.9120439557122073,2.9120439557122073,2.9120439557122073,2.9120439557122073,2.9120439557122073,2.9120439557122073,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.9154759474226504,2.920616373302047,2.920616373302047,2.920616373302047,2.920616373302047,2.920616373302047,2.920616373302047,2.920616373302047,2.920616373302047,2.9274562336608896,2.9274562336608896,2.9274562336608896,2.9274562336608896,2.9274562336608896,2.9274562336608896,2.9274562336608896,2.9274562336608896,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9410882339705484,2.9427877939124323,2.9427877939124323,2.9427877939124323,2.9427877939124323,2.9427877939124323,2.9427877939124323,2.9427877939124323,2.9427877939124323,2.95296461204668,2.95296461204668,2.95296461204668,2.95296461204668,2.95296461204668,2.95296461204668,2.95296461204668,2.95296461204668,2.954657340538832,2.954657340538832,2.954657340538832,2.954657340538832,2.954657340538832,2.954657340538832,2.954657340538832,2.954657340538832,2.96141857899217,2.96141857899217,2.96141857899217,2.96141857899217,2.96141857899217,2.96141857899217,2.96141857899217,2.96141857899217,2.968164415931166,2.968164415931166,2.968164415931166,2.968164415931166,2.968164415931166,2.968164415931166,2.968164415931166,2.968164415931166,2.9698484809834995,2.9698484809834995,2.9698484809834995,2.9698484809834995,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.973213749463701,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.98328677803526,2.9966648127543394,2.9966648127543394,2.9966648127543394,2.9966648127543394,2.9966648127543394,2.9966648127543394,2.9966648127543394,2.9966648127543394,3.0,3.0,3.0,3.0,3.0,3.0,3.0,3.0,3.0,3.0,3.0,3.0,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.001666203960727,3.006659275674582,3.006659275674582,3.006659275674582,3.006659275674582,3.006659275674582,3.006659275674582,3.006659275674582,3.006659275674582,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.0083217912982647,3.014962686336267,3.014962686336267,3.014962686336267,3.014962686336267,3.014962686336267,3.014962686336267,3.014962686336267,3.014962686336267,3.023243291566195,3.023243291566195,3.023243291566195,3.023243291566195,3.023243291566195,3.023243291566195,3.023243291566195,3.023243291566195,3.0265491900843116,3.0265491900843116,3.0265491900843116,3.0265491900843116,3.0265491900843116,3.0265491900843116,3.0265491900843116,3.0265491900843116,3.036445290137795,3.036445290137795,3.036445290137795,3.036445290137795,3.036445290137795,3.036445290137795,3.036445290137795,3.036445290137795,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.04138126514911,3.0463092423455636,3.0463092423455636,3.0463092423455636,3.0463092423455636,3.0463092423455636,3.0463092423455636,3.0463092423455636,3.0463092423455636,3.0479501308256345,3.0479501308256345,3.0479501308256345,3.0479501308256345,3.0479501308256345,3.0479501308256345,3.0479501308256345,3.0479501308256345,3.0528675044947495,3.0528675044947495,3.0528675044947495,3.0528675044947495,3.0528675044947495,3.0528675044947495,3.0528675044947495,3.0528675044947495,3.0594117081556713,3.0594117081556713,3.0594117081556713,3.0594117081556713,3.0594117081556713,3.0594117081556713,3.0594117081556713,3.0594117081556713,3.0610455730027937,3.0610455730027937,3.0610455730027937,3.0610455730027937,3.0610455730027937,3.0610455730027937,3.0610455730027937,3.0610455730027937,3.0675723300355937,3.0675723300355937,3.0675723300355937,3.0675723300355937,3.0675723300355937,3.0675723300355937,3.0675723300355937,3.0675723300355937,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0805843601498726,3.0870698080866266,3.0870698080866266,3.0870698080866266,3.0870698080866266,3.0870698080866266,3.0870698080866266,3.0870698080866266,3.0870698080866266,3.0886890422961004,3.0886890422961004,3.0886890422961004,3.0886890422961004,3.0886890422961004,3.0886890422961004,3.0886890422961004,3.0886890422961004,3.1,3.1,3.1,3.1,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.101612483854165,3.104834939252005,3.104834939252005,3.104834939252005,3.104834939252005,3.104834939252005,3.104834939252005,3.104834939252005,3.104834939252005,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.1064449134018135,3.111269837220809,3.111269837220809,3.111269837220809,3.111269837220809,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1144823004794873,3.1240998703626617,3.1240998703626617,3.1240998703626617,3.1240998703626617,3.1240998703626617,3.1240998703626617,3.1240998703626617,3.1240998703626617,3.125699921617557,3.125699921617557,3.125699921617557,3.125699921617557,3.125699921617557,3.125699921617557,3.125699921617557,3.125699921617557,3.130495168499706,3.130495168499706,3.130495168499706,3.130495168499706,3.130495168499706,3.130495168499706,3.130495168499706,3.130495168499706,3.132091952673165,3.132091952673165,3.132091952673165,3.132091952673165,3.132091952673165,3.132091952673165,3.132091952673165,3.132091952673165,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1384709652950433,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.1400636936215167,3.157530680769389,3.157530680769389,3.157530680769389,3.157530680769389,3.157530680769389,3.157530680769389,3.157530680769389,3.157530680769389,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.1622776601683795,3.176476034853718,3.176476034853718,3.176476034853718,3.176476034853718,3.176476034853718,3.176476034853718,3.176476034853718,3.176476034853718,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.178049716414141,3.1827660925679098,3.1827660925679098,3.1827660925679098,3.1827660925679098,3.1827660925679098,3.1827660925679098,3.1827660925679098,3.1827660925679098,3.1890437438203953,3.1890437438203953,3.1890437438203953,3.1890437438203953,3.1890437438203953,3.1890437438203953,3.1890437438203953,3.1890437438203953,3.1906112267087634,3.1906112267087634,3.1906112267087634,3.1906112267087634,3.1906112267087634,3.1906112267087634,3.1906112267087634,3.1906112267087634,3.1953090617340916,3.1953090617340916,3.1953090617340916,3.1953090617340916,3.1953090617340916,3.1953090617340916,3.1953090617340916,3.1953090617340916,3.2,3.2,3.2,3.2,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2015621187164243,3.2062439083762797,3.2062439083762797,3.2062439083762797,3.2062439083762797,3.2062439083762797,3.2062439083762797,3.2062439083762797,3.2062439083762797,3.2140317359976396,3.2140317359976396,3.2140317359976396,3.2140317359976396,3.2140317359976396,3.2140317359976396,3.2140317359976396,3.2140317359976396,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2202484376209237,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.2249030993194197,3.228002478313795,3.228002478313795,3.228002478313795,3.228002478313795,3.228002478313795,3.228002478313795,3.228002478313795,3.228002478313795,3.2310988842807022,3.2310988842807022,3.2310988842807022,3.2310988842807022,3.2310988842807022,3.2310988842807022,3.2310988842807022,3.2310988842807022,3.2388269481403293,3.2388269481403293,3.2388269481403293,3.2388269481403293,3.2388269481403293,3.2388269481403293,3.2388269481403293,3.2388269481403293,3.2449961479175906,3.2449961479175906,3.2449961479175906,3.2449961479175906,3.2449961479175906,3.2449961479175906,3.2449961479175906,3.2449961479175906,3.252691193458119,3.252691193458119,3.252691193458119,3.252691193458119,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.2557641192199416,3.257299494980466,3.257299494980466,3.257299494980466,3.257299494980466,3.257299494980466,3.257299494980466,3.257299494980466,3.257299494980466,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.264965543462902,3.2695565448543635,3.2695565448543635,3.2695565448543635,3.2695565448543635,3.2695565448543635,3.2695565448543635,3.2695565448543635,3.2695565448543635,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.27566787083184,3.2802438933713454,3.2802438933713454,3.2802438933713454,3.2802438933713454,3.2802438933713454,3.2802438933713454,3.2802438933713454,3.2802438933713454,3.2893768406797053,3.2893768406797053,3.2893768406797053,3.2893768406797053,3.2893768406797053,3.2893768406797053,3.2893768406797053,3.2893768406797053,3.2984845004941286,3.2984845004941286,3.2984845004941286,3.2984845004941286,3.2984845004941286,3.2984845004941286,3.2984845004941286,3.2984845004941286,3.3000000000000003,3.3000000000000003,3.3000000000000003,3.3000000000000003,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3015148038438356,3.3060550509633084,3.3060550509633084,3.3060550509633084,3.3060550509633084,3.3060550509633084,3.3060550509633084,3.3060550509633084,3.3060550509633084,3.3105890714493698,3.3105890714493698,3.3105890714493698,3.3105890714493698,3.3105890714493698,3.3105890714493698,3.3105890714493698,3.3105890714493698,3.3120990323358392,3.3120990323358392,3.3120990323358392,3.3120990323358392,3.3120990323358392,3.3120990323358392,3.3120990323358392,3.3120990323358392,3.3136083051561784,3.3136083051561784,3.3136083051561784,3.3136083051561784,3.3136083051561784,3.3136083051561784,3.3136083051561784,3.3136083051561784,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3241540277189325,3.3286633954186478,3.3286633954186478,3.3286633954186478,3.3286633954186478,3.3286633954186478,3.3286633954186478,3.3286633954186478,3.3286633954186478,3.330165161069343,3.330165161069343,3.330165161069343,3.330165161069343,3.330165161069343,3.330165161069343,3.330165161069343,3.330165161069343,3.3376638536557275,3.3376638536557275,3.3376638536557275,3.3376638536557275,3.3376638536557275,3.3376638536557275,3.3376638536557275,3.3376638536557275,3.342154993413681,3.342154993413681,3.342154993413681,3.342154993413681,3.342154993413681,3.342154993413681,3.342154993413681,3.342154993413681,3.3526109228480423,3.3526109228480423,3.3526109228480423,3.3526109228480423,3.3526109228480423,3.3526109228480423,3.3526109228480423,3.3526109228480423,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3541019662496847,3.3600595232822883,3.3600595232822883,3.3600595232822883,3.3600595232822883,3.3600595232822883,3.3600595232822883,3.3600595232822883,3.3600595232822883,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.361547262794322,3.3734255586866,3.3734255586866,3.3734255586866,3.3734255586866,3.3734255586866,3.3734255586866,3.3734255586866,3.3734255586866,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.3837848631377265,3.394112549695428,3.394112549695428,3.394112549695428,3.394112549695428,3.3955853692699294,3.3955853692699294,3.3955853692699294,3.3955853692699294,3.3955853692699294,3.3955853692699294,3.3955853692699294,3.3955853692699294,3.3970575502926064,3.3970575502926064,3.3970575502926064,3.3970575502926064,3.3970575502926064,3.3970575502926064,3.3970575502926064,3.3970575502926064,3.4000000000000004,3.4000000000000004,3.4000000000000004,3.4000000000000004,3.4000000000000004,3.4000000000000004,3.4000000000000004,3.4000000000000004,3.4000000000000004,3.4000000000000004,3.4000000000000004,3.4000000000000004,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.40147027033899,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.405877273185281,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4132096331752027,3.4176014981270124,3.4176014981270124,3.4176014981270124,3.4176014981270124,3.4176014981270124,3.4176014981270124,3.4176014981270124,3.4176014981270124,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.4205262752974144,3.423448553724738,3.423448553724738,3.423448553724738,3.423448553724738,3.423448553724738,3.423448553724738,3.423448553724738,3.423448553724738,3.4365680554879163,3.4365680554879163,3.4365680554879163,3.4365680554879163,3.4365680554879163,3.4365680554879163,3.4365680554879163,3.4365680554879163,3.440930106817051,3.440930106817051,3.440930106817051,3.440930106817051,3.440930106817051,3.440930106817051,3.440930106817051,3.440930106817051,3.4438350715445125,3.4438350715445125,3.4438350715445125,3.4438350715445125,3.4438350715445125,3.4438350715445125,3.4438350715445125,3.4438350715445125,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4481879299133333,3.4525353003264136,3.4525353003264136,3.4525353003264136,3.4525353003264136,3.4525353003264136,3.4525353003264136,3.4525353003264136,3.4525353003264136,3.4539832078341086,3.4539832078341086,3.4539832078341086,3.4539832078341086,3.4539832078341086,3.4539832078341086,3.4539832078341086,3.4539832078341086,3.465544690232692,3.465544690232692,3.465544690232692,3.465544690232692,3.465544690232692,3.465544690232692,3.465544690232692,3.465544690232692,3.4669871646719432,3.4669871646719432,3.4669871646719432,3.4669871646719432,3.4669871646719432,3.4669871646719432,3.4669871646719432,3.4669871646719432,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.471310991541957,3.4785054261852175,3.4785054261852175,3.4785054261852175,3.4785054261852175,3.4785054261852175,3.4785054261852175,3.4785054261852175,3.4785054261852175,3.4828149534536,3.4828149534536,3.4828149534536,3.4828149534536,3.4828149534536,3.4828149534536,3.4828149534536,3.4828149534536,3.488552708502482,3.488552708502482,3.488552708502482,3.488552708502482,3.488552708502482,3.488552708502482,3.488552708502482,3.488552708502482,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.492849839314596,3.4985711369071804,3.4985711369071804,3.4985711369071804,3.4985711369071804,3.4985711369071804,3.4985711369071804,3.4985711369071804,3.4985711369071804,3.5,3.5,3.5,3.5,3.5,3.5,3.5,3.5,3.5,3.5,3.5,3.5,3.5014282800023198,3.5014282800023198,3.5014282800023198,3.5014282800023198,3.5014282800023198,3.5014282800023198,3.5014282800023198,3.5014282800023198,3.505709628591621,3.505709628591621,3.505709628591621,3.505709628591621,3.505709628591621,3.505709628591621,3.505709628591621,3.505709628591621,3.511409973215888,3.511409973215888,3.511409973215888,3.511409973215888,3.511409973215888,3.511409973215888,3.511409973215888,3.511409973215888,3.5128336140500593,3.5128336140500593,3.5128336140500593,3.5128336140500593,3.5128336140500593,3.5128336140500593,3.5128336140500593,3.5128336140500593,3.517101079013795,3.517101079013795,3.517101079013795,3.517101079013795,3.517101079013795,3.517101079013795,3.517101079013795,3.517101079013795,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.522782990761707,3.5341194094144583,3.5341194094144583,3.5341194094144583,3.5341194094144583,3.5341194094144583,3.5341194094144583,3.5341194094144583,3.5341194094144583,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5355339059327378,3.5383612025908264,3.5383612025908264,3.5383612025908264,3.5383612025908264,3.5383612025908264,3.5383612025908264,3.5383612025908264,3.5383612025908264,3.54400902933387,3.54400902933387,3.54400902933387,3.54400902933387,3.54400902933387,3.54400902933387,3.54400902933387,3.54400902933387,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.54682957019364,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.5510561809129406,3.560898762952972,3.560898762952972,3.560898762952972,3.560898762952972,3.560898762952972,3.560898762952972,3.560898762952972,3.560898762952972,3.56931365951495,3.56931365951495,3.56931365951495,3.56931365951495,3.56931365951495,3.56931365951495,3.56931365951495,3.56931365951495,3.5735136770411273,3.5735136770411273,3.5735136770411273,3.5735136770411273,3.5735136770411273,3.5735136770411273,3.5735136770411273,3.5735136770411273,3.577708763999664,3.577708763999664,3.577708763999664,3.577708763999664,3.577708763999664,3.577708763999664,3.577708763999664,3.577708763999664,3.5805027579936315,3.5805027579936315,3.5805027579936315,3.5805027579936315,3.5805027579936315,3.5805027579936315,3.5805027579936315,3.5805027579936315,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.5846896657869842,3.590264614203248,3.590264614203248,3.590264614203248,3.590264614203248,3.590264614203248,3.590264614203248,3.590264614203248,3.590264614203248,3.6,3.6,3.6,3.6,3.6013886210738217,3.6013886210738217,3.6013886210738217,3.6013886210738217,3.6013886210738217,3.6013886210738217,3.6013886210738217,3.6013886210738217,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6055512754639896,3.6069377593742864,3.6069377593742864,3.6069377593742864,3.6069377593742864,3.6069377593742864,3.6069377593742864,3.6069377593742864,3.6069377593742864,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6124783736376886,3.6138621999185307,3.6138621999185307,3.6138621999185307,3.6138621999185307,3.6138621999185307,3.6138621999185307,3.6138621999185307,3.6138621999185307,3.622154055254967,3.622154055254967,3.622154055254967,3.622154055254967,3.622154055254967,3.622154055254967,3.622154055254967,3.622154055254967,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.623534186398688,3.624913792078372,3.624913792078372,3.624913792078372,3.624913792078372,3.624913792078372,3.624913792078372,3.624913792078372,3.624913792078372,3.6345563690772495,3.6345563690772495,3.6345563690772495,3.6345563690772495,3.6345563690772495,3.6345563690772495,3.6345563690772495,3.6345563690772495,3.635931792539569,3.635931792539569,3.635931792539569,3.635931792539569,3.635931792539569,3.635931792539569,3.635931792539569,3.635931792539569,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.6400549446402595,3.649657518178932,3.649657518178932,3.649657518178932,3.649657518178932,3.649657518178932,3.649657518178932,3.649657518178932,3.649657518178932,3.6619666847201113,3.6619666847201113,3.6619666847201113,3.6619666847201113,3.6619666847201113,3.6619666847201113,3.6619666847201113,3.6619666847201113,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.66742416417845,3.6687872655688283,3.6687872655688283,3.6687872655688283,3.6687872655688283,3.6687872655688283,3.6687872655688283,3.6687872655688283,3.6687872655688283,3.6715119501371642,3.6715119501371642,3.6715119501371642,3.6715119501371642,3.6715119501371642,3.6715119501371642,3.6715119501371642,3.6715119501371642,3.676955262170047,3.676955262170047,3.676955262170047,3.676955262170047,3.676955262170047,3.676955262170047,3.676955262170047,3.676955262170047,3.676955262170047,3.676955262170047,3.676955262170047,3.676955262170047,3.6796738985948196,3.6796738985948196,3.6796738985948196,3.6796738985948196,3.6796738985948196,3.6796738985948196,3.6796738985948196,3.6796738985948196,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.687817782917155,3.6891733491393435,3.6891733491393435,3.6891733491393435,3.6891733491393435,3.6891733491393435,3.6891733491393435,3.6891733491393435,3.6891733491393435,3.7,3.7,3.7,3.7,3.7,3.7,3.7,3.7,3.7,3.7,3.7,3.7,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7013511046643495,3.7054014627297813,3.7054014627297813,3.7054014627297813,3.7054014627297813,3.7054014627297813,3.7054014627297813,3.7054014627297813,3.7054014627297813,3.710795063055895,3.710795063055895,3.710795063055895,3.710795063055895,3.710795063055895,3.710795063055895,3.710795063055895,3.710795063055895,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.712142238654117,3.716180835212409,3.716180835212409,3.716180835212409,3.716180835212409,3.716180835212409,3.716180835212409,3.716180835212409,3.716180835212409,3.720215047547655,3.720215047547655,3.720215047547655,3.720215047547655,3.720215047547655,3.720215047547655,3.720215047547655,3.720215047547655,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.721558813185679,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.7336309405188945,3.736308338453881,3.736308338453881,3.736308338453881,3.736308338453881,3.736308338453881,3.736308338453881,3.736308338453881,3.736308338453881,3.7443290453698115,3.7443290453698115,3.7443290453698115,3.7443290453698115,3.7443290453698115,3.7443290453698115,3.7443290453698115,3.7443290453698115,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.748332962798263,3.753664875824692,3.753664875824692,3.753664875824692,3.753664875824692,3.753664875824692,3.753664875824692,3.753664875824692,3.753664875824692,3.7576588456111875,3.7576588456111875,3.7576588456111875,3.7576588456111875,3.7576588456111875,3.7576588456111875,3.7576588456111875,3.7576588456111875,3.7589892258425004,3.7589892258425004,3.7589892258425004,3.7589892258425004,3.7589892258425004,3.7589892258425004,3.7589892258425004,3.7589892258425004,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7643060449437424,3.7656340767525465,3.7656340767525465,3.7656340767525465,3.7656340767525465,3.7656340767525465,3.7656340767525465,3.7656340767525465,3.7656340767525465,3.769615364994153,3.769615364994153,3.769615364994153,3.769615364994153,3.769615364994153,3.769615364994153,3.769615364994153,3.769615364994153,3.7735924528226414,3.7735924528226414,3.7735924528226414,3.7735924528226414,3.7735924528226414,3.7735924528226414,3.7735924528226414,3.7735924528226414,3.78021163428716,3.78021163428716,3.78021163428716,3.78021163428716,3.78021163428716,3.78021163428716,3.78021163428716,3.78021163428716,3.78549864614954,3.78549864614954,3.78549864614954,3.78549864614954,3.78549864614954,3.78549864614954,3.78549864614954,3.78549864614954,3.7947331922020555,3.7947331922020555,3.7947331922020555,3.7947331922020555,3.7947331922020555,3.7947331922020555,3.7947331922020555,3.7947331922020555,3.8000000000000003,3.8000000000000003,3.8000000000000003,3.8000000000000003,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8013155617496426,3.8052595180880893,3.8052595180880893,3.8052595180880893,3.8052595180880893,3.8052595180880893,3.8052595180880893,3.8052595180880893,3.8052595180880893,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.8078865529319543,3.811823710509184,3.811823710509184,3.811823710509184,3.811823710509184,3.811823710509184,3.811823710509184,3.811823710509184,3.811823710509184,3.818376618407357,3.818376618407357,3.818376618407357,3.818376618407357,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.82099463490856,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.827531841800928,3.8288379438153295,3.8288379438153295,3.8288379438153295,3.8288379438153295,3.8288379438153295,3.8288379438153295,3.8288379438153295,3.8288379438153295,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.83275357934736,3.8418745424597094,3.8418745424597094,3.8418745424597094,3.8418745424597094,3.8418745424597094,3.8418745424597094,3.8418745424597094,3.8418745424597094,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8470768123342687,3.8483762809787714,3.8483762809787714,3.8483762809787714,3.8483762809787714,3.8483762809787714,3.8483762809787714,3.8483762809787714,3.8483762809787714,3.858756276314948,3.858756276314948,3.858756276314948,3.858756276314948,3.858756276314948,3.858756276314948,3.858756276314948,3.858756276314948,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8600518131237567,3.8626415831655936,3.8626415831655936,3.8626415831655936,3.8626415831655936,3.8626415831655936,3.8626415831655936,3.8626415831655936,3.8626415831655936,3.8639358172723313,3.8639358172723313,3.8639358172723313,3.8639358172723313,3.8639358172723313,3.8639358172723313,3.8639358172723313,3.8639358172723313,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.88329756778952,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8897300677553446,3.8910152916687437,3.8910152916687437,3.8910152916687437,3.8910152916687437,3.8910152916687437,3.8910152916687437,3.8910152916687437,3.8910152916687437,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.8948684188300895,3.9000000000000004,3.9000000000000004,3.9000000000000004,3.9000000000000004,3.9000000000000004,3.9000000000000004,3.9000000000000004,3.9000000000000004,3.9000000000000004,3.9000000000000004,3.9000000000000004,3.9000000000000004,3.901281840626232,3.901281840626232,3.901281840626232,3.901281840626232,3.901281840626232,3.901281840626232,3.901281840626232,3.901281840626232,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.905124837953327,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.9115214431215892,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.920459156782532,3.9217343102255158,3.9217343102255158,3.9217343102255158,3.9217343102255158,3.9217343102255158,3.9217343102255158,3.9217343102255158,3.9217343102255158,3.9293765408777,3.9293765408777,3.9293765408777,3.9293765408777,3.9293765408777,3.9293765408777,3.9293765408777,3.9293765408777,3.9319206502675,3.9319206502675,3.9319206502675,3.9319206502675,3.9319206502675,3.9319206502675,3.9319206502675,3.9319206502675,3.935733730830886,3.935733730830886,3.935733730830886,3.935733730830886,3.935733730830886,3.935733730830886,3.935733730830886,3.935733730830886,3.939543120718442,3.939543120718442,3.939543120718442,3.939543120718442,3.939543120718442,3.939543120718442,3.939543120718442,3.939543120718442,3.9408120990476068,3.9408120990476068,3.9408120990476068,3.9408120990476068,3.9408120990476068,3.9408120990476068,3.9408120990476068,3.9408120990476068,3.9446165846632044,3.9446165846632044,3.9446165846632044,3.9446165846632044,3.9446165846632044,3.9446165846632044,3.9446165846632044,3.9446165846632044,3.9458839313897722,3.9458839313897722,3.9458839313897722,3.9458839313897722,3.9458839313897722,3.9458839313897722,3.9458839313897722,3.9458839313897722,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9560080889704965,3.9597979746446663,3.9597979746446663,3.9597979746446663,3.9597979746446663,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.9623225512317903,3.966106403010388,3.966106403010388,3.966106403010388,3.966106403010388,3.966106403010388,3.966106403010388,3.966106403010388,3.966106403010388,3.9698866482558417,3.9698866482558417,3.9698866482558417,3.9698866482558417,3.9698866482558417,3.9698866482558417,3.9698866482558417,3.9698866482558417,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.9812058474788765,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.982461550347976,3.984971769034255,3.984971769034255,3.984971769034255,3.984971769034255,3.984971769034255,3.984971769034255,3.984971769034255,3.984971769034255,3.9924929555354263,3.9924929555354263,3.9924929555354263,3.9924929555354263,3.9924929555354263,3.9924929555354263,3.9924929555354263,3.9924929555354263,3.9962482405376174,3.9962482405376174,3.9962482405376174,3.9962482405376174,3.9962482405376174,3.9962482405376174,3.9962482405376174,3.9962482405376174,4.0,4.0,4.0,4.0,4.0,4.0,4.0,4.0,4.0,4.0,4.0,4.0,4.001249804748511,4.001249804748511,4.001249804748511,4.001249804748511,4.001249804748511,4.001249804748511,4.001249804748511,4.001249804748511,4.0024992192379,4.0024992192379,4.0024992192379,4.0024992192379,4.0024992192379,4.0024992192379,4.0024992192379,4.0024992192379,4.004996878900157,4.004996878900157,4.004996878900157,4.004996878900157,4.004996878900157,4.004996878900157,4.004996878900157,4.004996878900157,4.011234224026317,4.011234224026317,4.011234224026317,4.011234224026317,4.011234224026317,4.011234224026317,4.011234224026317,4.011234224026317,4.016217125604641,4.016217125604641,4.016217125604641,4.016217125604641,4.016217125604641,4.016217125604641,4.016217125604641,4.016217125604641,4.019950248448356,4.019950248448356,4.019950248448356,4.019950248448356,4.019950248448356,4.019950248448356,4.019950248448356,4.019950248448356,4.022437072223753,4.022437072223753,4.022437072223753,4.022437072223753,4.022437072223753,4.022437072223753,4.022437072223753,4.022437072223753,4.024922359499622,4.024922359499622,4.024922359499622,4.024922359499622,4.024922359499622,4.024922359499622,4.024922359499622,4.024922359499622,4.026164427839479,4.026164427839479,4.026164427839479,4.026164427839479,4.026164427839479,4.026164427839479,4.026164427839479,4.026164427839479,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.031128874149275,4.036087214122113,4.036087214122113,4.036087214122113,4.036087214122113,4.036087214122113,4.036087214122113,4.036087214122113,4.036087214122113,4.044749683231337,4.044749683231337,4.044749683231337,4.044749683231337,4.044749683231337,4.044749683231337,4.044749683231337,4.044749683231337,4.045985664828782,4.045985664828782,4.045985664828782,4.045985664828782,4.045985664828782,4.045985664828782,4.045985664828782,4.045985664828782,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.049691346263318,4.052159917870962,4.052159917870962,4.052159917870962,4.052159917870962,4.052159917870962,4.052159917870962,4.052159917870962,4.052159917870962,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.060788100849391,4.070626487409524,4.070626487409524,4.070626487409524,4.070626487409524,4.070626487409524,4.070626487409524,4.070626487409524,4.070626487409524,4.071854614300467,4.071854614300467,4.071854614300467,4.071854614300467,4.071854614300467,4.071854614300467,4.071854614300467,4.071854614300467,4.079215610874228,4.079215610874228,4.079215610874228,4.079215610874228,4.079215610874228,4.079215610874228,4.079215610874228,4.079215610874228,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.080441152620633,4.08166632639171,4.08166632639171,4.08166632639171,4.08166632639171,4.08166632639171,4.08166632639171,4.08166632639171,4.08166632639171,4.085339643163099,4.085339643163099,4.085339643163099,4.085339643163099,4.085339643163099,4.085339643163099,4.085339643163099,4.085339643163099,4.1000000000000005,4.1000000000000005,4.1000000000000005,4.1000000000000005,4.1000000000000005,4.1000000000000005,4.1000000000000005,4.1000000000000005,4.1000000000000005,4.1000000000000005,4.1000000000000005,4.1000000000000005,4.1012193308819755,4.1012193308819755,4.1012193308819755,4.1012193308819755,4.1012193308819755,4.1012193308819755,4.1012193308819755,4.1012193308819755,4.1012193308819755,4.1012193308819755,4.1012193308819755,4.1012193308819755,4.1036569057366385,4.1036569057366385,4.1036569057366385,4.1036569057366385,4.1036569057366385,4.1036569057366385,4.1036569057366385,4.1036569057366385,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.104875150354759,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.110960958218893,4.114608122288197,4.114608122288197,4.114608122288197,4.114608122288197,4.114608122288197,4.114608122288197,4.114608122288197,4.114608122288197,4.118252056394801,4.118252056394801,4.118252056394801,4.118252056394801,4.118252056394801,4.118252056394801,4.118252056394801,4.118252056394801,4.119465984809196,4.119465984809196,4.119465984809196,4.119465984809196,4.119465984809196,4.119465984809196,4.119465984809196,4.119465984809196,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.123105625617661,4.1303752856126765,4.1303752856126765,4.1303752856126765,4.1303752856126765,4.1303752856126765,4.1303752856126765,4.1303752856126765,4.1303752856126765,4.1340053217188775,4.1340053217188775,4.1340053217188775,4.1340053217188775,4.1340053217188775,4.1340053217188775,4.1340053217188775,4.1340053217188775,4.1400483088968905,4.1400483088968905,4.1400483088968905,4.1400483088968905,4.1400483088968905,4.1400483088968905,4.1400483088968905,4.1400483088968905,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.1436698710201325,4.148493702538309,4.148493702538309,4.148493702538309,4.148493702538309,4.148493702538309,4.148493702538309,4.148493702538309,4.148493702538309,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.159326868617085,4.161730409336963,4.161730409336963,4.161730409336963,4.161730409336963,4.161730409336963,4.161730409336963,4.161730409336963,4.161730409336963,4.16293165929973,4.16293165929973,4.16293165929973,4.16293165929973,4.16293165929973,4.16293165929973,4.16293165929973,4.16293165929973,4.167733196834941,4.167733196834941,4.167733196834941,4.167733196834941,4.167733196834941,4.167733196834941,4.167733196834941,4.167733196834941,4.172529209005013,4.172529209005013,4.172529209005013,4.172529209005013,4.172529209005013,4.172529209005013,4.172529209005013,4.172529209005013,4.17612260356422,4.17612260356422,4.17612260356422,4.17612260356422,4.17612260356422,4.17612260356422,4.17612260356422,4.17612260356422,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.177319714841085,4.178516483155236,4.178516483155236,4.178516483155236,4.178516483155236,4.178516483155236,4.178516483155236,4.178516483155236,4.178516483155236,4.186884283091664,4.186884283091664,4.186884283091664,4.186884283091664,4.186884283091664,4.186884283091664,4.186884283091664,4.186884283091664,4.188078318274385,4.188078318274385,4.188078318274385,4.188078318274385,4.188078318274385,4.188078318274385,4.188078318274385,4.188078318274385,4.197618372363071,4.197618372363071,4.197618372363071,4.197618372363071,4.197618372363071,4.197618372363071,4.197618372363071,4.197618372363071,4.2,4.2,4.2,4.2,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.201190307520001,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.204759208325728,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.205948168962618,4.21070065428546,4.21070065428546,4.21070065428546,4.21070065428546,4.21070065428546,4.21070065428546,4.21070065428546,4.21070065428546,4.215447781671599,4.215447781671599,4.215447781671599,4.215447781671599,4.215447781671599,4.215447781671599,4.215447781671599,4.215447781671599,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.219004621945798,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.220189569201838,4.2296571965113205,4.2296571965113205,4.2296571965113205,4.2296571965113205,4.2296571965113205,4.2296571965113205,4.2296571965113205,4.2296571965113205,4.237924020083418,4.237924020083418,4.237924020083418,4.237924020083418,4.237924020083418,4.237924020083418,4.237924020083418,4.237924020083418,4.242640687119286,4.242640687119286,4.242640687119286,4.242640687119286,4.242640687119286,4.242640687119286,4.242640687119286,4.242640687119286,4.242640687119286,4.242640687119286,4.242640687119286,4.242640687119286,4.243819034784589,4.243819034784589,4.243819034784589,4.243819034784589,4.243819034784589,4.243819034784589,4.243819034784589,4.243819034784589,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.244997055358225,4.2485291572496005,4.2485291572496005,4.2485291572496005,4.2485291572496005,4.2485291572496005,4.2485291572496005,4.2485291572496005,4.2485291572496005,4.25205832509386,4.25205832509386,4.25205832509386,4.25205832509386,4.25205832509386,4.25205832509386,4.25205832509386,4.25205832509386,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.25440947723653,4.257933771208754,4.257933771208754,4.257933771208754,4.257933771208754,4.257933771208754,4.257933771208754,4.257933771208754,4.257933771208754,4.263801121065568,4.263801121065568,4.263801121065568,4.263801121065568,4.263801121065568,4.263801121065568,4.263801121065568,4.263801121065568,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.272001872658765,4.27551166528639,4.27551166528639,4.27551166528639,4.27551166528639,4.27551166528639,4.27551166528639,4.27551166528639,4.27551166528639,4.280186911806539,4.280186911806539,4.280186911806539,4.280186911806539,4.280186911806539,4.280186911806539,4.280186911806539,4.280186911806539,4.294182110716778,4.294182110716778,4.294182110716778,4.294182110716778,4.294182110716778,4.294182110716778,4.294182110716778,4.294182110716778,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.295346318982906,4.3,4.3,4.3,4.3,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.301162633521314,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.304648650006177,4.308131845707603,4.308131845707603,4.308131845707603,4.308131845707603,4.308131845707603,4.308131845707603,4.308131845707603,4.308131845707603,4.310452412450462,4.310452412450462,4.310452412450462,4.310452412450462,4.310452412450462,4.310452412450462,4.310452412450462,4.310452412450462,4.313930922024599,4.313930922024599,4.313930922024599,4.313930922024599,4.313930922024599,4.313930922024599,4.313930922024599,4.313930922024599,4.317406628984581,4.317406628984581,4.317406628984581,4.317406628984581,4.317406628984581,4.317406628984581,4.317406628984581,4.317406628984581,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.318564576337837,4.3266615305567875,4.3266615305567875,4.3266615305567875,4.3266615305567875,4.3266615305567875,4.3266615305567875,4.3266615305567875,4.3266615305567875,4.327817001676481,4.327817001676481,4.327817001676481,4.327817001676481,4.327817001676481,4.327817001676481,4.327817001676481,4.327817001676481,4.328972164382672,4.328972164382672,4.328972164382672,4.328972164382672,4.328972164382672,4.328972164382672,4.328972164382672,4.328972164382672,4.332435804486894,4.332435804486894,4.332435804486894,4.332435804486894,4.332435804486894,4.332435804486894,4.332435804486894,4.332435804486894,4.338202392696773,4.338202392696773,4.338202392696773,4.338202392696773,4.338202392696773,4.338202392696773,4.338202392696773,4.338202392696773,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.341658669218482,4.346262762420146,4.346262762420146,4.346262762420146,4.346262762420146,4.346262762420146,4.346262762420146,4.346262762420146,4.346262762420146,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.356604182158393,4.360045871318328,4.360045871318328,4.360045871318328,4.360045871318328,4.360045871318328,4.360045871318328,4.360045871318328,4.360045871318328,4.365775990588615,4.365775990588615,4.365775990588615,4.365775990588615,4.365775990588615,4.365775990588615,4.365775990588615,4.365775990588615,4.368065933568311,4.368065933568311,4.368065933568311,4.368065933568311,4.368065933568311,4.368065933568311,4.368065933568311,4.368065933568311,4.373785545725808,4.373785545725808,4.373785545725808,4.373785545725808,4.373785545725808,4.373785545725808,4.373785545725808,4.373785545725808,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.382921400162226,4.384062043356595,4.384062043356595,4.384062043356595,4.384062043356595,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.386342439892262,4.39089968002003,4.39089968002003,4.39089968002003,4.39089968002003,4.39089968002003,4.39089968002003,4.39089968002003,4.39089968002003,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.39317652729776,4.396589587396122,4.396589587396122,4.396589587396122,4.396589587396122,4.396589587396122,4.396589587396122,4.396589587396122,4.396589587396122,4.4,4.4,4.4,4.4,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.401136216933078,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.404543109109048,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.410215414239989,4.4147480109288235,4.4147480109288235,4.4147480109288235,4.4147480109288235,4.4147480109288235,4.4147480109288235,4.4147480109288235,4.4147480109288235,4.418144406874904,4.418144406874904,4.418144406874904,4.418144406874904,4.418144406874904,4.418144406874904,4.418144406874904,4.418144406874904,4.420407221060069,4.420407221060069,4.420407221060069,4.420407221060069,4.420407221060069,4.420407221060069,4.420407221060069,4.420407221060069,4.427188724235731,4.427188724235731,4.427188724235731,4.427188724235731,4.427188724235731,4.427188724235731,4.427188724235731,4.427188724235731,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.428317965096906,4.4294469180700204,4.4294469180700204,4.4294469180700204,4.4294469180700204,4.4294469180700204,4.4294469180700204,4.4294469180700204,4.4294469180700204,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.43846820423443,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.440720662234904,4.4418464629025625,4.4418464629025625,4.4418464629025625,4.4418464629025625,4.4418464629025625,4.4418464629025625,4.4418464629025625,4.4418464629025625,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.455333881989093,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.459820624195552,4.464302857109943,4.464302857109943,4.464302857109943,4.464302857109943,4.464302857109943,4.464302857109943,4.464302857109943,4.464302857109943,4.465422712353222,4.465422712353222,4.465422712353222,4.465422712353222,4.465422712353222,4.465422712353222,4.465422712353222,4.465422712353222,4.468780594300866,4.468780594300866,4.468780594300866,4.468780594300866,4.468780594300866,4.468780594300866,4.468780594300866,4.468780594300866,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.47213595499958,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4777226354476225,4.4821869662029945,4.4821869662029945,4.4821869662029945,4.4821869662029945,4.4821869662029945,4.4821869662029945,4.4821869662029945,4.4821869662029945,4.491102314577124,4.491102314577124,4.491102314577124,4.491102314577124,4.491102314577124,4.491102314577124,4.491102314577124,4.491102314577124,4.49221548904324,4.49221548904324,4.49221548904324,4.49221548904324,4.49221548904324,4.49221548904324,4.49221548904324,4.49221548904324,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.494441010848846,4.5,4.5,4.5,4.5,4.5,4.5,4.5,4.5,4.5,4.5,4.5,4.5,4.50111097397076,4.50111097397076,4.50111097397076,4.50111097397076,4.50111097397076,4.50111097397076,4.50111097397076,4.50111097397076,4.504442251822083,4.504442251822083,4.504442251822083,4.504442251822083,4.504442251822083,4.504442251822083,4.504442251822083,4.504442251822083,4.509988913511872,4.509988913511872,4.509988913511872,4.509988913511872,4.509988913511872,4.509988913511872,4.509988913511872,4.509988913511872,4.512205669071391,4.512205669071391,4.512205669071391,4.512205669071391,4.512205669071391,4.512205669071391,4.512205669071391,4.512205669071391,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.517742799230607,4.518849411078001,4.518849411078001,4.518849411078001,4.518849411078001,4.518849411078001,4.518849411078001,4.518849411078001,4.518849411078001,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.522167621838006,4.525483399593905,4.525483399593905,4.525483399593905,4.525483399593905,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.527692569068709,4.531004303683677,4.531004303683677,4.531004303683677,4.531004303683677,4.531004303683677,4.531004303683677,4.531004303683677,4.531004303683677,4.534313619501854,4.534313619501854,4.534313619501854,4.534313619501854,4.534313619501854,4.534313619501854,4.534313619501854,4.534313619501854,4.535416188179427,4.535416188179427,4.535416188179427,4.535416188179427,4.535416188179427,4.535416188179427,4.535416188179427,4.535416188179427,4.539823785126467,4.539823785126467,4.539823785126467,4.539823785126467,4.539823785126467,4.539823785126467,4.539823785126467,4.539823785126467,4.545327270945405,4.545327270945405,4.545327270945405,4.545327270945405,4.545327270945405,4.545327270945405,4.545327270945405,4.545327270945405,4.548626166217664,4.548626166217664,4.548626166217664,4.548626166217664,4.548626166217664,4.548626166217664,4.548626166217664,4.548626166217664,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.5541190146942805,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.560701700396552,4.561797891182818,4.561797891182818,4.561797891182818,4.561797891182818,4.561797891182818,4.561797891182818,4.561797891182818,4.561797891182818,4.565084884205331,4.565084884205331,4.565084884205331,4.565084884205331,4.565084884205331,4.565084884205331,4.565084884205331,4.565084884205331,4.5694638635183455,4.5694638635183455,4.5694638635183455,4.5694638635183455,4.5694638635183455,4.5694638635183455,4.5694638635183455,4.5694638635183455,4.570557952810576,4.570557952810576,4.570557952810576,4.570557952810576,4.570557952810576,4.570557952810576,4.570557952810576,4.570557952810576,4.5793012567421245,4.5793012567421245,4.5793012567421245,4.5793012567421245,4.5793012567421245,4.5793012567421245,4.5793012567421245,4.5793012567421245,4.580392996239515,4.580392996239515,4.580392996239515,4.580392996239515,4.580392996239515,4.580392996239515,4.580392996239515,4.580392996239515,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.588027898781784,4.589117562233507,4.589117562233507,4.589117562233507,4.589117562233507,4.589117562233507,4.589117562233507,4.589117562233507,4.589117562233507,4.596737973824482,4.596737973824482,4.596737973824482,4.596737973824482,4.596737973824482,4.596737973824482,4.596737973824482,4.596737973824482,4.6000000000000005,4.6000000000000005,4.6000000000000005,4.6000000000000005,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.601086828130937,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.604345773288535,4.606517122512408,4.606517122512408,4.606517122512408,4.606517122512408,4.606517122512408,4.606517122512408,4.606517122512408,4.606517122512408,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.609772228646444,4.614108798023731,4.614108798023731,4.614108798023731,4.614108798023731,4.614108798023731,4.614108798023731,4.614108798023731,4.614108798023731,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.6173585522460785,4.622769732530489,4.622769732530489,4.622769732530489,4.622769732530489,4.622769732530489,4.622769732530489,4.622769732530489,4.622769732530489,4.623851208678757,4.623851208678757,4.623851208678757,4.623851208678757,4.623851208678757,4.623851208678757,4.623851208678757,4.623851208678757,4.62709412050371,4.62709412050371,4.62709412050371,4.62709412050371,4.62709412050371,4.62709412050371,4.62709412050371,4.62709412050371,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.632493928760188,4.638965401897281,4.638965401897281,4.638965401897281,4.638965401897281,4.638965401897281,4.638965401897281,4.638965401897281,4.638965401897281,4.640043103248073,4.640043103248073,4.640043103248073,4.640043103248073,4.640043103248073,4.640043103248073,4.640043103248073,4.640043103248073,4.648655719667784,4.648655719667784,4.648655719667784,4.648655719667784,4.648655719667784,4.648655719667784,4.648655719667784,4.648655719667784,4.651881339845203,4.651881339845203,4.651881339845203,4.651881339845203,4.651881339845203,4.651881339845203,4.651881339845203,4.651881339845203,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.652956049652737,4.657252408878008,4.657252408878008,4.657252408878008,4.657252408878008,4.657252408878008,4.657252408878008,4.657252408878008,4.657252408878008,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.661544808322666,4.664761515876241,4.664761515876241,4.664761515876241,4.664761515876241,4.664761515876241,4.664761515876241,4.664761515876241,4.664761515876241,4.666904755831214,4.666904755831214,4.666904755831214,4.666904755831214,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.669047011971501,4.675467891024384,4.675467891024384,4.675467891024384,4.675467891024384,4.675467891024384,4.675467891024384,4.675467891024384,4.675467891024384,4.681879964287851,4.681879964287851,4.681879964287851,4.681879964287851,4.681879964287851,4.681879964287851,4.681879964287851,4.681879964287851,4.684015371452148,4.684015371452148,4.684015371452148,4.684015371452148,4.684015371452148,4.684015371452148,4.684015371452148,4.684015371452148,4.686149805543993,4.686149805543993,4.686149805543993,4.686149805543993,4.686149805543993,4.686149805543993,4.686149805543993,4.686149805543993,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.687216658103186,4.695742752749559,4.695742752749559,4.695742752749559,4.695742752749559,4.695742752749559,4.695742752749559,4.695742752749559,4.695742752749559,4.7,4.7,4.7,4.7,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.701063709417264,4.70425339453563,4.70425339453563,4.70425339453563,4.70425339453563,4.70425339453563,4.70425339453563,4.70425339453563,4.70425339453563,4.707440918375928,4.707440918375928,4.707440918375928,4.707440918375928,4.707440918375928,4.707440918375928,4.707440918375928,4.707440918375928,4.709564735726647,4.709564735726647,4.709564735726647,4.709564735726647,4.709564735726647,4.709564735726647,4.709564735726647,4.709564735726647,4.712748667179272,4.712748667179272,4.712748667179272,4.712748667179272,4.712748667179272,4.712748667179272,4.712748667179272,4.712748667179272,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.7169905660283025,4.720169488482379,4.720169488482379,4.720169488482379,4.720169488482379,4.720169488482379,4.720169488482379,4.720169488482379,4.720169488482379,4.72652091923859,4.72652091923859,4.72652091923859,4.72652091923859,4.72652091923859,4.72652091923859,4.72652091923859,4.72652091923859,4.729693436154188,4.729693436154188,4.729693436154188,4.729693436154188,4.729693436154188,4.729693436154188,4.729693436154188,4.729693436154188,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.738143096192854,4.741307836451879,4.741307836451879,4.741307836451879,4.741307836451879,4.741307836451879,4.741307836451879,4.741307836451879,4.741307836451879,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.742362280551751,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.743416490252569,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.750789408087882,4.751841748206689,4.751841748206689,4.751841748206689,4.751841748206689,4.751841748206689,4.751841748206689,4.751841748206689,4.751841748206689,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.753945729601885,4.763402145525822,4.763402145525822,4.763402145525822,4.763402145525822,4.763402145525822,4.763402145525822,4.763402145525822,4.763402145525822,4.767598976424087,4.767598976424087,4.767598976424087,4.767598976424087,4.767598976424087,4.767598976424087,4.767598976424087,4.767598976424087,4.770744176750625,4.770744176750625,4.770744176750625,4.770744176750625,4.770744176750625,4.770744176750625,4.770744176750625,4.770744176750625,4.775981574503821,4.775981574503821,4.775981574503821,4.775981574503821,4.775981574503821,4.775981574503821,4.775981574503821,4.775981574503821,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.780167361086848,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.78539444560216,4.788527957525152,4.788527957525152,4.788527957525152,4.788527957525152,4.788527957525152,4.788527957525152,4.788527957525152,4.788527957525152,4.792702786528704,4.792702786528704,4.792702786528704,4.792702786528704,4.792702786528704,4.792702786528704,4.792702786528704,4.792702786528704,4.800000000000001,4.800000000000001,4.800000000000001,4.800000000000001,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.801041553663122,4.8020828814171885,4.8020828814171885,4.8020828814171885,4.8020828814171885,4.8020828814171885,4.8020828814171885,4.8020828814171885,4.8020828814171885,4.804164859785725,4.804164859785725,4.804164859785725,4.804164859785725,4.804164859785725,4.804164859785725,4.804164859785725,4.804164859785725,4.805205510693586,4.805205510693586,4.805205510693586,4.805205510693586,4.805205510693586,4.805205510693586,4.805205510693586,4.805205510693586,4.8083261120685235,4.8083261120685235,4.8083261120685235,4.8083261120685235,4.8083261120685235,4.8083261120685235,4.8083261120685235,4.8083261120685235,4.8083261120685235,4.8083261120685235,4.8083261120685235,4.8083261120685235,4.80936586256442,4.80936586256442,4.80936586256442,4.80936586256442,4.80936586256442,4.80936586256442,4.80936586256442,4.80936586256442,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.810405388322278,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.8166378315169185,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.825971404805462,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.8270073544588685,4.830113870293329,4.830113870293329,4.830113870293329,4.830113870293329,4.830113870293329,4.830113870293329,4.830113870293329,4.830113870293329,4.833218389437829,4.833218389437829,4.833218389437829,4.833218389437829,4.833218389437829,4.833218389437829,4.833218389437829,4.833218389437829,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.8373546489791295,4.838388161361178,4.838388161361178,4.838388161361178,4.838388161361178,4.838388161361178,4.838388161361178,4.838388161361178,4.838388161361178,4.841487374764082,4.841487374764082,4.841487374764082,4.841487374764082,4.841487374764082,4.841487374764082,4.841487374764082,4.841487374764082,4.846648326421054,4.846648326421054,4.846648326421054,4.846648326421054,4.846648326421054,4.846648326421054,4.846648326421054,4.846648326421054,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.850773134253962,4.854894437575343,4.854894437575343,4.854894437575343,4.854894437575343,4.854894437575343,4.854894437575343,4.854894437575343,4.854894437575343,4.860041152089147,4.860041152089147,4.860041152089147,4.860041152089147,4.860041152089147,4.860041152089147,4.860041152089147,4.860041152089147,4.866210024238576,4.866210024238576,4.866210024238576,4.866210024238576,4.866210024238576,4.866210024238576,4.866210024238576,4.866210024238576,4.870318264754369,4.870318264754369,4.870318264754369,4.870318264754369,4.870318264754369,4.870318264754369,4.870318264754369,4.870318264754369,4.8754486972995625,4.8754486972995625,4.8754486972995625,4.8754486972995625,4.8754486972995625,4.8754486972995625,4.8754486972995625,4.8754486972995625,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.876474136094644,4.879549159502341,4.879549159502341,4.879549159502341,4.879549159502341,4.879549159502341,4.879549159502341,4.879549159502341,4.879549159502341,4.882622246293481,4.882622246293481,4.882622246293481,4.882622246293481,4.882622246293481,4.882622246293481,4.882622246293481,4.882622246293481,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.883646178829912,4.884669896727925,4.884669896727925,4.884669896727925,4.884669896727925,4.884669896727925,4.884669896727925,4.884669896727925,4.884669896727925,4.887739763939975,4.887739763939975,4.887739763939975,4.887739763939975,4.887739763939975,4.887739763939975,4.887739763939975,4.887739763939975,4.891829923454004,4.891829923454004,4.891829923454004,4.891829923454004,4.891829923454004,4.891829923454004,4.891829923454004,4.891829923454004,4.9,4.9,4.9,4.9,4.901020301937138,4.901020301937138,4.901020301937138,4.901020301937138,4.901020301937138,4.901020301937138,4.901020301937138,4.901020301937138,4.903060268852506,4.903060268852506,4.903060268852506,4.903060268852506,4.903060268852506,4.903060268852506,4.903060268852506,4.903060268852506,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.904079934095692,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.909175083453431,4.9162994213127424,4.9162994213127424,4.9162994213127424,4.9162994213127424,4.9162994213127424,4.9162994213127424,4.9162994213127424,4.9162994213127424,4.919349550499538,4.919349550499538,4.919349550499538,4.919349550499538,4.919349550499538,4.919349550499538,4.919349550499538,4.919349550499538,4.920365840057018,4.920365840057018,4.920365840057018,4.920365840057018,4.920365840057018,4.920365840057018,4.920365840057018,4.920365840057018,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.924428900898053,4.925444142409901,4.925444142409901,4.925444142409901,4.925444142409901,4.925444142409901,4.925444142409901,4.925444142409901,4.925444142409901,4.933558553417605,4.933558553417605,4.933558553417605,4.933558553417605,4.933558553417605,4.933558553417605,4.933558553417605,4.933558553417605,4.9365980188789935,4.9365980188789935,4.9365980188789935,4.9365980188789935,4.9365980188789935,4.9365980188789935,4.9365980188789935,4.9365980188789935,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.9396356140913875,4.94064773081425,4.94064773081425,4.94064773081425,4.94064773081425,4.94064773081425,4.94064773081425,4.94064773081425,4.94064773081425,4.947726750741193,4.947726750741193,4.947726750741193,4.947726750741193,4.947726750741193,4.947726750741193,4.947726750741193,4.947726750741193,4.949747468305834,4.949747468305834,4.949747468305834,4.949747468305834,4.949747468305834,4.949747468305834,4.949747468305834,4.949747468305834,4.949747468305834,4.949747468305834,4.949747468305834,4.949747468305834,4.951767361255979,4.951767361255979,4.951767361255979,4.951767361255979,4.951767361255979,4.951767361255979,4.951767361255979,4.951767361255979,4.957822102496216,4.957822102496216,4.957822102496216,4.957822102496216,4.957822102496216,4.957822102496216,4.957822102496216,4.957822102496216,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.964876634922565,4.965883607174055,4.965883607174055,4.965883607174055,4.965883607174055,4.965883607174055,4.965883607174055,4.965883607174055,4.965883607174055,4.967896939349689,4.967896939349689,4.967896939349689,4.967896939349689,4.967896939349689,4.967896939349689,4.967896939349689,4.967896939349689,4.972926703662543,4.972926703662543,4.972926703662543,4.972926703662543,4.972926703662543,4.972926703662543,4.972926703662543,4.972926703662543,4.973932046178355,4.973932046178355,4.973932046178355,4.973932046178355,4.973932046178355,4.973932046178355,4.973932046178355,4.973932046178355,4.976946855251722,4.976946855251722,4.976946855251722,4.976946855251722,4.976946855251722,4.976946855251722,4.976946855251722,4.976946855251722,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.98196748283246,4.992995093127972,4.992995093127972,4.992995093127972,4.992995093127972,4.992995093127972,4.992995093127972,4.992995093127972,4.992995093127972,4.99799959983992,4.99799959983992,4.99799959983992,4.99799959983992,4.99799959983992,4.99799959983992,4.99799959983992,4.99799959983992,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0}; + +const int expansion_pos_l[7845] = {0,0,0,1,-1,1,1,-1,-1,0,0,2,-2,1,1,-1,-1,2,2,-2,-2,2,2,-2,-2,0,0,3,-3,1,1,-1,-1,3,3,-3,-3,2,2,-2,-2,3,3,-3,-3,0,0,4,-4,1,1,-1,-1,4,4,-4,-4,3,3,-3,-3,2,2,-2,-2,4,4,-4,-4,0,0,3,3,-3,-3,4,4,-4,-4,5,-5,1,1,-1,-1,5,5,-5,-5,2,2,-2,-2,5,5,-5,-5,4,4,-4,-4,3,3,-3,-3,5,5,-5,-5,0,0,6,-6,1,1,-1,-1,6,6,-6,-6,2,2,-2,-2,6,6,-6,-6,4,4,-4,-4,5,5,-5,-5,3,3,-3,-3,6,6,-6,-6,0,0,7,-7,1,1,-1,-1,5,5,-5,-5,7,7,-7,-7,4,4,-4,-4,6,6,-6,-6,2,2,-2,-2,7,7,-7,-7,3,3,-3,-3,7,7,-7,-7,5,5,-5,-5,6,6,-6,-6,0,0,8,-8,1,1,-1,-1,4,4,-4,-4,7,7,-7,-7,8,8,-8,-8,2,2,-2,-2,8,8,-8,-8,6,6,-6,-6,3,3,-3,-3,8,8,-8,-8,5,5,-5,-5,7,7,-7,-7,4,4,-4,-4,8,8,-8,-8,0,0,9,-9,1,1,-1,-1,9,9,-9,-9,2,2,-2,-2,6,6,-6,-6,7,7,-7,-7,9,9,-9,-9,5,5,-5,-5,8,8,-8,-8,3,3,-3,-3,9,9,-9,-9,4,4,-4,-4,9,9,-9,-9,7,7,-7,-7,0,0,6,6,-6,-6,8,8,-8,-8,10,-10,1,1,-1,-1,10,10,-10,-10,2,2,-2,-2,10,10,-10,-10,5,5,-5,-5,9,9,-9,-9,3,3,-3,-3,10,10,-10,-10,7,7,-7,-7,8,8,-8,-8,4,4,-4,-4,10,10,-10,-10,6,6,-6,-6,9,9,-9,-9,0,0,11,-11,1,1,-1,-1,11,11,-11,-11,2,2,-2,-2,5,5,-5,-5,10,10,-10,-10,11,11,-11,-11,8,8,-8,-8,3,3,-3,-3,7,7,-7,-7,9,9,-9,-9,11,11,-11,-11,6,6,-6,-6,10,10,-10,-10,4,4,-4,-4,11,11,-11,-11,0,0,12,-12,1,1,-1,-1,8,8,-8,-8,9,9,-9,-9,12,12,-12,-12,5,5,-5,-5,11,11,-11,-11,2,2,-2,-2,12,12,-12,-12,7,7,-7,-7,10,10,-10,-10,3,3,-3,-3,12,12,-12,-12,6,6,-6,-6,11,11,-11,-11,4,4,-4,-4,12,12,-12,-12,9,9,-9,-9,8,8,-8,-8,10,10,-10,-10,0,0,5,5,-5,-5,12,12,-12,-12,13,-13,1,1,-1,-1,7,7,-7,-7,11,11,-11,-11,13,13,-13,-13,2,2,-2,-2,13,13,-13,-13,3,3,-3,-3,13,13,-13,-13,6,6,-6,-6,12,12,-12,-12,9,9,-9,-9,10,10,-10,-10,4,4,-4,-4,8,8,-8,-8,11,11,-11,-11,13,13,-13,-13,7,7,-7,-7,12,12,-12,-12,5,5,-5,-5,13,13,-13,-13,0,0,14,-14,1,1,-1,-1,14,14,-14,-14,2,2,-2,-2,10,10,-10,-10,14,14,-14,-14,9,9,-9,-9,11,11,-11,-11,3,3,-3,-3,6,6,-6,-6,13,13,-13,-13,14,14,-14,-14,8,8,-8,-8,12,12,-12,-12,4,4,-4,-4,14,14,-14,-14,7,7,-7,-7,13,13,-13,-13,5,5,-5,-5,10,10,-10,-10,11,11,-11,-11,14,14,-14,-14,0,0,9,9,-9,-9,12,12,-12,-12,15,-15,1,1,-1,-1,15,15,-15,-15,2,2,-2,-2,15,15,-15,-15,6,6,-6,-6,14,14,-14,-14,8,8,-8,-8,13,13,-13,-13,3,3,-3,-3,15,15,-15,-15,4,4,-4,-4,15,15,-15,-15,11,11,-11,-11,10,10,-10,-10,12,12,-12,-12,7,7,-7,-7,14,14,-14,-14,5,5,-5,-5,9,9,-9,-9,13,13,-13,-13,15,15,-15,-15,0,0,16,-16,1,1,-1,-1,16,16,-16,-16,2,2,-2,-2,8,8,-8,-8,14,14,-14,-14,16,16,-16,-16,6,6,-6,-6,15,15,-15,-15,3,3,-3,-3,11,11,-11,-11,12,12,-12,-12,16,16,-16,-16,10,10,-10,-10,13,13,-13,-13,4,4,-4,-4,16,16,-16,-16,7,7,-7,-7,15,15,-15,-15,9,9,-9,-9,14,14,-14,-14,5,5,-5,-5,16,16,-16,-16,12,12,-12,-12,0,0,8,8,-8,-8,15,15,-15,-15,17,-17,1,1,-1,-1,11,11,-11,-11,13,13,-13,-13,17,17,-17,-17,6,6,-6,-6,16,16,-16,-16,2,2,-2,-2,17,17,-17,-17,10,10,-10,-10,14,14,-14,-14,3,3,-3,-3,17,17,-17,-17,4,4,-4,-4,7,7,-7,-7,16,16,-16,-16,17,17,-17,-17,9,9,-9,-9,15,15,-15,-15,12,12,-12,-12,13,13,-13,-13,5,5,-5,-5,17,17,-17,-17,11,11,-11,-11,14,14,-14,-14,8,8,-8,-8,16,16,-16,-16,0,0,18,-18,1,1,-1,-1,6,6,-6,-6,10,10,-10,-10,15,15,-15,-15,17,17,-17,-17,18,18,-18,-18,2,2,-2,-2,18,18,-18,-18,3,3,-3,-3,18,18,-18,-18,9,9,-9,-9,16,16,-16,-16,7,7,-7,-7,13,13,-13,-13,17,17,-17,-17,4,4,-4,-4,12,12,-12,-12,14,14,-14,-14,18,18,-18,-18,11,11,-11,-11,15,15,-15,-15,5,5,-5,-5,18,18,-18,-18,8,8,-8,-8,17,17,-17,-17,10,10,-10,-10,16,16,-16,-16,6,6,-6,-6,18,18,-18,-18,0,0,19,-19,1,1,-1,-1,19,19,-19,-19,2,2,-2,-2,13,13,-13,-13,14,14,-14,-14,19,19,-19,-19,12,12,-12,-12,15,15,-15,-15,3,3,-3,-3,9,9,-9,-9,17,17,-17,-17,19,19,-19,-19,7,7,-7,-7,18,18,-18,-18,4,4,-4,-4,11,11,-11,-11,16,16,-16,-16,19,19,-19,-19,5,5,-5,-5,19,19,-19,-19,8,8,-8,-8,18,18,-18,-18,10,10,-10,-10,17,17,-17,-17,14,14,-14,-14,13,13,-13,-13,15,15,-15,-15,6,6,-6,-6,19,19,-19,-19,0,0,12,12,-12,-12,16,16,-16,-16,20,-20,1,1,-1,-1,20,20,-20,-20,2,2,-2,-2,20,20,-20,-20,9,9,-9,-9,18,18,-18,-18,3,3,-3,-3,20,20,-20,-20,7,7,-7,-7,11,11,-11,-11,17,17,-17,-17,19,19,-19,-19,4,4,-4,-4,20,20,-20,-20,14,14,-14,-14,15,15,-15,-15,10,10,-10,-10,18,18,-18,-18,5,5,-5,-5,8,8,-8,-8,13,13,-13,-13,16,16,-16,-16,19,19,-19,-19,20,20,-20,-20,12,12,-12,-12,17,17,-17,-17,6,6,-6,-6,20,20,-20,-20,0,0,21,-21,1,1,-1,-1,9,9,-9,-9,19,19,-19,-19,21,21,-21,-21,2,2,-2,-2,11,11,-11,-11,18,18,-18,-18,21,21,-21,-21,7,7,-7,-7,20,20,-20,-20,3,3,-3,-3,15,15,-15,-15,21,21,-21,-21,14,14,-14,-14,16,16,-16,-16,4,4,-4,-4,21,21,-21,-21,13,13,-13,-13,17,17,-17,-17,10,10,-10,-10,19,19,-19,-19,8,8,-8,-8,20,20,-20,-20,5,5,-5,-5,21,21,-21,-21,12,12,-12,-12,18,18,-18,-18,6,6,-6,-6,21,21,-21,-21,9,9,-9,-9,15,15,-15,-15,16,16,-16,-16,20,20,-20,-20,11,11,-11,-11,19,19,-19,-19,0,0,22,-22,1,1,-1,-1,14,14,-14,-14,17,17,-17,-17,22,22,-22,-22,2,2,-2,-2,22,22,-22,-22,7,7,-7,-7,21,21,-21,-21,3,3,-3,-3,13,13,-13,-13,18,18,-18,-18,22,22,-22,-22,4,4,-4,-4,10,10,-10,-10,20,20,-20,-20,22,22,-22,-22,8,8,-8,-8,12,12,-12,-12,19,19,-19,-19,21,21,-21,-21,5,5,-5,-5,22,22,-22,-22,16,16,-16,-16,15,15,-15,-15,17,17,-17,-17,6,6,-6,-6,14,14,-14,-14,18,18,-18,-18,22,22,-22,-22,11,11,-11,-11,20,20,-20,-20,9,9,-9,-9,21,21,-21,-21,0,0,23,-23,1,1,-1,-1,13,13,-13,-13,19,19,-19,-19,23,23,-23,-23,2,2,-2,-2,7,7,-7,-7,22,22,-22,-22,23,23,-23,-23,3,3,-3,-3,23,23,-23,-23,10,10,-10,-10,21,21,-21,-21,12,12,-12,-12,20,20,-20,-20,4,4,-4,-4,16,16,-16,-16,17,17,-17,-17,23,23,-23,-23,8,8,-8,-8,22,22,-22,-22,15,15,-15,-15,18,18,-18,-18,5,5,-5,-5,23,23,-23,-23,14,14,-14,-14,19,19,-19,-19,11,11,-11,-11,21,21,-21,-21,6,6,-6,-6,9,9,-9,-9,22,22,-22,-22,23,23,-23,-23,13,13,-13,-13,20,20,-20,-20,0,0,24,-24,1,1,-1,-1,24,24,-24,-24,7,7,-7,-7,17,17,-17,-17,23,23,-23,-23,2,2,-2,-2,16,16,-16,-16,18,18,-18,-18,24,24,-24,-24,10,10,-10,-10,22,22,-22,-22,3,3,-3,-3,12,12,-12,-12,21,21,-21,-21,24,24,-24,-24,15,15,-15,-15,19,19,-19,-19,4,4,-4,-4,24,24,-24,-24,8,8,-8,-8,23,23,-23,-23,14,14,-14,-14,20,20,-20,-20,5,5,-5,-5,24,24,-24,-24,11,11,-11,-11,22,22,-22,-22,9,9,-9,-9,13,13,-13,-13,21,21,-21,-21,23,23,-23,-23,6,6,-6,-6,24,24,-24,-24,17,17,-17,-17,18,18,-18,-18,16,16,-16,-16,19,19,-19,-19,0,0,7,7,-7,-7,15,15,-15,-15,20,20,-20,-20,24,24,-24,-24,25,-25,1,1,-1,-1,25,25,-25,-25,12,12,-12,-12,22,22,-22,-22,2,2,-2,-2,10,10,-10,-10,23,23,-23,-23,25,25,-25,-25,3,3,-3,-3,25,25,-25,-25,14,14,-14,-14,21,21,-21,-21,8,8,-8,-8,24,24,-24,-24,4,4,-4,-4,25,25,-25,-25,18,18,-18,-18,5,5,-5,-5,11,11,-11,-11,17,17,-17,-17,19,19,-19,-19,23,23,-23,-23,25,25,-25,-25,13,13,-13,-13,22,22,-22,-22,16,16,-16,-16,20,20,-20,-20,9,9,-9,-9,24,24,-24,-24,6,6,-6,-6,25,25,-25,-25,15,15,-15,-15,21,21,-21,-21,12,12,-12,-12,23,23,-23,-23,7,7,-7,-7,25,25,-25,-25,0,0,10,10,-10,-10,24,24,-24,-24,26,-26,1,1,-1,-1,26,26,-26,-26,2,2,-2,-2,14,14,-14,-14,22,22,-22,-22,26,26,-26,-26,3,3,-3,-3,18,18,-18,-18,19,19,-19,-19,26,26,-26,-26,8,8,-8,-8,17,17,-17,-17,20,20,-20,-20,25,25,-25,-25,4,4,-4,-4,26,26,-26,-26,11,11,-11,-11,16,16,-16,-16,21,21,-21,-21,24,24,-24,-24,13,13,-13,-13,23,23,-23,-23,5,5,-5,-5,26,26,-26,-26,9,9,-9,-9,25,25,-25,-25,15,15,-15,-15,22,22,-22,-22,6,6,-6,-6,26,26,-26,-26,12,12,-12,-12,24,24,-24,-24,19,19,-19,-19,18,18,-18,-18,20,20,-20,-20,7,7,-7,-7,10,10,-10,-10,14,14,-14,-14,23,23,-23,-23,25,25,-25,-25,26,26,-26,-26,0,0,27,-27,1,1,-1,-1,17,17,-17,-17,21,21,-21,-21,27,27,-27,-27,2,2,-2,-2,27,27,-27,-27,3,3,-3,-3,27,27,-27,-27,8,8,-8,-8,16,16,-16,-16,22,22,-22,-22,26,26,-26,-26,4,4,-4,-4,13,13,-13,-13,24,24,-24,-24,27,27,-27,-27,11,11,-11,-11,25,25,-25,-25,5,5,-5,-5,15,15,-15,-15,23,23,-23,-23,27,27,-27,-27,9,9,-9,-9,26,26,-26,-26,19,19,-19,-19,20,20,-20,-20,6,6,-6,-6,18,18,-18,-18,21,21,-21,-21,27,27,-27,-27,12,12,-12,-12,25,25,-25,-25,14,14,-14,-14,24,24,-24,-24,17,17,-17,-17,22,22,-22,-22,10,10,-10,-10,26,26,-26,-26,7,7,-7,-7,27,27,-27,-27,0,0,28,-28,1,1,-1,-1,16,16,-16,-16,23,23,-23,-23,28,28,-28,-28,2,2,-2,-2,28,28,-28,-28,3,3,-3,-3,8,8,-8,-8,27,27,-27,-27,28,28,-28,-28,13,13,-13,-13,25,25,-25,-25,11,11,-11,-11,26,26,-26,-26,4,4,-4,-4,20,20,-20,-20,28,28,-28,-28,15,15,-15,-15,24,24,-24,-24,19,19,-19,-19,21,21,-21,-21,18,18,-18,-18,22,22,-22,-22,5,5,-5,-5,28,28,-28,-28,9,9,-9,-9,27,27,-27,-27,17,17,-17,-17,23,23,-23,-23,6,6,-6,-6,12,12,-12,-12,26,26,-26,-26,28,28,-28,-28,14,14,-14,-14,25,25,-25,-25,10,10,-10,-10,27,27,-27,-27,16,16,-16,-16,24,24,-24,-24,7,7,-7,-7,28,28,-28,-28,0,0,20,20,-20,-20,21,21,-21,-21,29,-29,1,1,-1,-1,29,29,-29,-29,2,2,-2,-2,13,13,-13,-13,19,19,-19,-19,22,22,-22,-22,26,26,-26,-26,29,29,-29,-29,8,8,-8,-8,28,28,-28,-28,3,3,-3,-3,11,11,-11,-11,15,15,-15,-15,25,25,-25,-25,27,27,-27,-27,29,29,-29,-29,18,18,-18,-18,23,23,-23,-23,4,4,-4,-4,29,29,-29,-29,9,9,-9,-9,17,17,-17,-17,24,24,-24,-24,28,28,-28,-28,5,5,-5,-5,29,29,-29,-29,14,14,-14,-14,26,26,-26,-26,12,12,-12,-12,27,27,-27,-27,6,6,-6,-6,29,29,-29,-29,16,16,-16,-16,25,25,-25,-25,21,21,-21,-21,10,10,-10,-10,20,20,-20,-20,22,22,-22,-22,28,28,-28,-28,7,7,-7,-7,19,19,-19,-19,23,23,-23,-23,29,29,-29,-29,13,13,-13,-13,27,27,-27,-27,0,0,18,18,-18,-18,24,24,-24,-24,30,-30,1,1,-1,-1,15,15,-15,-15,26,26,-26,-26,30,30,-30,-30,2,2,-2,-2,30,30,-30,-30,8,8,-8,-8,11,11,-11,-11,28,28,-28,-28,29,29,-29,-29,3,3,-3,-3,30,30,-30,-30,17,17,-17,-17,25,25,-25,-25,4,4,-4,-4,30,30,-30,-30,9,9,-9,-9,29,29,-29,-29,5,5,-5,-5,14,14,-14,-14,21,21,-21,-21,22,22,-22,-22,27,27,-27,-27,30,30,-30,-30,12,12,-12,-12,28,28,-28,-28,20,20,-20,-20,23,23,-23,-23,16,16,-16,-16,26,26,-26,-26,6,6,-6,-6,30,30,-30,-30,19,19,-19,-19,24,24,-24,-24,10,10,-10,-10,29,29,-29,-29,7,7,-7,-7,18,18,-18,-18,25,25,-25,-25,30,30,-30,-30,13,13,-13,-13,28,28,-28,-28,15,15,-15,-15,27,27,-27,-27,0,0,31,-31,1,1,-1,-1,11,11,-11,-11,29,29,-29,-29,31,31,-31,-31,8,8,-8,-8,30,30,-30,-30,2,2,-2,-2,17,17,-17,-17,26,26,-26,-26,31,31,-31,-31,22,22,-22,-22,3,3,-3,-3,21,21,-21,-21,23,23,-23,-23,31,31,-31,-31,20,20,-20,-20,24,24,-24,-24,4,4,-4,-4,31,31,-31,-31,14,14,-14,-14,28,28,-28,-28,9,9,-9,-9,30,30,-30,-30,12,12,-12,-12,16,16,-16,-16,27,27,-27,-27,29,29,-29,-29,5,5,-5,-5,19,19,-19,-19,25,25,-25,-25,31,31,-31,-31,6,6,-6,-6,31,31,-31,-31,10,10,-10,-10,18,18,-18,-18,26,26,-26,-26,30,30,-30,-30,15,15,-15,-15,28,28,-28,-28,7,7,-7,-7,13,13,-13,-13,29,29,-29,-29,31,31,-31,-31,22,22,-22,-22,23,23,-23,-23,21,21,-21,-21,24,24,-24,-24,17,17,-17,-17,27,27,-27,-27,11,11,-11,-11,30,30,-30,-30,0,0,32,-32,1,1,-1,-1,8,8,-8,-8,20,20,-20,-20,25,25,-25,-25,31,31,-31,-31,32,32,-32,-32,2,2,-2,-2,32,32,-32,-32,3,3,-3,-3,32,32,-32,-32,14,14,-14,-14,19,19,-19,-19,26,26,-26,-26,29,29,-29,-29,4,4,-4,-4,16,16,-16,-16,28,28,-28,-28,32,32,-32,-32,9,9,-9,-9,31,31,-31,-31,12,12,-12,-12,30,30,-30,-30,5,5,-5,-5,32,32,-32,-32,18,18,-18,-18,27,27,-27,-27,23,23,-23,-23,6,6,-6,-6,22,22,-22,-22,24,24,-24,-24,32,32,-32,-32,10,10,-10,-10,31,31,-31,-31,15,15,-15,-15,21,21,-21,-21,25,25,-25,-25,29,29,-29,-29,13,13,-13,-13,30,30,-30,-30,7,7,-7,-7,17,17,-17,-17,28,28,-28,-28,32,32,-32,-32,20,20,-20,-20,26,26,-26,-26,11,11,-11,-11,31,31,-31,-31,8,8,-8,-8,32,32,-32,-32,0,0,33,-33,1,1,-1,-1,19,19,-19,-19,27,27,-27,-27,33,33,-33,-33,2,2,-2,-2,33,33,-33,-33,14,14,-14,-14,30,30,-30,-30,16,16,-16,-16,29,29,-29,-29,3,3,-3,-3,33,33,-33,-33,4,4,-4,-4,9,9,-9,-9,12,12,-12,-12,23,23,-23,-23,24,24,-24,-24,31,31,-31,-31,32,32,-32,-32,33,33,-33,-33,18,18,-18,-18,28,28,-28,-28,22,22,-22,-22,25,25,-25,-25,5,5,-5,-5,33,33,-33,-33,21,21,-21,-21,26,26,-26,-26,10,10,-10,-10,32,32,-32,-32,6,6,-6,-6,15,15,-15,-15,30,30,-30,-30,33,33,-33,-33,20,20,-20,-20,27,27,-27,-27,13,13,-13,-13,17,17,-17,-17,29,29,-29,-29,31,31,-31,-31,7,7,-7,-7,33,33,-33,-33,11,11,-11,-11,19,19,-19,-19,28,28,-28,-28,32,32,-32,-32,24,24,-24,-24,8,8,-8,-8,33,33,-33,-33,23,23,-23,-23,25,25,-25,-25,0,0,16,16,-16,-16,30,30,-30,-30,34,-34,1,1,-1,-1,14,14,-14,-14,31,31,-31,-31,34,34,-34,-34,2,2,-2,-2,22,22,-22,-22,26,26,-26,-26,34,34,-34,-34,3,3,-3,-3,18,18,-18,-18,29,29,-29,-29,34,34,-34,-34,12,12,-12,-12,32,32,-32,-32,9,9,-9,-9,21,21,-21,-21,27,27,-27,-27,33,33,-33,-33,4,4,-4,-4,34,34,-34,-34,5,5,-5,-5,34,34,-34,-34,20,20,-20,-20,28,28,-28,-28,15,15,-15,-15,31,31,-31,-31,10,10,-10,-10,17,17,-17,-17,30,30,-30,-30,33,33,-33,-33,6,6,-6,-6,34,34,-34,-34,13,13,-13,-13,32,32,-32,-32,24,24,-24,-24,25,25,-25,-25,19,19,-19,-19,29,29,-29,-29,7,7,-7,-7,23,23,-23,-23,26,26,-26,-26,34,34,-34,-34,11,11,-11,-11,33,33,-33,-33,22,22,-22,-22,27,27,-27,-27,16,16,-16,-16,31,31,-31,-31,8,8,-8,-8,14,14,-14,-14,32,32,-32,-32,34,34,-34,-34,18,18,-18,-18,30,30,-30,-30,0,0,21,21,-21,-21,28,28,-28,-28,35,-35,1,1,-1,-1,35,35,-35,-35,2,2,-2,-2,35,35,-35,-35,12,12,-12,-12,33,33,-33,-33,3,3,-3,-3,35,35,-35,-35,9,9,-9,-9,34,34,-34,-34,4,4,-4,-4,20,20,-20,-20,29,29,-29,-29,35,35,-35,-35,15,15,-15,-15,32,32,-32,-32,5,5,-5,-5,17,17,-17,-17,25,25,-25,-25,31,31,-31,-31,35,35,-35,-35,24,24,-24,-24,26,26,-26,-26,10,10,-10,-10,34,34,-34,-34,13,13,-13,-13,23,23,-23,-23,27,27,-27,-27,33,33,-33,-33,6,6,-6,-6,19,19,-19,-19,30,30,-30,-30,35,35,-35,-35,22,22,-22,-22,28,28,-28,-28,7,7,-7,-7,35,35,-35,-35,11,11,-11,-11,34,34,-34,-34,16,16,-16,-16,32,32,-32,-32,21,21,-21,-21,29,29,-29,-29,14,14,-14,-14,18,18,-18,-18,31,31,-31,-31,33,33,-33,-33,8,8,-8,-8,35,35,-35,-35,0,0,36,-36,1,1,-1,-1,36,36,-36,-36,2,2,-2,-2,12,12,-12,-12,20,20,-20,-20,30,30,-30,-30,34,34,-34,-34,36,36,-36,-36,25,25,-25,-25,26,26,-26,-26,3,3,-3,-3,24,24,-24,-24,27,27,-27,-27,36,36,-36,-36,9,9,-9,-9,35,35,-35,-35,4,4,-4,-4,36,36,-36,-36,17,17,-17,-17,23,23,-23,-23,28,28,-28,-28,32,32,-32,-32,15,15,-15,-15,33,33,-33,-33,5,5,-5,-5,36,36,-36,-36,19,19,-19,-19,31,31,-31,-31,10,10,-10,-10,13,13,-13,-13,22,22,-22,-22,29,29,-29,-29,34,34,-34,-34,35,35,-35,-35,6,6,-6,-6,36,36,-36,-36,21,21,-21,-21,30,30,-30,-30,7,7,-7,-7,16,16,-16,-16,33,33,-33,-33,36,36,-36,-36,11,11,-11,-11,35,35,-35,-35,18,18,-18,-18,32,32,-32,-32,14,14,-14,-14,26,26,-26,-26,34,34,-34,-34,25,25,-25,-25,27,27,-27,-27,8,8,-8,-8,24,24,-24,-24,28,28,-28,-28,36,36,-36,-36,20,20,-20,-20,31,31,-31,-31,0,0,12,12,-12,-12,35,35,-35,-35,37,-37,1,1,-1,-1,23,23,-23,-23,29,29,-29,-29,37,37,-37,-37,2,2,-2,-2,37,37,-37,-37,9,9,-9,-9,36,36,-36,-36,3,3,-3,-3,17,17,-17,-17,33,33,-33,-33,37,37,-37,-37,15,15,-15,-15,34,34,-34,-34,22,22,-22,-22,30,30,-30,-30,4,4,-4,-4,19,19,-19,-19,32,32,-32,-32,37,37,-37,-37,5,5,-5,-5,13,13,-13,-13,35,35,-35,-35,37,37,-37,-37,10,10,-10,-10,36,36,-36,-36,21,21,-21,-21,31,31,-31,-31,6,6,-6,-6,26,26,-26,-26,27,27,-27,-27,37,37,-37,-37,25,25,-25,-25,28,28,-28,-28,16,16,-16,-16,34,34,-34,-34,18,18,-18,-18,33,33,-33,-33,11,11,-11,-11,24,24,-24,-24,29,29,-29,-29,36,36,-36,-36,7,7,-7,-7,37,37,-37,-37,14,14,-14,-14,35,35,-35,-35,20,20,-20,-20,32,32,-32,-32,23,23,-23,-23,30,30,-30,-30,8,8,-8,-8,37,37,-37,-37,12,12,-12,-12,36,36,-36,-36,0,0,38,-38,1,1,-1,-1,17,17,-17,-17,22,22,-22,-22,31,31,-31,-31,34,34,-34,-34,38,38,-38,-38,2,2,-2,-2,38,38,-38,-38,9,9,-9,-9,15,15,-15,-15,19,19,-19,-19,33,33,-33,-33,35,35,-35,-35,37,37,-37,-37,3,3,-3,-3,38,38,-38,-38,27,27,-27,-27,4,4,-4,-4,26,26,-26,-26,28,28,-28,-28,38,38,-38,-38,13,13,-13,-13,21,21,-21,-21,32,32,-32,-32,36,36,-36,-36,25,25,-25,-25,29,29,-29,-29,5,5,-5,-5,10,10,-10,-10,37,37,-37,-37,38,38,-38,-38,24,24,-24,-24,30,30,-30,-30,6,6,-6,-6,18,18,-18,-18,34,34,-34,-34,38,38,-38,-38,16,16,-16,-16,35,35,-35,-35,20,20,-20,-20,33,33,-33,-33,11,11,-11,-11,23,23,-23,-23,31,31,-31,-31,37,37,-37,-37,14,14,-14,-14,36,36,-36,-36,7,7,-7,-7,38,38,-38,-38,8,8,-8,-8,22,22,-22,-22,32,32,-32,-32,38,38,-38,-38,12,12,-12,-12,27,27,-27,-27,28,28,-28,-28,37,37,-37,-37,17,17,-17,-17,35,35,-35,-35,19,19,-19,-19,26,26,-26,-26,29,29,-29,-29,34,34,-34,-34,0,0,15,15,-15,-15,36,36,-36,-36,39,-39,1,1,-1,-1,39,39,-39,-39,2,2,-2,-2,9,9,-9,-9,25,25,-25,-25,30,30,-30,-30,38,38,-38,-38,39,39,-39,-39,3,3,-3,-3,21,21,-21,-21,33,33,-33,-33,39,39,-39,-39,4,4,-4,-4,24,24,-24,-24,31,31,-31,-31,39,39,-39,-39,13,13,-13,-13,37,37,-37,-37,10,10,-10,-10,38,38,-38,-38,5,5,-5,-5,39,39,-39,-39,18,18,-18,-18,35,35,-35,-35,16,16,-16,-16,36,36,-36,-36,23,23,-23,-23,32,32,-32,-32,20,20,-20,-20,34,34,-34,-34,6,6,-6,-6,39,39,-39,-39,11,11,-11,-11,14,14,-14,-14,37,37,-37,-37,38,38,-38,-38,28,28,-28,-28,7,7,-7,-7,27,27,-27,-27,29,29,-29,-29,39,39,-39,-39,22,22,-22,-22,33,33,-33,-33,26,26,-26,-26,30,30,-30,-30,8,8,-8,-8,17,17,-17,-17,36,36,-36,-36,39,39,-39,-39,19,19,-19,-19,25,25,-25,-25,31,31,-31,-31,35,35,-35,-35,12,12,-12,-12,38,38,-38,-38,15,15,-15,-15,37,37,-37,-37,21,21,-21,-21,34,34,-34,-34,0,0,24,24,-24,-24,32,32,-32,-32,40,-40,1,1,-1,-1,40,40,-40,-40,9,9,-9,-9,39,39,-39,-39,2,2,-2,-2,40,40,-40,-40,3,3,-3,-3,40,40,-40,-40,13,13,-13,-13,38,38,-38,-38,4,4,-4,-4,40,40,-40,-40,23,23,-23,-23,33,33,-33,-33,18,18,-18,-18,36,36,-36,-36,10,10,-10,-10,39,39,-39,-39,5,5,-5,-5,16,16,-16,-16,20,20,-20,-20,28,28,-28,-28,29,29,-29,-29,35,35,-35,-35,37,37,-37,-37,40,40,-40,-40,27,27,-27,-27,30,30,-30,-30,6,6,-6,-6,40,40,-40,-40,26,26,-26,-26,31,31,-31,-31,14,14,-14,-14,22,22,-22,-22,34,34,-34,-34,38,38,-38,-38,11,11,-11,-11,39,39,-39,-39,7,7,-7,-7,25,25,-25,-25,32,32,-32,-32,40,40,-40,-40,19,19,-19,-19,36,36,-36,-36,17,17,-17,-17,37,37,-37,-37,8,8,-8,-8,40,40,-40,-40,12,12,-12,-12,24,24,-24,-24,33,33,-33,-33,39,39,-39,-39,21,21,-21,-21,35,35,-35,-35,15,15,-15,-15,38,38,-38,-38,0,0,9,9,-9,-9,40,40,-40,-40,41,-41,1,1,-1,-1,29,29,-29,-29,41,41,-41,-41,28,28,-28,-28,30,30,-30,-30,2,2,-2,-2,23,23,-23,-23,34,34,-34,-34,41,41,-41,-41,3,3,-3,-3,13,13,-13,-13,27,27,-27,-27,31,31,-31,-31,39,39,-39,-39,41,41,-41,-41,18,18,-18,-18,37,37,-37,-37,20,20,-20,-20,36,36,-36,-36,4,4,-4,-4,41,41,-41,-41,10,10,-10,-10,16,16,-16,-16,26,26,-26,-26,32,32,-32,-32,38,38,-38,-38,40,40,-40,-40,5,5,-5,-5,41,41,-41,-41,22,22,-22,-22,35,35,-35,-35,25,25,-25,-25,33,33,-33,-33,6,6,-6,-6,14,14,-14,-14,39,39,-39,-39,41,41,-41,-41,11,11,-11,-11,40,40,-40,-40,7,7,-7,-7,19,19,-19,-19,37,37,-37,-37,41,41,-41,-41,24,24,-24,-24,34,34,-34,-34,17,17,-17,-17,38,38,-38,-38,21,21,-21,-21,36,36,-36,-36,29,29,-29,-29,30,30,-30,-30,12,12,-12,-12,40,40,-40,-40,8,8,-8,-8,28,28,-28,-28,31,31,-31,-31,41,41,-41,-41,15,15,-15,-15,39,39,-39,-39,27,27,-27,-27,32,32,-32,-32,23,23,-23,-23,35,35,-35,-35,9,9,-9,-9,41,41,-41,-41,0,0,42,-42,1,1,-1,-1,26,26,-26,-26,33,33,-33,-33,42,42,-42,-42,2,2,-2,-2,18,18,-18,-18,38,38,-38,-38,42,42,-42,-42,13,13,-13,-13,20,20,-20,-20,37,37,-37,-37,40,40,-40,-40,3,3,-3,-3,42,42,-42,-42,16,16,-16,-16,39,39,-39,-39,4,4,-4,-4,22,22,-22,-22,36,36,-36,-36,42,42,-42,-42,10,10,-10,-10,25,25,-25,-25,34,34,-34,-34,41,41,-41,-41,5,5,-5,-5,42,42,-42,-42,14,14,-14,-14,40,40,-40,-40,6,6,-6,-6,30,30,-30,-30,42,42,-42,-42,24,24,-24,-24,35,35,-35,-35,11,11,-11,-11,29,29,-29,-29,31,31,-31,-31,41,41,-41,-41,19,19,-19,-19,38,38,-38,-38,28,28,-28,-28,32,32,-32,-32,17,17,-17,-17,21,21,-21,-21,37,37,-37,-37,39,39,-39,-39,7,7,-7,-7,42,42,-42,-42,27,27,-27,-27,33,33,-33,-33,12,12,-12,-12,15,15,-15,-15,23,23,-23,-23,36,36,-36,-36,40,40,-40,-40,41,41,-41,-41,8,8,-8,-8,42,42,-42,-42,26,26,-26,-26,34,34,-34,-34,20,20,-20,-20,38,38,-38,-38,9,9,-9,-9,18,18,-18,-18,39,39,-39,-39,42,42,-42,-42,0,0,43,-43,1,1,-1,-1,13,13,-13,-13,25,25,-25,-25,35,35,-35,-35,41,41,-41,-41,43,43,-43,-43,2,2,-2,-2,22,22,-22,-22,37,37,-37,-37,43,43,-43,-43,16,16,-16,-16,40,40,-40,-40,3,3,-3,-3,43,43,-43,-43,30,30,-30,-30,31,31,-31,-31,10,10,-10,-10,42,42,-42,-42,4,4,-4,-4,29,29,-29,-29,32,32,-32,-32,43,43,-43,-43,24,24,-24,-24,36,36,-36,-36,28,28,-28,-28,33,33,-33,-33,5,5,-5,-5,43,43,-43,-43,14,14,-14,-14,41,41,-41,-41,19,19,-19,-19,39,39,-39,-39,6,6,-6,-6,11,11,-11,-11,21,21,-21,-21,27,27,-27,-27,34,34,-34,-34,38,38,-38,-38,42,42,-42,-42,43,43,-43,-43,17,17,-17,-17,40,40,-40,-40,7,7,-7,-7,23,23,-23,-23,37,37,-37,-37,43,43,-43,-43,26,26,-26,-26,35,35,-35,-35,15,15,-15,-15,41,41,-41,-41,12,12,-12,-12,42,42,-42,-42,8,8,-8,-8,43,43,-43,-43,20,20,-20,-20,25,25,-25,-25,36,36,-36,-36,39,39,-39,-39,31,31,-31,-31,18,18,-18,-18,30,30,-30,-30,32,32,-32,-32,40,40,-40,-40,22,22,-22,-22,38,38,-38,-38,9,9,-9,-9,29,29,-29,-29,33,33,-33,-33,43,43,-43,-43,13,13,-13,-13,42,42,-42,-42,0,0,44,-44,1,1,-1,-1,16,16,-16,-16,41,41,-41,-41,44,44,-44,-44,2,2,-2,-2,28,28,-28,-28,34,34,-34,-34,44,44,-44,-44,3,3,-3,-3,24,24,-24,-24,37,37,-37,-37,44,44,-44,-44,10,10,-10,-10,43,43,-43,-43,4,4,-4,-4,44,44,-44,-44,27,27,-27,-27,35,35,-35,-35,14,14,-14,-14,42,42,-42,-42,5,5,-5,-5,19,19,-19,-19,40,40,-40,-40,44,44,-44,-44,21,21,-21,-21,39,39,-39,-39,11,11,-11,-11,17,17,-17,-17,41,41,-41,-41,43,43,-43,-43,6,6,-6,-6,26,26,-26,-26,36,36,-36,-36,44,44,-44,-44,23,23,-23,-23,38,38,-38,-38,7,7,-7,-7,31,31,-31,-31,32,32,-32,-32,44,44,-44,-44,15,15,-15,-15,30,30,-30,-30,33,33,-33,-33,42,42,-42,-42,12,12,-12,-12,43,43,-43,-43,25,25,-25,-25,37,37,-37,-37,29,29,-29,-29,34,34,-34,-34,8,8,-8,-8,20,20,-20,-20,40,40,-40,-40,44,44,-44,-44,18,18,-18,-18,22,22,-22,-22,39,39,-39,-39,41,41,-41,-41,28,28,-28,-28,35,35,-35,-35,9,9,-9,-9,44,44,-44,-44,13,13,-13,-13,43,43,-43,-43,16,16,-16,-16,24,24,-24,-24,38,38,-38,-38,42,42,-42,-42,0,0,27,27,-27,-27,36,36,-36,-36,45,-45,1,1,-1,-1,45,45,-45,-45,2,2,-2,-2,45,45,-45,-45,3,3,-3,-3,45,45,-45,-45,10,10,-10,-10,44,44,-44,-44,4,4,-4,-4,21,21,-21,-21,40,40,-40,-40,45,45,-45,-45,19,19,-19,-19,41,41,-41,-41,14,14,-14,-14,26,26,-26,-26,37,37,-37,-37,43,43,-43,-43,32,32,-32,-32,5,5,-5,-5,23,23,-23,-23,31,31,-31,-31,33,33,-33,-33,39,39,-39,-39,45,45,-45,-45,17,17,-17,-17,42,42,-42,-42,30,30,-30,-30,34,34,-34,-34,11,11,-11,-11,44,44,-44,-44,6,6,-6,-6,45,45,-45,-45,29,29,-29,-29,35,35,-35,-35,25,25,-25,-25,38,38,-38,-38,7,7,-7,-7,15,15,-15,-15,43,43,-43,-43,45,45,-45,-45,12,12,-12,-12,28,28,-28,-28,36,36,-36,-36,44,44,-44,-44,20,20,-20,-20,41,41,-41,-41,22,22,-22,-22,40,40,-40,-40,18,18,-18,-18,42,42,-42,-42,8,8,-8,-8,45,45,-45,-45,24,24,-24,-24,39,39,-39,-39,27,27,-27,-27,37,37,-37,-37,13,13,-13,-13,16,16,-16,-16,43,43,-43,-43,44,44,-44,-44,9,9,-9,-9,45,45,-45,-45,32,32,-32,-32,33,33,-33,-33,0,0,46,-46,1,1,-1,-1,31,31,-31,-31,34,34,-34,-34,46,46,-46,-46,2,2,-2,-2,26,26,-26,-26,38,38,-38,-38,46,46,-46,-46,21,21,-21,-21,41,41,-41,-41,3,3,-3,-3,10,10,-10,-10,19,19,-19,-19,30,30,-30,-30,35,35,-35,-35,42,42,-42,-42,45,45,-45,-45,46,46,-46,-46,23,23,-23,-23,40,40,-40,-40,4,4,-4,-4,14,14,-14,-14,44,44,-44,-44,46,46,-46,-46,29,29,-29,-29,36,36,-36,-36,17,17,-17,-17,43,43,-43,-43,5,5,-5,-5,46,46,-46,-46,11,11,-11,-11,25,25,-25,-25,39,39,-39,-39,45,45,-45,-45,6,6,-6,-6,46,46,-46,-46,28,28,-28,-28,37,37,-37,-37,15,15,-15,-15,44,44,-44,-44,20,20,-20,-20,42,42,-42,-42,7,7,-7,-7,22,22,-22,-22,41,41,-41,-41,46,46,-46,-46,12,12,-12,-12,45,45,-45,-45,18,18,-18,-18,27,27,-27,-27,38,38,-38,-38,43,43,-43,-43,24,24,-24,-24,40,40,-40,-40,33,33,-33,-33,8,8,-8,-8,32,32,-32,-32,34,34,-34,-34,46,46,-46,-46,31,31,-31,-31,35,35,-35,-35,16,16,-16,-16,44,44,-44,-44,13,13,-13,-13,45,45,-45,-45,30,30,-30,-30,36,36,-36,-36,9,9,-9,-9,26,26,-26,-26,39,39,-39,-39,46,46,-46,-46,21,21,-21,-21,42,42,-42,-42,0,0,47,-47,1,1,-1,-1,19,19,-19,-19,23,23,-23,-23,29,29,-29,-29,37,37,-37,-37,41,41,-41,-41,43,43,-43,-43,47,47,-47,-47,2,2,-2,-2,47,47,-47,-47,10,10,-10,-10,46,46,-46,-46,3,3,-3,-3,47,47,-47,-47,14,14,-14,-14,45,45,-45,-45,4,4,-4,-4,17,17,-17,-17,25,25,-25,-25,40,40,-40,-40,44,44,-44,-44,47,47,-47,-47,28,28,-28,-28,38,38,-38,-38,5,5,-5,-5,47,47,-47,-47,11,11,-11,-11,46,46,-46,-46,6,6,-6,-6,33,33,-33,-33,34,34,-34,-34,47,47,-47,-47,22,22,-22,-22,42,42,-42,-42,20,20,-20,-20,32,32,-32,-32,35,35,-35,-35,43,43,-43,-43,15,15,-15,-15,27,27,-27,-27,39,39,-39,-39,45,45,-45,-45,24,24,-24,-24,31,31,-31,-31,36,36,-36,-36,41,41,-41,-41,7,7,-7,-7,47,47,-47,-47,12,12,-12,-12,18,18,-18,-18,44,44,-44,-44,46,46,-46,-46,30,30,-30,-30,37,37,-37,-37,8,8,-8,-8,47,47,-47,-47,26,26,-26,-26,40,40,-40,-40,16,16,-16,-16,45,45,-45,-45,13,13,-13,-13,29,29,-29,-29,38,38,-38,-38,46,46,-46,-46,9,9,-9,-9,21,21,-21,-21,43,43,-43,-43,47,47,-47,-47,23,23,-23,-23,42,42,-42,-42,19,19,-19,-19,44,44,-44,-44,0,0,48,-48,1,1,-1,-1,28,28,-28,-28,39,39,-39,-39,48,48,-48,-48,25,25,-25,-25,41,41,-41,-41,2,2,-2,-2,48,48,-48,-48,10,10,-10,-10,47,47,-47,-47,14,14,-14,-14,34,34,-34,-34,46,46,-46,-46,3,3,-3,-3,48,48,-48,-48,17,17,-17,-17,33,33,-33,-33,35,35,-35,-35,45,45,-45,-45,4,4,-4,-4,32,32,-32,-32,36,36,-36,-36,48,48,-48,-48,5,5,-5,-5,27,27,-27,-27,40,40,-40,-40,48,48,-48,-48,11,11,-11,-11,31,31,-31,-31,37,37,-37,-37,47,47,-47,-47,22,22,-22,-22,43,43,-43,-43,20,20,-20,-20,44,44,-44,-44,6,6,-6,-6,24,24,-24,-24,42,42,-42,-42,48,48,-48,-48,15,15,-15,-15,46,46,-46,-46,30,30,-30,-30,38,38,-38,-38,18,18,-18,-18,45,45,-45,-45,7,7,-7,-7,12,12,-12,-12,47,47,-47,-47,48,48,-48,-48,26,26,-26,-26,41,41,-41,-41,29,29,-29,-29,39,39,-39,-39,8,8,-8,-8,48,48,-48,-48,16,16,-16,-16,46,46,-46,-46,21,21,-21,-21,44,44,-44,-44,13,13,-13,-13,23,23,-23,-23,43,43,-43,-43,47,47,-47,-47,34,34,-34,-34,35,35,-35,-35,28,28,-28,-28,40,40,-40,-40,9,9,-9,-9,33,33,-33,-33,36,36,-36,-36,48,48,-48,-48,19,19,-19,-19,45,45,-45,-45,25,25,-25,-25,42,42,-42,-42,32,32,-32,-32,37,37,-37,-37,0,0,49,-49,1,1,-1,-1,49,49,-49,-49,10,10,-10,-10,48,48,-48,-48,2,2,-2,-2,14,14,-14,-14,17,17,-17,-17,31,31,-31,-31,38,38,-38,-38,46,46,-46,-46,47,47,-47,-47,49,49,-49,-49,3,3,-3,-3,27,27,-27,-27,41,41,-41,-41,49,49,-49,-49,4,4,-4,-4,49,49,-49,-49,22,22,-22,-22,44,44,-44,-44,30,30,-30,-30,39,39,-39,-39,11,11,-11,-11,20,20,-20,-20,24,24,-24,-24,43,43,-43,-43,45,45,-45,-45,48,48,-48,-48,5,5,-5,-5,49,49,-49,-49,15,15,-15,-15,47,47,-47,-47,6,6,-6,-6,49,49,-49,-49,18,18,-18,-18,26,26,-26,-26,42,42,-42,-42,46,46,-46,-46,29,29,-29,-29,40,40,-40,-40,12,12,-12,-12,48,48,-48,-48,7,7,-7,-7,35,35,-35,-35,49,49,-49,-49,34,34,-34,-34,36,36,-36,-36,33,33,-33,-33,37,37,-37,-37,8,8,-8,-8,16,16,-16,-16,23,23,-23,-23,28,28,-28,-28,41,41,-41,-41,44,44,-44,-44,47,47,-47,-47,49,49,-49,-49,21,21,-21,-21,45,45,-45,-45,32,32,-32,-32,38,38,-38,-38,13,13,-13,-13,48,48,-48,-48,25,25,-25,-25,43,43,-43,-43,19,19,-19,-19,46,46,-46,-46,9,9,-9,-9,31,31,-31,-31,39,39,-39,-39,49,49,-49,-49,27,27,-27,-27,42,42,-42,-42,17,17,-17,-17,47,47,-47,-47,0,0,14,14,-14,-14,30,30,-30,-30,40,40,-40,-40,48,48,-48,-48,50,-50}; + +const int expansion_pos_c[7845] = {0,1,-1,0,0,1,-1,1,-1,2,-2,0,0,2,-2,2,-2,1,-1,1,-1,2,-2,2,-2,3,-3,0,0,3,-3,3,-3,1,-1,1,-1,3,-3,3,-3,2,-2,2,-2,4,-4,0,0,4,-4,4,-4,1,-1,1,-1,3,-3,3,-3,4,-4,4,-4,2,-2,2,-2,5,-5,4,-4,4,-4,3,-3,3,-3,0,0,5,-5,5,-5,1,-1,1,-1,5,-5,5,-5,2,-2,2,-2,4,-4,4,-4,5,-5,5,-5,3,-3,3,-3,6,-6,0,0,6,-6,6,-6,1,-1,1,-1,6,-6,6,-6,2,-2,2,-2,5,-5,5,-5,4,-4,4,-4,6,-6,6,-6,3,-3,3,-3,7,-7,0,0,7,-7,7,-7,5,-5,5,-5,1,-1,1,-1,6,-6,6,-6,4,-4,4,-4,7,-7,7,-7,2,-2,2,-2,7,-7,7,-7,3,-3,3,-3,6,-6,6,-6,5,-5,5,-5,8,-8,0,0,8,-8,8,-8,7,-7,7,-7,4,-4,4,-4,1,-1,1,-1,8,-8,8,-8,2,-2,2,-2,6,-6,6,-6,8,-8,8,-8,3,-3,3,-3,7,-7,7,-7,5,-5,5,-5,8,-8,8,-8,4,-4,4,-4,9,-9,0,0,9,-9,9,-9,1,-1,1,-1,9,-9,9,-9,7,-7,7,-7,6,-6,6,-6,2,-2,2,-2,8,-8,8,-8,5,-5,5,-5,9,-9,9,-9,3,-3,3,-3,9,-9,9,-9,4,-4,4,-4,7,-7,7,-7,10,-10,8,-8,8,-8,6,-6,6,-6,0,0,10,-10,10,-10,1,-1,1,-1,10,-10,10,-10,2,-2,2,-2,9,-9,9,-9,5,-5,5,-5,10,-10,10,-10,3,-3,3,-3,8,-8,8,-8,7,-7,7,-7,10,-10,10,-10,4,-4,4,-4,9,-9,9,-9,6,-6,6,-6,11,-11,0,0,11,-11,11,-11,1,-1,1,-1,11,-11,11,-11,10,-10,10,-10,5,-5,5,-5,2,-2,2,-2,8,-8,8,-8,11,-11,11,-11,9,-9,9,-9,7,-7,7,-7,3,-3,3,-3,10,-10,10,-10,6,-6,6,-6,11,-11,11,-11,4,-4,4,-4,12,-12,0,0,12,-12,12,-12,9,-9,9,-9,8,-8,8,-8,1,-1,1,-1,11,-11,11,-11,5,-5,5,-5,12,-12,12,-12,2,-2,2,-2,10,-10,10,-10,7,-7,7,-7,12,-12,12,-12,3,-3,3,-3,11,-11,11,-11,6,-6,6,-6,12,-12,12,-12,4,-4,4,-4,9,-9,9,-9,10,-10,10,-10,8,-8,8,-8,13,-13,12,-12,12,-12,5,-5,5,-5,0,0,13,-13,13,-13,11,-11,11,-11,7,-7,7,-7,1,-1,1,-1,13,-13,13,-13,2,-2,2,-2,13,-13,13,-13,3,-3,3,-3,12,-12,12,-12,6,-6,6,-6,10,-10,10,-10,9,-9,9,-9,13,-13,13,-13,11,-11,11,-11,8,-8,8,-8,4,-4,4,-4,12,-12,12,-12,7,-7,7,-7,13,-13,13,-13,5,-5,5,-5,14,-14,0,0,14,-14,14,-14,1,-1,1,-1,14,-14,14,-14,10,-10,10,-10,2,-2,2,-2,11,-11,11,-11,9,-9,9,-9,14,-14,14,-14,13,-13,13,-13,6,-6,6,-6,3,-3,3,-3,12,-12,12,-12,8,-8,8,-8,14,-14,14,-14,4,-4,4,-4,13,-13,13,-13,7,-7,7,-7,14,-14,14,-14,11,-11,11,-11,10,-10,10,-10,5,-5,5,-5,15,-15,12,-12,12,-12,9,-9,9,-9,0,0,15,-15,15,-15,1,-1,1,-1,15,-15,15,-15,2,-2,2,-2,14,-14,14,-14,6,-6,6,-6,13,-13,13,-13,8,-8,8,-8,15,-15,15,-15,3,-3,3,-3,15,-15,15,-15,4,-4,4,-4,11,-11,11,-11,12,-12,12,-12,10,-10,10,-10,14,-14,14,-14,7,-7,7,-7,15,-15,15,-15,13,-13,13,-13,9,-9,9,-9,5,-5,5,-5,16,-16,0,0,16,-16,16,-16,1,-1,1,-1,16,-16,16,-16,14,-14,14,-14,8,-8,8,-8,2,-2,2,-2,15,-15,15,-15,6,-6,6,-6,16,-16,16,-16,12,-12,12,-12,11,-11,11,-11,3,-3,3,-3,13,-13,13,-13,10,-10,10,-10,16,-16,16,-16,4,-4,4,-4,15,-15,15,-15,7,-7,7,-7,14,-14,14,-14,9,-9,9,-9,16,-16,16,-16,5,-5,5,-5,12,-12,12,-12,17,-17,15,-15,15,-15,8,-8,8,-8,0,0,17,-17,17,-17,13,-13,13,-13,11,-11,11,-11,1,-1,1,-1,16,-16,16,-16,6,-6,6,-6,17,-17,17,-17,2,-2,2,-2,14,-14,14,-14,10,-10,10,-10,17,-17,17,-17,3,-3,3,-3,17,-17,17,-17,16,-16,16,-16,7,-7,7,-7,4,-4,4,-4,15,-15,15,-15,9,-9,9,-9,13,-13,13,-13,12,-12,12,-12,17,-17,17,-17,5,-5,5,-5,14,-14,14,-14,11,-11,11,-11,16,-16,16,-16,8,-8,8,-8,18,-18,0,0,18,-18,18,-18,17,-17,17,-17,15,-15,15,-15,10,-10,10,-10,6,-6,6,-6,1,-1,1,-1,18,-18,18,-18,2,-2,2,-2,18,-18,18,-18,3,-3,3,-3,16,-16,16,-16,9,-9,9,-9,17,-17,17,-17,13,-13,13,-13,7,-7,7,-7,18,-18,18,-18,14,-14,14,-14,12,-12,12,-12,4,-4,4,-4,15,-15,15,-15,11,-11,11,-11,18,-18,18,-18,5,-5,5,-5,17,-17,17,-17,8,-8,8,-8,16,-16,16,-16,10,-10,10,-10,18,-18,18,-18,6,-6,6,-6,19,-19,0,0,19,-19,19,-19,1,-1,1,-1,19,-19,19,-19,14,-14,14,-14,13,-13,13,-13,2,-2,2,-2,15,-15,15,-15,12,-12,12,-12,19,-19,19,-19,17,-17,17,-17,9,-9,9,-9,3,-3,3,-3,18,-18,18,-18,7,-7,7,-7,19,-19,19,-19,16,-16,16,-16,11,-11,11,-11,4,-4,4,-4,19,-19,19,-19,5,-5,5,-5,18,-18,18,-18,8,-8,8,-8,17,-17,17,-17,10,-10,10,-10,14,-14,14,-14,15,-15,15,-15,13,-13,13,-13,19,-19,19,-19,6,-6,6,-6,20,-20,16,-16,16,-16,12,-12,12,-12,0,0,20,-20,20,-20,1,-1,1,-1,20,-20,20,-20,2,-2,2,-2,18,-18,18,-18,9,-9,9,-9,20,-20,20,-20,3,-3,3,-3,19,-19,19,-19,17,-17,17,-17,11,-11,11,-11,7,-7,7,-7,20,-20,20,-20,4,-4,4,-4,15,-15,15,-15,14,-14,14,-14,18,-18,18,-18,10,-10,10,-10,20,-20,20,-20,19,-19,19,-19,16,-16,16,-16,13,-13,13,-13,8,-8,8,-8,5,-5,5,-5,17,-17,17,-17,12,-12,12,-12,20,-20,20,-20,6,-6,6,-6,21,-21,0,0,21,-21,21,-21,19,-19,19,-19,9,-9,9,-9,1,-1,1,-1,21,-21,21,-21,18,-18,18,-18,11,-11,11,-11,2,-2,2,-2,20,-20,20,-20,7,-7,7,-7,21,-21,21,-21,15,-15,15,-15,3,-3,3,-3,16,-16,16,-16,14,-14,14,-14,21,-21,21,-21,4,-4,4,-4,17,-17,17,-17,13,-13,13,-13,19,-19,19,-19,10,-10,10,-10,20,-20,20,-20,8,-8,8,-8,21,-21,21,-21,5,-5,5,-5,18,-18,18,-18,12,-12,12,-12,21,-21,21,-21,6,-6,6,-6,20,-20,20,-20,16,-16,16,-16,15,-15,15,-15,9,-9,9,-9,19,-19,19,-19,11,-11,11,-11,22,-22,0,0,22,-22,22,-22,17,-17,17,-17,14,-14,14,-14,1,-1,1,-1,22,-22,22,-22,2,-2,2,-2,21,-21,21,-21,7,-7,7,-7,22,-22,22,-22,18,-18,18,-18,13,-13,13,-13,3,-3,3,-3,22,-22,22,-22,20,-20,20,-20,10,-10,10,-10,4,-4,4,-4,21,-21,21,-21,19,-19,19,-19,12,-12,12,-12,8,-8,8,-8,22,-22,22,-22,5,-5,5,-5,16,-16,16,-16,17,-17,17,-17,15,-15,15,-15,22,-22,22,-22,18,-18,18,-18,14,-14,14,-14,6,-6,6,-6,20,-20,20,-20,11,-11,11,-11,21,-21,21,-21,9,-9,9,-9,23,-23,0,0,23,-23,23,-23,19,-19,19,-19,13,-13,13,-13,1,-1,1,-1,23,-23,23,-23,22,-22,22,-22,7,-7,7,-7,2,-2,2,-2,23,-23,23,-23,3,-3,3,-3,21,-21,21,-21,10,-10,10,-10,20,-20,20,-20,12,-12,12,-12,23,-23,23,-23,17,-17,17,-17,16,-16,16,-16,4,-4,4,-4,22,-22,22,-22,8,-8,8,-8,18,-18,18,-18,15,-15,15,-15,23,-23,23,-23,5,-5,5,-5,19,-19,19,-19,14,-14,14,-14,21,-21,21,-21,11,-11,11,-11,23,-23,23,-23,22,-22,22,-22,9,-9,9,-9,6,-6,6,-6,20,-20,20,-20,13,-13,13,-13,24,-24,0,0,24,-24,24,-24,1,-1,1,-1,23,-23,23,-23,17,-17,17,-17,7,-7,7,-7,24,-24,24,-24,18,-18,18,-18,16,-16,16,-16,2,-2,2,-2,22,-22,22,-22,10,-10,10,-10,24,-24,24,-24,21,-21,21,-21,12,-12,12,-12,3,-3,3,-3,19,-19,19,-19,15,-15,15,-15,24,-24,24,-24,4,-4,4,-4,23,-23,23,-23,8,-8,8,-8,20,-20,20,-20,14,-14,14,-14,24,-24,24,-24,5,-5,5,-5,22,-22,22,-22,11,-11,11,-11,23,-23,23,-23,21,-21,21,-21,13,-13,13,-13,9,-9,9,-9,24,-24,24,-24,6,-6,6,-6,18,-18,18,-18,17,-17,17,-17,19,-19,19,-19,16,-16,16,-16,25,-25,24,-24,24,-24,20,-20,20,-20,15,-15,15,-15,7,-7,7,-7,0,0,25,-25,25,-25,1,-1,1,-1,22,-22,22,-22,12,-12,12,-12,25,-25,25,-25,23,-23,23,-23,10,-10,10,-10,2,-2,2,-2,25,-25,25,-25,3,-3,3,-3,21,-21,21,-21,14,-14,14,-14,24,-24,24,-24,8,-8,8,-8,25,-25,25,-25,4,-4,4,-4,18,-18,18,-18,25,-25,25,-25,23,-23,23,-23,19,-19,19,-19,17,-17,17,-17,11,-11,11,-11,5,-5,5,-5,22,-22,22,-22,13,-13,13,-13,20,-20,20,-20,16,-16,16,-16,24,-24,24,-24,9,-9,9,-9,25,-25,25,-25,6,-6,6,-6,21,-21,21,-21,15,-15,15,-15,23,-23,23,-23,12,-12,12,-12,25,-25,25,-25,7,-7,7,-7,26,-26,24,-24,24,-24,10,-10,10,-10,0,0,26,-26,26,-26,1,-1,1,-1,26,-26,26,-26,22,-22,22,-22,14,-14,14,-14,2,-2,2,-2,26,-26,26,-26,19,-19,19,-19,18,-18,18,-18,3,-3,3,-3,25,-25,25,-25,20,-20,20,-20,17,-17,17,-17,8,-8,8,-8,26,-26,26,-26,4,-4,4,-4,24,-24,24,-24,21,-21,21,-21,16,-16,16,-16,11,-11,11,-11,23,-23,23,-23,13,-13,13,-13,26,-26,26,-26,5,-5,5,-5,25,-25,25,-25,9,-9,9,-9,22,-22,22,-22,15,-15,15,-15,26,-26,26,-26,6,-6,6,-6,24,-24,24,-24,12,-12,12,-12,19,-19,19,-19,20,-20,20,-20,18,-18,18,-18,26,-26,26,-26,25,-25,25,-25,23,-23,23,-23,14,-14,14,-14,10,-10,10,-10,7,-7,7,-7,27,-27,0,0,27,-27,27,-27,21,-21,21,-21,17,-17,17,-17,1,-1,1,-1,27,-27,27,-27,2,-2,2,-2,27,-27,27,-27,3,-3,3,-3,26,-26,26,-26,22,-22,22,-22,16,-16,16,-16,8,-8,8,-8,27,-27,27,-27,24,-24,24,-24,13,-13,13,-13,4,-4,4,-4,25,-25,25,-25,11,-11,11,-11,27,-27,27,-27,23,-23,23,-23,15,-15,15,-15,5,-5,5,-5,26,-26,26,-26,9,-9,9,-9,20,-20,20,-20,19,-19,19,-19,27,-27,27,-27,21,-21,21,-21,18,-18,18,-18,6,-6,6,-6,25,-25,25,-25,12,-12,12,-12,24,-24,24,-24,14,-14,14,-14,22,-22,22,-22,17,-17,17,-17,26,-26,26,-26,10,-10,10,-10,27,-27,27,-27,7,-7,7,-7,28,-28,0,0,28,-28,28,-28,23,-23,23,-23,16,-16,16,-16,1,-1,1,-1,28,-28,28,-28,2,-2,2,-2,28,-28,28,-28,27,-27,27,-27,8,-8,8,-8,3,-3,3,-3,25,-25,25,-25,13,-13,13,-13,26,-26,26,-26,11,-11,11,-11,28,-28,28,-28,20,-20,20,-20,4,-4,4,-4,24,-24,24,-24,15,-15,15,-15,21,-21,21,-21,19,-19,19,-19,22,-22,22,-22,18,-18,18,-18,28,-28,28,-28,5,-5,5,-5,27,-27,27,-27,9,-9,9,-9,23,-23,23,-23,17,-17,17,-17,28,-28,28,-28,26,-26,26,-26,12,-12,12,-12,6,-6,6,-6,25,-25,25,-25,14,-14,14,-14,27,-27,27,-27,10,-10,10,-10,24,-24,24,-24,16,-16,16,-16,28,-28,28,-28,7,-7,7,-7,29,-29,21,-21,21,-21,20,-20,20,-20,0,0,29,-29,29,-29,1,-1,1,-1,29,-29,29,-29,26,-26,26,-26,22,-22,22,-22,19,-19,19,-19,13,-13,13,-13,2,-2,2,-2,28,-28,28,-28,8,-8,8,-8,29,-29,29,-29,27,-27,27,-27,25,-25,25,-25,15,-15,15,-15,11,-11,11,-11,3,-3,3,-3,23,-23,23,-23,18,-18,18,-18,29,-29,29,-29,4,-4,4,-4,28,-28,28,-28,24,-24,24,-24,17,-17,17,-17,9,-9,9,-9,29,-29,29,-29,5,-5,5,-5,26,-26,26,-26,14,-14,14,-14,27,-27,27,-27,12,-12,12,-12,29,-29,29,-29,6,-6,6,-6,25,-25,25,-25,16,-16,16,-16,21,-21,21,-21,28,-28,28,-28,22,-22,22,-22,20,-20,20,-20,10,-10,10,-10,29,-29,29,-29,23,-23,23,-23,19,-19,19,-19,7,-7,7,-7,27,-27,27,-27,13,-13,13,-13,30,-30,24,-24,24,-24,18,-18,18,-18,0,0,30,-30,30,-30,26,-26,26,-26,15,-15,15,-15,1,-1,1,-1,30,-30,30,-30,2,-2,2,-2,29,-29,29,-29,28,-28,28,-28,11,-11,11,-11,8,-8,8,-8,30,-30,30,-30,3,-3,3,-3,25,-25,25,-25,17,-17,17,-17,30,-30,30,-30,4,-4,4,-4,29,-29,29,-29,9,-9,9,-9,30,-30,30,-30,27,-27,27,-27,22,-22,22,-22,21,-21,21,-21,14,-14,14,-14,5,-5,5,-5,28,-28,28,-28,12,-12,12,-12,23,-23,23,-23,20,-20,20,-20,26,-26,26,-26,16,-16,16,-16,30,-30,30,-30,6,-6,6,-6,24,-24,24,-24,19,-19,19,-19,29,-29,29,-29,10,-10,10,-10,30,-30,30,-30,25,-25,25,-25,18,-18,18,-18,7,-7,7,-7,28,-28,28,-28,13,-13,13,-13,27,-27,27,-27,15,-15,15,-15,31,-31,0,0,31,-31,31,-31,29,-29,29,-29,11,-11,11,-11,1,-1,1,-1,30,-30,30,-30,8,-8,8,-8,31,-31,31,-31,26,-26,26,-26,17,-17,17,-17,2,-2,2,-2,22,-22,22,-22,31,-31,31,-31,23,-23,23,-23,21,-21,21,-21,3,-3,3,-3,24,-24,24,-24,20,-20,20,-20,31,-31,31,-31,4,-4,4,-4,28,-28,28,-28,14,-14,14,-14,30,-30,30,-30,9,-9,9,-9,29,-29,29,-29,27,-27,27,-27,16,-16,16,-16,12,-12,12,-12,31,-31,31,-31,25,-25,25,-25,19,-19,19,-19,5,-5,5,-5,31,-31,31,-31,6,-6,6,-6,30,-30,30,-30,26,-26,26,-26,18,-18,18,-18,10,-10,10,-10,28,-28,28,-28,15,-15,15,-15,31,-31,31,-31,29,-29,29,-29,13,-13,13,-13,7,-7,7,-7,23,-23,23,-23,22,-22,22,-22,24,-24,24,-24,21,-21,21,-21,27,-27,27,-27,17,-17,17,-17,30,-30,30,-30,11,-11,11,-11,32,-32,0,0,32,-32,32,-32,31,-31,31,-31,25,-25,25,-25,20,-20,20,-20,8,-8,8,-8,1,-1,1,-1,32,-32,32,-32,2,-2,2,-2,32,-32,32,-32,3,-3,3,-3,29,-29,29,-29,26,-26,26,-26,19,-19,19,-19,14,-14,14,-14,32,-32,32,-32,28,-28,28,-28,16,-16,16,-16,4,-4,4,-4,31,-31,31,-31,9,-9,9,-9,30,-30,30,-30,12,-12,12,-12,32,-32,32,-32,5,-5,5,-5,27,-27,27,-27,18,-18,18,-18,23,-23,23,-23,32,-32,32,-32,24,-24,24,-24,22,-22,22,-22,6,-6,6,-6,31,-31,31,-31,10,-10,10,-10,29,-29,29,-29,25,-25,25,-25,21,-21,21,-21,15,-15,15,-15,30,-30,30,-30,13,-13,13,-13,32,-32,32,-32,28,-28,28,-28,17,-17,17,-17,7,-7,7,-7,26,-26,26,-26,20,-20,20,-20,31,-31,31,-31,11,-11,11,-11,32,-32,32,-32,8,-8,8,-8,33,-33,0,0,33,-33,33,-33,27,-27,27,-27,19,-19,19,-19,1,-1,1,-1,33,-33,33,-33,2,-2,2,-2,30,-30,30,-30,14,-14,14,-14,29,-29,29,-29,16,-16,16,-16,33,-33,33,-33,3,-3,3,-3,33,-33,33,-33,32,-32,32,-32,31,-31,31,-31,24,-24,24,-24,23,-23,23,-23,12,-12,12,-12,9,-9,9,-9,4,-4,4,-4,28,-28,28,-28,18,-18,18,-18,25,-25,25,-25,22,-22,22,-22,33,-33,33,-33,5,-5,5,-5,26,-26,26,-26,21,-21,21,-21,32,-32,32,-32,10,-10,10,-10,33,-33,33,-33,30,-30,30,-30,15,-15,15,-15,6,-6,6,-6,27,-27,27,-27,20,-20,20,-20,31,-31,31,-31,29,-29,29,-29,17,-17,17,-17,13,-13,13,-13,33,-33,33,-33,7,-7,7,-7,32,-32,32,-32,28,-28,28,-28,19,-19,19,-19,11,-11,11,-11,24,-24,24,-24,33,-33,33,-33,8,-8,8,-8,25,-25,25,-25,23,-23,23,-23,34,-34,30,-30,30,-30,16,-16,16,-16,0,0,34,-34,34,-34,31,-31,31,-31,14,-14,14,-14,1,-1,1,-1,34,-34,34,-34,26,-26,26,-26,22,-22,22,-22,2,-2,2,-2,34,-34,34,-34,29,-29,29,-29,18,-18,18,-18,3,-3,3,-3,32,-32,32,-32,12,-12,12,-12,33,-33,33,-33,27,-27,27,-27,21,-21,21,-21,9,-9,9,-9,34,-34,34,-34,4,-4,4,-4,34,-34,34,-34,5,-5,5,-5,28,-28,28,-28,20,-20,20,-20,31,-31,31,-31,15,-15,15,-15,33,-33,33,-33,30,-30,30,-30,17,-17,17,-17,10,-10,10,-10,34,-34,34,-34,6,-6,6,-6,32,-32,32,-32,13,-13,13,-13,25,-25,25,-25,24,-24,24,-24,29,-29,29,-29,19,-19,19,-19,34,-34,34,-34,26,-26,26,-26,23,-23,23,-23,7,-7,7,-7,33,-33,33,-33,11,-11,11,-11,27,-27,27,-27,22,-22,22,-22,31,-31,31,-31,16,-16,16,-16,34,-34,34,-34,32,-32,32,-32,14,-14,14,-14,8,-8,8,-8,30,-30,30,-30,18,-18,18,-18,35,-35,28,-28,28,-28,21,-21,21,-21,0,0,35,-35,35,-35,1,-1,1,-1,35,-35,35,-35,2,-2,2,-2,33,-33,33,-33,12,-12,12,-12,35,-35,35,-35,3,-3,3,-3,34,-34,34,-34,9,-9,9,-9,35,-35,35,-35,29,-29,29,-29,20,-20,20,-20,4,-4,4,-4,32,-32,32,-32,15,-15,15,-15,35,-35,35,-35,31,-31,31,-31,25,-25,25,-25,17,-17,17,-17,5,-5,5,-5,26,-26,26,-26,24,-24,24,-24,34,-34,34,-34,10,-10,10,-10,33,-33,33,-33,27,-27,27,-27,23,-23,23,-23,13,-13,13,-13,35,-35,35,-35,30,-30,30,-30,19,-19,19,-19,6,-6,6,-6,28,-28,28,-28,22,-22,22,-22,35,-35,35,-35,7,-7,7,-7,34,-34,34,-34,11,-11,11,-11,32,-32,32,-32,16,-16,16,-16,29,-29,29,-29,21,-21,21,-21,33,-33,33,-33,31,-31,31,-31,18,-18,18,-18,14,-14,14,-14,35,-35,35,-35,8,-8,8,-8,36,-36,0,0,36,-36,36,-36,1,-1,1,-1,36,-36,36,-36,34,-34,34,-34,30,-30,30,-30,20,-20,20,-20,12,-12,12,-12,2,-2,2,-2,26,-26,26,-26,25,-25,25,-25,36,-36,36,-36,27,-27,27,-27,24,-24,24,-24,3,-3,3,-3,35,-35,35,-35,9,-9,9,-9,36,-36,36,-36,4,-4,4,-4,32,-32,32,-32,28,-28,28,-28,23,-23,23,-23,17,-17,17,-17,33,-33,33,-33,15,-15,15,-15,36,-36,36,-36,5,-5,5,-5,31,-31,31,-31,19,-19,19,-19,35,-35,35,-35,34,-34,34,-34,29,-29,29,-29,22,-22,22,-22,13,-13,13,-13,10,-10,10,-10,36,-36,36,-36,6,-6,6,-6,30,-30,30,-30,21,-21,21,-21,36,-36,36,-36,33,-33,33,-33,16,-16,16,-16,7,-7,7,-7,35,-35,35,-35,11,-11,11,-11,32,-32,32,-32,18,-18,18,-18,34,-34,34,-34,26,-26,26,-26,14,-14,14,-14,27,-27,27,-27,25,-25,25,-25,36,-36,36,-36,28,-28,28,-28,24,-24,24,-24,8,-8,8,-8,31,-31,31,-31,20,-20,20,-20,37,-37,35,-35,35,-35,12,-12,12,-12,0,0,37,-37,37,-37,29,-29,29,-29,23,-23,23,-23,1,-1,1,-1,37,-37,37,-37,2,-2,2,-2,36,-36,36,-36,9,-9,9,-9,37,-37,37,-37,33,-33,33,-33,17,-17,17,-17,3,-3,3,-3,34,-34,34,-34,15,-15,15,-15,30,-30,30,-30,22,-22,22,-22,37,-37,37,-37,32,-32,32,-32,19,-19,19,-19,4,-4,4,-4,37,-37,37,-37,35,-35,35,-35,13,-13,13,-13,5,-5,5,-5,36,-36,36,-36,10,-10,10,-10,31,-31,31,-31,21,-21,21,-21,37,-37,37,-37,27,-27,27,-27,26,-26,26,-26,6,-6,6,-6,28,-28,28,-28,25,-25,25,-25,34,-34,34,-34,16,-16,16,-16,33,-33,33,-33,18,-18,18,-18,36,-36,36,-36,29,-29,29,-29,24,-24,24,-24,11,-11,11,-11,37,-37,37,-37,7,-7,7,-7,35,-35,35,-35,14,-14,14,-14,32,-32,32,-32,20,-20,20,-20,30,-30,30,-30,23,-23,23,-23,37,-37,37,-37,8,-8,8,-8,36,-36,36,-36,12,-12,12,-12,38,-38,0,0,38,-38,38,-38,34,-34,34,-34,31,-31,31,-31,22,-22,22,-22,17,-17,17,-17,1,-1,1,-1,38,-38,38,-38,2,-2,2,-2,37,-37,37,-37,35,-35,35,-35,33,-33,33,-33,19,-19,19,-19,15,-15,15,-15,9,-9,9,-9,38,-38,38,-38,3,-3,3,-3,27,-27,27,-27,38,-38,38,-38,28,-28,28,-28,26,-26,26,-26,4,-4,4,-4,36,-36,36,-36,32,-32,32,-32,21,-21,21,-21,13,-13,13,-13,29,-29,29,-29,25,-25,25,-25,38,-38,38,-38,37,-37,37,-37,10,-10,10,-10,5,-5,5,-5,30,-30,30,-30,24,-24,24,-24,38,-38,38,-38,34,-34,34,-34,18,-18,18,-18,6,-6,6,-6,35,-35,35,-35,16,-16,16,-16,33,-33,33,-33,20,-20,20,-20,37,-37,37,-37,31,-31,31,-31,23,-23,23,-23,11,-11,11,-11,36,-36,36,-36,14,-14,14,-14,38,-38,38,-38,7,-7,7,-7,38,-38,38,-38,32,-32,32,-32,22,-22,22,-22,8,-8,8,-8,37,-37,37,-37,28,-28,28,-28,27,-27,27,-27,12,-12,12,-12,35,-35,35,-35,17,-17,17,-17,34,-34,34,-34,29,-29,29,-29,26,-26,26,-26,19,-19,19,-19,39,-39,36,-36,36,-36,15,-15,15,-15,0,0,39,-39,39,-39,1,-1,1,-1,39,-39,39,-39,38,-38,38,-38,30,-30,30,-30,25,-25,25,-25,9,-9,9,-9,2,-2,2,-2,39,-39,39,-39,33,-33,33,-33,21,-21,21,-21,3,-3,3,-3,39,-39,39,-39,31,-31,31,-31,24,-24,24,-24,4,-4,4,-4,37,-37,37,-37,13,-13,13,-13,38,-38,38,-38,10,-10,10,-10,39,-39,39,-39,5,-5,5,-5,35,-35,35,-35,18,-18,18,-18,36,-36,36,-36,16,-16,16,-16,32,-32,32,-32,23,-23,23,-23,34,-34,34,-34,20,-20,20,-20,39,-39,39,-39,6,-6,6,-6,38,-38,38,-38,37,-37,37,-37,14,-14,14,-14,11,-11,11,-11,28,-28,28,-28,39,-39,39,-39,29,-29,29,-29,27,-27,27,-27,7,-7,7,-7,33,-33,33,-33,22,-22,22,-22,30,-30,30,-30,26,-26,26,-26,39,-39,39,-39,36,-36,36,-36,17,-17,17,-17,8,-8,8,-8,35,-35,35,-35,31,-31,31,-31,25,-25,25,-25,19,-19,19,-19,38,-38,38,-38,12,-12,12,-12,37,-37,37,-37,15,-15,15,-15,34,-34,34,-34,21,-21,21,-21,40,-40,32,-32,32,-32,24,-24,24,-24,0,0,40,-40,40,-40,1,-1,1,-1,39,-39,39,-39,9,-9,9,-9,40,-40,40,-40,2,-2,2,-2,40,-40,40,-40,3,-3,3,-3,38,-38,38,-38,13,-13,13,-13,40,-40,40,-40,4,-4,4,-4,33,-33,33,-33,23,-23,23,-23,36,-36,36,-36,18,-18,18,-18,39,-39,39,-39,10,-10,10,-10,40,-40,40,-40,37,-37,37,-37,35,-35,35,-35,29,-29,29,-29,28,-28,28,-28,20,-20,20,-20,16,-16,16,-16,5,-5,5,-5,30,-30,30,-30,27,-27,27,-27,40,-40,40,-40,6,-6,6,-6,31,-31,31,-31,26,-26,26,-26,38,-38,38,-38,34,-34,34,-34,22,-22,22,-22,14,-14,14,-14,39,-39,39,-39,11,-11,11,-11,40,-40,40,-40,32,-32,32,-32,25,-25,25,-25,7,-7,7,-7,36,-36,36,-36,19,-19,19,-19,37,-37,37,-37,17,-17,17,-17,40,-40,40,-40,8,-8,8,-8,39,-39,39,-39,33,-33,33,-33,24,-24,24,-24,12,-12,12,-12,35,-35,35,-35,21,-21,21,-21,38,-38,38,-38,15,-15,15,-15,41,-41,40,-40,40,-40,9,-9,9,-9,0,0,41,-41,41,-41,29,-29,29,-29,1,-1,1,-1,30,-30,30,-30,28,-28,28,-28,41,-41,41,-41,34,-34,34,-34,23,-23,23,-23,2,-2,2,-2,41,-41,41,-41,39,-39,39,-39,31,-31,31,-31,27,-27,27,-27,13,-13,13,-13,3,-3,3,-3,37,-37,37,-37,18,-18,18,-18,36,-36,36,-36,20,-20,20,-20,41,-41,41,-41,4,-4,4,-4,40,-40,40,-40,38,-38,38,-38,32,-32,32,-32,26,-26,26,-26,16,-16,16,-16,10,-10,10,-10,41,-41,41,-41,5,-5,5,-5,35,-35,35,-35,22,-22,22,-22,33,-33,33,-33,25,-25,25,-25,41,-41,41,-41,39,-39,39,-39,14,-14,14,-14,6,-6,6,-6,40,-40,40,-40,11,-11,11,-11,41,-41,41,-41,37,-37,37,-37,19,-19,19,-19,7,-7,7,-7,34,-34,34,-34,24,-24,24,-24,38,-38,38,-38,17,-17,17,-17,36,-36,36,-36,21,-21,21,-21,30,-30,30,-30,29,-29,29,-29,40,-40,40,-40,12,-12,12,-12,41,-41,41,-41,31,-31,31,-31,28,-28,28,-28,8,-8,8,-8,39,-39,39,-39,15,-15,15,-15,32,-32,32,-32,27,-27,27,-27,35,-35,35,-35,23,-23,23,-23,41,-41,41,-41,9,-9,9,-9,42,-42,0,0,42,-42,42,-42,33,-33,33,-33,26,-26,26,-26,1,-1,1,-1,42,-42,42,-42,38,-38,38,-38,18,-18,18,-18,2,-2,2,-2,40,-40,40,-40,37,-37,37,-37,20,-20,20,-20,13,-13,13,-13,42,-42,42,-42,3,-3,3,-3,39,-39,39,-39,16,-16,16,-16,42,-42,42,-42,36,-36,36,-36,22,-22,22,-22,4,-4,4,-4,41,-41,41,-41,34,-34,34,-34,25,-25,25,-25,10,-10,10,-10,42,-42,42,-42,5,-5,5,-5,40,-40,40,-40,14,-14,14,-14,42,-42,42,-42,30,-30,30,-30,6,-6,6,-6,35,-35,35,-35,24,-24,24,-24,41,-41,41,-41,31,-31,31,-31,29,-29,29,-29,11,-11,11,-11,38,-38,38,-38,19,-19,19,-19,32,-32,32,-32,28,-28,28,-28,39,-39,39,-39,37,-37,37,-37,21,-21,21,-21,17,-17,17,-17,42,-42,42,-42,7,-7,7,-7,33,-33,33,-33,27,-27,27,-27,41,-41,41,-41,40,-40,40,-40,36,-36,36,-36,23,-23,23,-23,15,-15,15,-15,12,-12,12,-12,42,-42,42,-42,8,-8,8,-8,34,-34,34,-34,26,-26,26,-26,38,-38,38,-38,20,-20,20,-20,42,-42,42,-42,39,-39,39,-39,18,-18,18,-18,9,-9,9,-9,43,-43,0,0,43,-43,43,-43,41,-41,41,-41,35,-35,35,-35,25,-25,25,-25,13,-13,13,-13,1,-1,1,-1,43,-43,43,-43,37,-37,37,-37,22,-22,22,-22,2,-2,2,-2,40,-40,40,-40,16,-16,16,-16,43,-43,43,-43,3,-3,3,-3,31,-31,31,-31,30,-30,30,-30,42,-42,42,-42,10,-10,10,-10,43,-43,43,-43,32,-32,32,-32,29,-29,29,-29,4,-4,4,-4,36,-36,36,-36,24,-24,24,-24,33,-33,33,-33,28,-28,28,-28,43,-43,43,-43,5,-5,5,-5,41,-41,41,-41,14,-14,14,-14,39,-39,39,-39,19,-19,19,-19,43,-43,43,-43,42,-42,42,-42,38,-38,38,-38,34,-34,34,-34,27,-27,27,-27,21,-21,21,-21,11,-11,11,-11,6,-6,6,-6,40,-40,40,-40,17,-17,17,-17,43,-43,43,-43,37,-37,37,-37,23,-23,23,-23,7,-7,7,-7,35,-35,35,-35,26,-26,26,-26,41,-41,41,-41,15,-15,15,-15,42,-42,42,-42,12,-12,12,-12,43,-43,43,-43,8,-8,8,-8,39,-39,39,-39,36,-36,36,-36,25,-25,25,-25,20,-20,20,-20,31,-31,31,-31,40,-40,40,-40,32,-32,32,-32,30,-30,30,-30,18,-18,18,-18,38,-38,38,-38,22,-22,22,-22,43,-43,43,-43,33,-33,33,-33,29,-29,29,-29,9,-9,9,-9,42,-42,42,-42,13,-13,13,-13,44,-44,0,0,44,-44,44,-44,41,-41,41,-41,16,-16,16,-16,1,-1,1,-1,44,-44,44,-44,34,-34,34,-34,28,-28,28,-28,2,-2,2,-2,44,-44,44,-44,37,-37,37,-37,24,-24,24,-24,3,-3,3,-3,43,-43,43,-43,10,-10,10,-10,44,-44,44,-44,4,-4,4,-4,35,-35,35,-35,27,-27,27,-27,42,-42,42,-42,14,-14,14,-14,44,-44,44,-44,40,-40,40,-40,19,-19,19,-19,5,-5,5,-5,39,-39,39,-39,21,-21,21,-21,43,-43,43,-43,41,-41,41,-41,17,-17,17,-17,11,-11,11,-11,44,-44,44,-44,36,-36,36,-36,26,-26,26,-26,6,-6,6,-6,38,-38,38,-38,23,-23,23,-23,44,-44,44,-44,32,-32,32,-32,31,-31,31,-31,7,-7,7,-7,42,-42,42,-42,33,-33,33,-33,30,-30,30,-30,15,-15,15,-15,43,-43,43,-43,12,-12,12,-12,37,-37,37,-37,25,-25,25,-25,34,-34,34,-34,29,-29,29,-29,44,-44,44,-44,40,-40,40,-40,20,-20,20,-20,8,-8,8,-8,41,-41,41,-41,39,-39,39,-39,22,-22,22,-22,18,-18,18,-18,35,-35,35,-35,28,-28,28,-28,44,-44,44,-44,9,-9,9,-9,43,-43,43,-43,13,-13,13,-13,42,-42,42,-42,38,-38,38,-38,24,-24,24,-24,16,-16,16,-16,45,-45,36,-36,36,-36,27,-27,27,-27,0,0,45,-45,45,-45,1,-1,1,-1,45,-45,45,-45,2,-2,2,-2,45,-45,45,-45,3,-3,3,-3,44,-44,44,-44,10,-10,10,-10,45,-45,45,-45,40,-40,40,-40,21,-21,21,-21,4,-4,4,-4,41,-41,41,-41,19,-19,19,-19,43,-43,43,-43,37,-37,37,-37,26,-26,26,-26,14,-14,14,-14,32,-32,32,-32,45,-45,45,-45,39,-39,39,-39,33,-33,33,-33,31,-31,31,-31,23,-23,23,-23,5,-5,5,-5,42,-42,42,-42,17,-17,17,-17,34,-34,34,-34,30,-30,30,-30,44,-44,44,-44,11,-11,11,-11,45,-45,45,-45,6,-6,6,-6,35,-35,35,-35,29,-29,29,-29,38,-38,38,-38,25,-25,25,-25,45,-45,45,-45,43,-43,43,-43,15,-15,15,-15,7,-7,7,-7,44,-44,44,-44,36,-36,36,-36,28,-28,28,-28,12,-12,12,-12,41,-41,41,-41,20,-20,20,-20,40,-40,40,-40,22,-22,22,-22,42,-42,42,-42,18,-18,18,-18,45,-45,45,-45,8,-8,8,-8,39,-39,39,-39,24,-24,24,-24,37,-37,37,-37,27,-27,27,-27,44,-44,44,-44,43,-43,43,-43,16,-16,16,-16,13,-13,13,-13,45,-45,45,-45,9,-9,9,-9,33,-33,33,-33,32,-32,32,-32,46,-46,0,0,46,-46,46,-46,34,-34,34,-34,31,-31,31,-31,1,-1,1,-1,46,-46,46,-46,38,-38,38,-38,26,-26,26,-26,2,-2,2,-2,41,-41,41,-41,21,-21,21,-21,46,-46,46,-46,45,-45,45,-45,42,-42,42,-42,35,-35,35,-35,30,-30,30,-30,19,-19,19,-19,10,-10,10,-10,3,-3,3,-3,40,-40,40,-40,23,-23,23,-23,46,-46,46,-46,44,-44,44,-44,14,-14,14,-14,4,-4,4,-4,36,-36,36,-36,29,-29,29,-29,43,-43,43,-43,17,-17,17,-17,46,-46,46,-46,5,-5,5,-5,45,-45,45,-45,39,-39,39,-39,25,-25,25,-25,11,-11,11,-11,46,-46,46,-46,6,-6,6,-6,37,-37,37,-37,28,-28,28,-28,44,-44,44,-44,15,-15,15,-15,42,-42,42,-42,20,-20,20,-20,46,-46,46,-46,41,-41,41,-41,22,-22,22,-22,7,-7,7,-7,45,-45,45,-45,12,-12,12,-12,43,-43,43,-43,38,-38,38,-38,27,-27,27,-27,18,-18,18,-18,40,-40,40,-40,24,-24,24,-24,33,-33,33,-33,46,-46,46,-46,34,-34,34,-34,32,-32,32,-32,8,-8,8,-8,35,-35,35,-35,31,-31,31,-31,44,-44,44,-44,16,-16,16,-16,45,-45,45,-45,13,-13,13,-13,36,-36,36,-36,30,-30,30,-30,46,-46,46,-46,39,-39,39,-39,26,-26,26,-26,9,-9,9,-9,42,-42,42,-42,21,-21,21,-21,47,-47,0,0,47,-47,47,-47,43,-43,43,-43,41,-41,41,-41,37,-37,37,-37,29,-29,29,-29,23,-23,23,-23,19,-19,19,-19,1,-1,1,-1,47,-47,47,-47,2,-2,2,-2,46,-46,46,-46,10,-10,10,-10,47,-47,47,-47,3,-3,3,-3,45,-45,45,-45,14,-14,14,-14,47,-47,47,-47,44,-44,44,-44,40,-40,40,-40,25,-25,25,-25,17,-17,17,-17,4,-4,4,-4,38,-38,38,-38,28,-28,28,-28,47,-47,47,-47,5,-5,5,-5,46,-46,46,-46,11,-11,11,-11,47,-47,47,-47,34,-34,34,-34,33,-33,33,-33,6,-6,6,-6,42,-42,42,-42,22,-22,22,-22,43,-43,43,-43,35,-35,35,-35,32,-32,32,-32,20,-20,20,-20,45,-45,45,-45,39,-39,39,-39,27,-27,27,-27,15,-15,15,-15,41,-41,41,-41,36,-36,36,-36,31,-31,31,-31,24,-24,24,-24,47,-47,47,-47,7,-7,7,-7,46,-46,46,-46,44,-44,44,-44,18,-18,18,-18,12,-12,12,-12,37,-37,37,-37,30,-30,30,-30,47,-47,47,-47,8,-8,8,-8,40,-40,40,-40,26,-26,26,-26,45,-45,45,-45,16,-16,16,-16,46,-46,46,-46,38,-38,38,-38,29,-29,29,-29,13,-13,13,-13,47,-47,47,-47,43,-43,43,-43,21,-21,21,-21,9,-9,9,-9,42,-42,42,-42,23,-23,23,-23,44,-44,44,-44,19,-19,19,-19,48,-48,0,0,48,-48,48,-48,39,-39,39,-39,28,-28,28,-28,1,-1,1,-1,41,-41,41,-41,25,-25,25,-25,48,-48,48,-48,2,-2,2,-2,47,-47,47,-47,10,-10,10,-10,46,-46,46,-46,34,-34,34,-34,14,-14,14,-14,48,-48,48,-48,3,-3,3,-3,45,-45,45,-45,35,-35,35,-35,33,-33,33,-33,17,-17,17,-17,48,-48,48,-48,36,-36,36,-36,32,-32,32,-32,4,-4,4,-4,48,-48,48,-48,40,-40,40,-40,27,-27,27,-27,5,-5,5,-5,47,-47,47,-47,37,-37,37,-37,31,-31,31,-31,11,-11,11,-11,43,-43,43,-43,22,-22,22,-22,44,-44,44,-44,20,-20,20,-20,48,-48,48,-48,42,-42,42,-42,24,-24,24,-24,6,-6,6,-6,46,-46,46,-46,15,-15,15,-15,38,-38,38,-38,30,-30,30,-30,45,-45,45,-45,18,-18,18,-18,48,-48,48,-48,47,-47,47,-47,12,-12,12,-12,7,-7,7,-7,41,-41,41,-41,26,-26,26,-26,39,-39,39,-39,29,-29,29,-29,48,-48,48,-48,8,-8,8,-8,46,-46,46,-46,16,-16,16,-16,44,-44,44,-44,21,-21,21,-21,47,-47,47,-47,43,-43,43,-43,23,-23,23,-23,13,-13,13,-13,35,-35,35,-35,34,-34,34,-34,40,-40,40,-40,28,-28,28,-28,48,-48,48,-48,36,-36,36,-36,33,-33,33,-33,9,-9,9,-9,45,-45,45,-45,19,-19,19,-19,42,-42,42,-42,25,-25,25,-25,37,-37,37,-37,32,-32,32,-32,49,-49,0,0,49,-49,49,-49,1,-1,1,-1,48,-48,48,-48,10,-10,10,-10,49,-49,49,-49,47,-47,47,-47,46,-46,46,-46,38,-38,38,-38,31,-31,31,-31,17,-17,17,-17,14,-14,14,-14,2,-2,2,-2,49,-49,49,-49,41,-41,41,-41,27,-27,27,-27,3,-3,3,-3,49,-49,49,-49,4,-4,4,-4,44,-44,44,-44,22,-22,22,-22,39,-39,39,-39,30,-30,30,-30,48,-48,48,-48,45,-45,45,-45,43,-43,43,-43,24,-24,24,-24,20,-20,20,-20,11,-11,11,-11,49,-49,49,-49,5,-5,5,-5,47,-47,47,-47,15,-15,15,-15,49,-49,49,-49,6,-6,6,-6,46,-46,46,-46,42,-42,42,-42,26,-26,26,-26,18,-18,18,-18,40,-40,40,-40,29,-29,29,-29,48,-48,48,-48,12,-12,12,-12,49,-49,49,-49,35,-35,35,-35,7,-7,7,-7,36,-36,36,-36,34,-34,34,-34,37,-37,37,-37,33,-33,33,-33,49,-49,49,-49,47,-47,47,-47,44,-44,44,-44,41,-41,41,-41,28,-28,28,-28,23,-23,23,-23,16,-16,16,-16,8,-8,8,-8,45,-45,45,-45,21,-21,21,-21,38,-38,38,-38,32,-32,32,-32,48,-48,48,-48,13,-13,13,-13,43,-43,43,-43,25,-25,25,-25,46,-46,46,-46,19,-19,19,-19,49,-49,49,-49,39,-39,39,-39,31,-31,31,-31,9,-9,9,-9,42,-42,42,-42,27,-27,27,-27,47,-47,47,-47,17,-17,17,-17,50,-50,48,-48,48,-48,40,-40,40,-40,30,-30,30,-30,14,-14,14,-14,0,0}; diff --git a/cpp/a_star/expansion_groups.py b/cpp/a_star/expansion_groups.py new file mode 100644 index 0000000..e2a2490 --- /dev/null +++ b/cpp/a_star/expansion_groups.py @@ -0,0 +1,60 @@ +from os import getcwd +from os.path import join, dirname +from math import sqrt + +MAX_RADIUS = 5 +LINES = 321 +COLS = 221 + +expansion_groups = dict() +CWD = join(getcwd(), dirname(__file__)) + +for l in range(MAX_RADIUS*10+1): + for c in range(MAX_RADIUS*10+1): + dist = sqrt(l*l+c*c)*0.1 + if dist <= MAX_RADIUS: + if not dist in expansion_groups: + expansion_groups[dist] = [] + expansion_groups[dist].append((l,c)) + + if c>0: expansion_groups[dist].append((l,-c)) + if l>0: expansion_groups[dist].append((-l,c)) + if c>0 and l>0: expansion_groups[dist].append((-l,-c)) + +ascending_dist = sorted(expansion_groups) +groups_no = len(expansion_groups) + +#============================================= prepare text to file + +no_of_pos = 0 +for g in expansion_groups.values(): + no_of_pos += len(g) + +file = [] +file.append(f"const int expansion_positions_no = {no_of_pos};\n") +file.append(f"const float expansion_pos_dist[{no_of_pos}] = {{") + +for dist in ascending_dist: + for p in expansion_groups[dist]: + file.extend([f"{dist}",","]) +file.pop() + +file.append(f"}};\n\nconst int expansion_pos_l[{no_of_pos}] = {{") + +for dist in ascending_dist: + for p in expansion_groups[dist]: + file.extend([f"{p[0]}",","]) +file.pop() + +file.append(f"}};\n\nconst int expansion_pos_c[{no_of_pos}] = {{") + +for dist in ascending_dist: + for p in expansion_groups[dist]: + file.extend([f"{p[1]}",","]) +file.pop() +file.append("};\n") + +#============================================= write to file + +with open(join(CWD, "expansion_groups.h"), 'w') as f: + f.write(''.join(file)) diff --git a/cpp/a_star/lib_main.cpp b/cpp/a_star/lib_main.cpp new file mode 100644 index 0000000..088c0ae --- /dev/null +++ b/cpp/a_star/lib_main.cpp @@ -0,0 +1,43 @@ +#include "a_star.h" +#include +#include + +namespace py = pybind11; +using namespace std; + + +py::array_t compute( py::array_t parameters ){ + + // ================================================= 1. Parse data + + py::buffer_info parameters_buf = parameters.request(); + int params_len = parameters_buf.shape[0]; + + // ================================================= 2. Compute path + + astar( (float*)parameters_buf.ptr, params_len ); + + // ================================================= 3. Prepare data to return + + py::array_t retval = py::array_t(final_path_size); //allocate + py::buffer_info buff = retval.request(); + float *ptr = (float *) buff.ptr; + + for(int i=0; i "argname"_a + +PYBIND11_MODULE(a_star, m) { // the python module name, m is the interface to create bindings + m.doc() = "Custom A-star implementation"; // optional module docstring + + // optional arguments names + m.def("compute", &compute, "Compute the best path", "parameters"_a); +} \ No newline at end of file diff --git a/cpp/ball_predictor/Makefile b/cpp/ball_predictor/Makefile new file mode 100644 index 0000000..c60f708 --- /dev/null +++ b/cpp/ball_predictor/Makefile @@ -0,0 +1,14 @@ +src = $(wildcard *.cpp) +obj = $(src:.c=.o) + +CFLAGS = -O3 -shared -std=c++11 -fPIC -Wall $(PYBIND_INCLUDES) + +all: $(obj) + g++ $(CFLAGS) -o ball_predictor.so $^ + +debug: $(filter-out lib_main.cpp,$(obj)) + g++ -O0 -std=c++14 -Wall -g -o debug.bin debug_main.cc $^ + +.PHONY: clean +clean: + rm -f $(obj) all diff --git a/cpp/ball_predictor/ball_predictor.cpp b/cpp/ball_predictor/ball_predictor.cpp new file mode 100644 index 0000000..7cf769a --- /dev/null +++ b/cpp/ball_predictor/ball_predictor.cpp @@ -0,0 +1,104 @@ +#include +#include "ball_predictor.h" + + +float ball_pos_pred[600]; // ball position (x,y) prediction for 300*0.02s = 6s +float ball_vel_pred[600]; // ball velocity (x,y) prediction for 300*0.02s = 6s +float ball_spd_pred[300]; // ball linear speed (s) prediction for 300*0.02s = 6s +int pos_pred_len=0; + +/** + * @brief Get intersection with moving ball (intersection point and distance) + * @param x robot position (x) + * @param y robot position (y) + * @param max_robot_sp_per_step maximum speed per step + * @param ball_pos imported ball positions (possibly modified version of ball_pos_pred) + * @param ball_pos_len length of ball_pos + * @param ret_x returned position (x) of intersection point + * @param ret_y returned position (y) of intersection point + * @param ret_d returned distance between robot and intersection point + */ +void get_intersection_with_ball(float x, float y, float max_robot_sp_per_step, float ball_pos[], float ball_pos_len, + float &ret_x, float &ret_y, float &ret_d){ + + float robot_max_displacement = 0.2; // robot has an immediate reach radius of 0.2m + int j=0; + + while(1){ + float vec_x = ball_pos[j++] - x; + float vec_y = ball_pos[j++] - y; + float b_dist_sq = vec_x*vec_x + vec_y*vec_y; // squared ball distance + + // If robot has reached the ball, or the ball has stopped but the robot is still not there + if (b_dist_sq <= robot_max_displacement*robot_max_displacement or j>=ball_pos_len){ + float d = sqrtf(b_dist_sq); + ret_d = d; + ret_x = ball_pos[j-2]; + ret_y = ball_pos[j-1]; + break; + } + robot_max_displacement += max_robot_sp_per_step; + } +} + + +/** + * @brief Predict ball position/velocity until the ball stops or gets out of bounds (up to 6s) + * Adequate when the ball is rolling on the ground + * @param bx ball position (x) + * @param by ball position (y) + * @param vx ball velocity (x) + * @param vy ball velocity (y) + */ +void predict_rolling_ball_pos_vel_spd(double bx, double by, double vx, double vy){ + + // acceleration = Rolling Drag Force * mass (constant = 0.026 kg) + // acceleration = k1 * velocity^2 + k2 * velocity + const double k1 = -0.01; + const double k2 = -1; + + const double k1_x = (vx < 0) ? -k1 : k1; // invert k1 if vx is negative, because vx^2 absorbs the sign + const double k1_y = (vy < 0) ? -k1 : k1; // invert k1 if vy is negative, because vy^2 absorbs the sign + + ball_pos_pred[0] = bx; // current ball position + ball_pos_pred[1] = by; + ball_vel_pred[0] = vx; // current ball velocity + ball_vel_pred[1] = vy; + ball_spd_pred[0] = sqrt(vx*vx+vy*vy); + + int counter = 2; + + while(counter < 600){ + + // acceleration + double acc_x = vx*vx*k1_x + vx*k2; + double acc_y = vy*vy*k1_y + vy*k2; + + // second equation of motion: displacement = v0*t + 0.5*a*t^2 + double dx = vx*0.02 + acc_x*0.0002; // 0.5*0.02^2 = 0.0002 + double dy = vy*0.02 + acc_y*0.0002; // 0.5*0.02^2 = 0.0002 + + // position + bx += dx; + by += dy; + + // abort when displacement is low or ball is out of bounds + if ((fabs(dx) < 0.0005 and fabs(dy) < 0.0005) or fabs(bx) > 15 or fabs(by) > 10){ + break; + } + + // velocity + vx += acc_x*0.02; + vy += acc_y*0.02; + + // store as 32b + ball_spd_pred[counter/2] = sqrt(vx*vx+vy*vy); + ball_vel_pred[counter] = vx; + ball_pos_pred[counter++] = bx; + ball_vel_pred[counter] = vy; + ball_pos_pred[counter++] = by; + + } + + pos_pred_len = counter; +} \ No newline at end of file diff --git a/cpp/ball_predictor/ball_predictor.h b/cpp/ball_predictor/ball_predictor.h new file mode 100644 index 0000000..22018b4 --- /dev/null +++ b/cpp/ball_predictor/ball_predictor.h @@ -0,0 +1,10 @@ +#pragma once + +extern float ball_pos_pred[600]; // ball position (x,y) prediction for 300*0.02s = 6s +extern float ball_vel_pred[600]; // ball velocoty (x,y) prediction for 300*0.02s = 6s +extern float ball_spd_pred[300]; // ball linear speed (s) prediction for 300*0.02s = 6s +extern int pos_pred_len; + +extern void get_intersection_with_ball(float x, float y, float max_robot_sp_per_step, float ball_pos[], float ball_pos_len, + float &ret_x, float &ret_y, float &ret_d); +extern void predict_rolling_ball_pos_vel_spd(double bx, double by, double vx, double vy); diff --git a/cpp/ball_predictor/debug_main.cc b/cpp/ball_predictor/debug_main.cc new file mode 100644 index 0000000..469c4f3 --- /dev/null +++ b/cpp/ball_predictor/debug_main.cc @@ -0,0 +1,54 @@ +#include "ball_predictor.h" +#include +#include +#include + +using std::cout; +using std::chrono::high_resolution_clock; +using std::chrono::duration_cast; +using std::chrono::microseconds; + +std::chrono::_V2::system_clock::time_point t1,t2; + +int main(){ + + // ================================================= 1. Generate data + + float px = 3; + float py = 4; + float vx = -5; + float vy = -1; + + // ================================================= 2. Compute prediction + + t1 = high_resolution_clock::now(); + predict_rolling_ball_pos_vel_spd(px, py, vx, vy); + t2 = high_resolution_clock::now(); + + cout << std::fixed << std::setprecision(8); + + for(int i=0; i(t2 - t1).count() << "us for prediction\n"; + + // ================================================= 3. Generate data + + float robot_x = -1; + float robot_y = 1; + float max_speed_per_step = 0.7*0.02; + float ret_x, ret_y, ret_d; + + // ================================================= 4. Compute intersection + + t1 = high_resolution_clock::now(); + get_intersection_with_ball(robot_x, robot_y, max_speed_per_step, ball_pos_pred, pos_pred_len, ret_x, ret_y, ret_d); + t2 = high_resolution_clock::now(); + + cout << duration_cast(t2 - t1).count() << "us for intersection\n\n"; + cout << "Intersection: " << ret_x << "," << ret_y << " dist: " << ret_d << "\n\n"; + +} diff --git a/cpp/ball_predictor/lib_main.cpp b/cpp/ball_predictor/lib_main.cpp new file mode 100644 index 0000000..9d6be32 --- /dev/null +++ b/cpp/ball_predictor/lib_main.cpp @@ -0,0 +1,100 @@ +#include "ball_predictor.h" +#include +#include + +namespace py = pybind11; +using namespace std; + + +/** + * @brief Predict rolling ball position, velocity, linear speed + * + * @param parameters + * ball_x, ball_y, ball_vel_x, ball_vel_y + * @return ball_pos_pred, ball_vel_pred, ball_spd_pred + */ +py::array_t predict_rolling_ball( py::array_t parameters ){ + + // ================================================= 1. Parse data + + py::buffer_info parameters_buf = parameters.request(); + float* parameters_ptr = (float*)parameters_buf.ptr; + + float px = parameters_ptr[0]; + float py = parameters_ptr[1]; + float vx = parameters_ptr[2]; + float vy = parameters_ptr[3]; + + // ================================================= 2. Compute path + + predict_rolling_ball_pos_vel_spd(px, py, vx, vy); + + // ================================================= 3. Prepare data to return + + py::array_t retval = py::array_t(pos_pred_len+pos_pred_len+pos_pred_len/2); //allocate + py::buffer_info buff = retval.request(); + float *ptr = (float *) buff.ptr; + + for(int i=0; i get_intersection( py::array_t parameters ){ + + // ================================================= 1. Parse data + + py::buffer_info parameters_buf = parameters.request(); + float* parameters_ptr = (float*)parameters_buf.ptr; + int params_len = parameters_buf.shape[0]; + + float x = parameters_ptr[0]; + float y = parameters_ptr[1]; + float max_sp = parameters_ptr[2]; + float* ball_pos = parameters_ptr + 3; + float ret_x, ret_y, ret_d; + + // ================================================= 2. Compute path + + get_intersection_with_ball(x, y, max_sp, ball_pos, params_len-3, ret_x, ret_y, ret_d); + + // ================================================= 3. Prepare data to return + + py::array_t retval = py::array_t(3); //allocate + py::buffer_info buff = retval.request(); + float *ptr = (float *) buff.ptr; + + ptr[0] = ret_x; + ptr[1] = ret_y; + ptr[2] = ret_d; + return retval; +} + + +using namespace pybind11::literals; // to add informative argument names as -> "argname"_a + +PYBIND11_MODULE(ball_predictor, m) { // the python module name, m is the interface to create bindings + m.doc() = "Ball predictor"; // optional module docstring + + // optional arguments names + m.def("predict_rolling_ball", &predict_rolling_ball, "Predict rolling ball", "parameters"_a); + m.def("get_intersection", &get_intersection, "Get point of intersection with moving ball", "parameters"_a); +} + diff --git a/cpp/localization/Field.cpp b/cpp/localization/Field.cpp new file mode 100644 index 0000000..f158d6b --- /dev/null +++ b/cpp/localization/Field.cpp @@ -0,0 +1,532 @@ +#include "Field.h" +#include "RobovizLogger.h" +#include "World.h" + + +static World& world = SWorld::getInstance(); + +//================================================================================================= +//=========================================================================== constexpr definitions +//================================================================================================= + +decltype(Field::cRingLineLength) constexpr Field::cRingLineLength; +decltype(Field::cPenaltyBoxDistX) constexpr Field::cPenaltyBoxDistX; +decltype(Field::cHalfPenaltyWidth) constexpr Field::cHalfPenaltyWidth; +decltype(Field::cHalfGoalWidth) constexpr Field::cHalfGoalWidth; +decltype(Field::cHalfFielfLength) constexpr Field::cHalfFielfLength; +decltype(Field::cGoalWidth) constexpr Field::cGoalWidth; +decltype(Field::cGoalDepth) constexpr Field::cGoalDepth; +decltype(Field::cGoalHeight) constexpr Field::cGoalHeight; +decltype(Field::cFieldLength) constexpr Field::cFieldLength; +decltype(Field::cFieldWidth) constexpr Field::cFieldWidth; +decltype(Field::cPenaltyLength) constexpr Field::cPenaltyLength; +decltype(Field::cPenaltyWidth) constexpr Field::cPenaltyWidth; +decltype(Field::cFieldLineSegments::list) constexpr Field::cFieldLineSegments::list; +decltype(Field::cFieldPoints::list) constexpr Field::cFieldPoints::list; + +//non-constexpr definitions +decltype(Field::list_8_landmarks::list) Field::list_8_landmarks::list; + +//================================================================================================= +//=============================================================================== Drawing utilities +//================================================================================================= + + +/** + * Draw estimates of all visible lines, markers, self position and ball + * */ + +void Field::draw_visible(const Matrix4D& headToFieldT, bool is_right_side) const{ + + if(is_right_side){ + return draw_visible_switch(headToFieldT); + } + + string draw_name = "localization"; + + RobovizLogger* roboviz = RobovizLogger::Instance(); + roboviz->init(); // only initialized when draw_visible is called (only happens once) + + //--------------------------------- Print all lines, whether they were identified or not + for(const Line6f& l : list_segments) { + + Vector3f s = headToFieldT * l.startc; + Vector3f e = headToFieldT * l.endc; + + roboviz->drawLine(s.x, s.y, s.z, e.x, e.y, e.z, 1, 0.8,0,0, &draw_name); + } + + //--------------------------------- Print identified line segments, with their fixed abs coordinates + + for(const auto& s : list_known_segments){ + + Vector3f mid = Vector3f::determineMidpoint(s.point[0].absPos.get_vector(), s.point[1].absPos.get_vector()); + + string line_name(s.fieldSegment->name); + roboviz->drawAnnotation(&line_name,mid.x,mid.y,mid.z, 0,1,0,&draw_name); + roboviz->drawLine(s.point[0].absPos.x, s.point[0].absPos.y, s.point[0].absPos.z, + s.point[1].absPos.x, s.point[1].absPos.y, s.point[1].absPos.z, 3, 0,0.8,0, &draw_name); + } + + for(const auto& m : list_known_markers){ + string line_name(m.fieldPt->name); + roboviz->drawAnnotation(&line_name,m.absPos.x, m.absPos.y, m.absPos.z+1, 1,0,0,&draw_name); + roboviz->drawLine(m.absPos.x, m.absPos.y, m.absPos.z, + m.absPos.x, m.absPos.y, m.absPos.z+0.5, 1, 0.8,0.8,0.8, &draw_name); + } + for(const auto& m : list_unknown_markers){ + string line_name = "!"; + roboviz->drawAnnotation(&line_name,m.absPos.x, m.absPos.y, m.absPos.z+1, 1,0,0,&draw_name); + roboviz->drawLine(m.absPos.x, m.absPos.y, m.absPos.z, + m.absPos.x, m.absPos.y, m.absPos.z+0.5, 1, 0.8,0.8,0.8, &draw_name); + } + + //--------------------------------- Draw player and ball arrows + + Vector3f me = headToFieldT.toVector3f(); + + roboviz->drawLine(me.x, me.y, me.z, me.x, me.y, me.z+0.5, 2,1,0,0,&draw_name); + roboviz->drawLine(me.x, me.y, me.z, me.x-0.2, me.y, me.z+0.2, 2,1,0,0,&draw_name); + roboviz->drawLine(me.x, me.y, me.z, me.x+0.2, me.y, me.z+0.2, 2,1,0,0,&draw_name); + + //There is no need to draw the ball position here (but it works well) + + /*static Vector3f last_known_ball_pos = Vector3f(); + if(world.ball_seen){ + last_known_ball_pos = headToFieldT * world.ball_rel_pos_cart; + } + + Vector3f &b = last_known_ball_pos; + + roboviz->drawLine(b.x, b.y, b.z, b.x, b.y, b.z+0.5, 2,1,1,0,&draw_name); + roboviz->drawLine(b.x, b.y, b.z, b.x-0.2, b.y, b.z+0.2, 2,1,1,0,&draw_name); + roboviz->drawLine(b.x, b.y, b.z, b.x+0.2, b.y, b.z+0.2, 2,1,1,0,&draw_name);*/ + + roboviz->swapBuffers(&draw_name); + + +} + +/** + * Draw estimates of all visible lines, markers, self position and ball, but switch field sides + * */ + +void Field::draw_visible_switch(const Matrix4D& headToFieldT) const{ + + string draw_name = "localization"; + + RobovizLogger* roboviz = RobovizLogger::Instance(); + roboviz->init(); // only initialized when draw_visible is called (only happens once) + + //--------------------------------- Print all lines, whether they were identified or not + for(const Line6f& l : list_segments) { + + Vector3f s = headToFieldT * l.startc; + Vector3f e = headToFieldT * l.endc; + + roboviz->drawLine(-s.x, -s.y, s.z, -e.x, -e.y, e.z, 1, 0.8,0,0, &draw_name); + } + + //--------------------------------- Print identified line segments, with their fixed abs coordinates + + for(const auto& s : list_known_segments){ + + Vector3f mid = Vector3f::determineMidpoint(s.point[0].absPos.get_vector(), s.point[1].absPos.get_vector()); + + string line_name(s.fieldSegment->name); + roboviz->drawAnnotation(&line_name,-mid.x,-mid.y,mid.z, 0,1,0,&draw_name); + roboviz->drawLine(-s.point[0].absPos.x, -s.point[0].absPos.y, s.point[0].absPos.z, + -s.point[1].absPos.x, -s.point[1].absPos.y, s.point[1].absPos.z, 3, 0,0.8,0, &draw_name); + } + + for(const auto& m : list_known_markers){ + string line_name(m.fieldPt->name); + roboviz->drawAnnotation(&line_name,-m.absPos.x, -m.absPos.y, m.absPos.z+1, 1,0,0,&draw_name); + roboviz->drawLine(-m.absPos.x, -m.absPos.y, m.absPos.z, + -m.absPos.x, -m.absPos.y, m.absPos.z+0.5, 1, 0.8,0.8,0.8, &draw_name); + } + for(const auto& m : list_unknown_markers){ + string line_name = "!"; + roboviz->drawAnnotation(&line_name,-m.absPos.x, -m.absPos.y, m.absPos.z+1, 1,0,0,&draw_name); + roboviz->drawLine(-m.absPos.x, -m.absPos.y, m.absPos.z, + -m.absPos.x, -m.absPos.y, m.absPos.z+0.5, 1, 0.8,0.8,0.8, &draw_name); + } + + //--------------------------------- Draw player and ball arrows + + Vector3f me = headToFieldT.toVector3f(); + + roboviz->drawLine(-me.x, -me.y, me.z, -me.x, -me.y, me.z+0.5, 2,1,0,0,&draw_name); + roboviz->drawLine(-me.x, -me.y, me.z, -me.x+0.2, -me.y, me.z+0.2, 2,1,0,0,&draw_name); + roboviz->drawLine(-me.x, -me.y, me.z, -me.x-0.2, -me.y, me.z+0.2, 2,1,0,0,&draw_name); + + //There is no need to draw the ball position here (but it works well) + + /*static Vector3f last_known_ball_pos = Vector3f(); + if(world.ball_seen){ + last_known_ball_pos = headToFieldT * world.ball_rel_pos_cart; + } + + Vector3f &b = last_known_ball_pos; + + roboviz->drawLine(b.x, b.y, b.z, b.x, b.y, b.z+0.5, 2,1,1,0,&draw_name); + roboviz->drawLine(b.x, b.y, b.z, b.x-0.2, b.y, b.z+0.2, 2,1,1,0,&draw_name); + roboviz->drawLine(b.x, b.y, b.z, b.x+0.2, b.y, b.z+0.2, 2,1,1,0,&draw_name);*/ + + roboviz->swapBuffers(&draw_name); + + +} + + +//================================================================================================= +//==================================================================== Refresh / Identify / Collect +//================================================================================================= + + +/** + * Gather markers with known absolute z: line endpoints + foot contact points + [toe contact points] + **/ +void Field::gather_ground_markers(){ + + /** + * Add NAO's feet ground contact points to zmarks + * Dependency: the agent's feet must be touching the ground + * Flaws: + * - if the feet are touching other players or the ball (may be solved if it is problematic) + * - robot 4 may be touching the ground with its toes and they are currently ignored + **/ + + for(int i=0; i<2; i++){ + if( world.foot_touch[i] ){ //if this foot is touching the ground + + //Vector3f contactAux = world.foot_contact_pt[i]; //contact point using strange coordinate system + //Vector3f contactpt = Vector3f(contactAux.y,-contactAux.x,contactAux.z); // fix coordinate system for both feet + + Vector3f relPos = world.foot_contact_rel_pos[i]; + list_feet_contact_points.emplace_back( sVector3d({0,0,0}), relPos.toPolar(), relPos); + list_ground_markers.emplace_back( sVector3d({0,0,0}), relPos.toPolar(), relPos); + } + } + + //Deactivated since it did not produce better results, even when both feet are floating (it was only better when the robot fell) + /*const Types::BodyParts toePart[2] = {Types::ilLToe, Types::ilRToe}; + for(int i=0; i<2; i++){ + if( agent::model->getToeTouch(feetSide[i]) ){ //if this foot is touching the ground + + Vector3f contactAux = agent::model->getToeContact(feetSide[i]); //contact point using strange coordinate system + Vector3f contactpt = Vector3f(contactAux.y,-contactAux.x,contactAux.z); // fix coordinate system for both feet + Vector3f relPos = agent::model->getRelPositionOfBodyPoint(agent::model->getBodyPart(toePart[i]),contactpt); + + if(agent::model->getFootTouch(Types::iLeft) == false && agent::model->getFootTouch(Types::iRight) == false){ + zmarks.emplace_back( 0,0,0,relPos ); + } + } + }*/ + + + //Add all line endings to ground markers + for(const Line6f& l : list_segments){ + list_ground_markers.emplace_back( sVector3d({0,0,0}), l.startp, l.startc); + list_ground_markers.emplace_back( sVector3d({0,0,0}), l.endp, l.endc); + } + + non_collinear_ground_markers = list_ground_markers.size(); //Excludes corner flags + + //Add corner flags + for(const auto& c : list_landmarks_corners){ + list_ground_markers.emplace_back( sVector3d({0,0,0}), c.relPosPolar, c.relPosCart); + } + + + /** + * All the polar coordinates' errors are dependent on the distance + * Var[distance error] = Var[ed*d/100] + Var[er] (ed-error distance, er-error rounding) + * Var[distance error] = (d/100)^2 * Var[ed] + Var[er] + * Their importance will be given by Inverse-variance weighting + * + * Application: + * repetition = max(int(k*(1/var)),1), where k=1/1500 + * repetitions for 1 meter: 71 + * repetitions for 2 meters: 55 + * repetitions for >=19 meters: 1 + */ + + for(const auto& g : list_ground_markers){ + float var = pow(g.relPosPolar.x / 100.f,2) * var_distance + var_round_hundredth; + float w = 1.f/(1500.f*var); //weight = (1/var)*k where k is a constant to transform the large weights into number of repetitions + int repetitions = max(int(w),1); + + list_weighted_ground_markers.insert(list_weighted_ground_markers.end(), repetitions, g); + } +} + + +/** + * Update markers after visual step + * Marks attributes: abs(x,y,z), rel(r,h,v) + * Possible markers: + * - 8 landmarks + * - line endings: + * - 8 @ field corners + * - 12 @ penalty box corners + * - 20 @ center ring + * - 2 @ halfway line + * - 8 noisy estimates from corner-originated lines + * */ +void Field::update(){ + + //no need to reserve space since these vectors will expand mostly in the first cycles + list_segments.clear(); + list_landmarks.clear(); + list_landmarks_corners.clear(); + list_landmarks_goalposts.clear(); + list_feet_contact_points.clear(); + list_known_markers.clear(); + list_unknown_markers.clear(); + list_known_segments.clear(); + list_ground_markers.clear(); + list_weighted_ground_markers.clear(); + + //----------------------------------------- Pre-processing: prepare landmark lists + + for(int i=0; i<8; i++){ + sFixedMarker *l8; + const sFieldPoint *fp; + World::sLMark *l = &world.landmark[i]; + if (l->pos.x == -15 && l->pos.y == -10) {l8 = &list_8_landmarks::_corner_mm; fp = &cFieldPoints::corner_mm;} + else if(l->pos.x == -15 && l->pos.y == +10) {l8 = &list_8_landmarks::_corner_mp; fp = &cFieldPoints::corner_mp;} + else if(l->pos.x == +15 && l->pos.y == -10) {l8 = &list_8_landmarks::_corner_pm; fp = &cFieldPoints::corner_pm;} + else if(l->pos.x == +15 && l->pos.y == +10) {l8 = &list_8_landmarks::_corner_pp; fp = &cFieldPoints::corner_pp;} + else if(l->pos.x == -15 && l->pos.y < 0) {l8 = &list_8_landmarks::_goal_mm; fp = &cFieldPoints::goal_mm; } + else if(l->pos.x == -15 && l->pos.y > 0) {l8 = &list_8_landmarks::_goal_mp; fp = &cFieldPoints::goal_mp; } + else if(l->pos.x == +15 && l->pos.y < 0) {l8 = &list_8_landmarks::_goal_pm; fp = &cFieldPoints::goal_pm; } + else if(l->pos.x == +15 && l->pos.y > 0) {l8 = &list_8_landmarks::_goal_pp; fp = &cFieldPoints::goal_pp; } + else{ return; } + + if(l->seen){ + l8->set_relPos(l->rel_pos); + l8->visible = true; + + sMarker seen_mark(fp, l->rel_pos); + list_landmarks.push_back(seen_mark); + list_known_markers.push_back(seen_mark); + + if (l->isCorner){ list_landmarks_corners.push_back( seen_mark); } + else { list_landmarks_goalposts.push_back(seen_mark); } + }else{ + l8->visible = false; + } + } + + //----------------------------------------- Pre-processing: prepare lines and landmarks' coordinates sign + + for(const auto& l : world.lines_polar) { + list_segments.emplace_back(l.start, l.end); + } + + //----------------------------------------- Gather markers with known absolute z: line endpoints + foot contact points + + gather_ground_markers(); + +} + + + + + + + + +void Field::update_from_transformation(const Matrix4D& tmatrix){ + + /** + * Identify segments based on transformation matrix + * + * Identification starts from longest to shortest line + * The visible line segment is identified if there is only 1 close field line + * If there is more than 1 close field line and all but 1 were already taken, it is still identified + */ + + //----------------------------------------------- get lines ordered from largest to shortest + vector lines_descending_length; + for(auto& l : list_segments){ + lines_descending_length.push_back(&l); + } + + //Sort from largest to smallest radius + sort(lines_descending_length.begin(),lines_descending_length.end(), + [](const Line6f* a, const Line6f* b) { return (a->length > b->length); }); + + //----------------------------------------------- identify lines + + for(const Line6f* l : lines_descending_length){ + Vector3f l_abs[2] = {tmatrix * l->startc, tmatrix * l->endc}; + + float l_angle = atan2f(l_abs[1].y - l_abs[0].y, l_abs[1].x - l_abs[0].x); + + const float min_err = 0.3; //maximum allowed distance (startdist + enddist < 0.3m) + const sFieldSegment* best_line = nullptr; + for(const auto& s : cFieldLineSegments::list){ //find distance to closest field line + + //Skip field line if seen line is substantially larger + if( l->length > (s.length + 0.7) ){ continue; } + + //Skip field line if orientation does not match + float line_angle_difference = normalize_line_angle_rad(l_angle - s.angle); + if(line_angle_difference > 0.26) continue; //tolerance 15deg + + //Skip field line if it was already identified + bool already_identified = false; + for(const auto& k : list_known_segments){ + if(k.fieldSegment == &s){ already_identified=true; break; } + } + if(already_identified) continue; + + //Error is the sum of the distance of a single line segment to both endpoints of seen line + float err = fieldLineSegmentDistToCart2DPoint(s,l_abs[0].to2d()); + if(err < min_err) err += fieldLineSegmentDistToCart2DPoint(s,l_abs[1].to2d()); + + if(err < min_err){ + if(best_line == nullptr){ best_line = &s; } //Save the field line for now (others may emerge) + else{ + best_line = nullptr; //Two close field lines, none of which was taken yet, so abort + break; + } + } + } + + if(best_line != nullptr){ + + //-------------- Fix the seen line's start<->end order to match the corresponding field line + + int l_index[2] = {0,1}; //line index of [0]start and [1]end points + + if(normalize_vector_angle_rad(l_angle - best_line->angle) > 1.57079633f){ // they point in opposite directions + l_index[0] = 1; + l_index[1] = 0; + } + + const Vector3f *lAbs[2] = {&l_abs[l_index[0]], &l_abs[l_index[1]]}; + const Vector3f *lRelP[2] = {&l->get_polar_pt(l_index[0]), &l->get_polar_pt(l_index[1])}; + const Vector3f *lRelC[2] = {&l->get_cart_pt(l_index[0]), &l->get_cart_pt(l_index[1])}; + + //-------------- Fix the absolute coordinates with field information + + bool isInFoV[2] = {false,false}; + + //1st: recognize endpoints as field points (& save known markers) + + /** + * //---------------------------------------------- General solution + * All points reasonably within the FoV are identified + * Noise applied horizontally sigma=0.1225, Pr[-0.5 0.2 + * + * Warning 2: sometimes the error of phi and theta is larger than expected, producing points like + * (rel polar: 0.57,-36.36,-54.41) (rel cart: 0.267,-0.1967,-0.4635) + * which goes with the theory that the FoV is actually defined as a cone (a bit unrealistic though) + * current solution: cone_angle < hor_FoV-5 + */ + + for( int i=0; i<2; i++){ + float cone_angle = acosf(lRelC[i]->x / lRelP[i]->x); //angle between vector and (1,0,0) + const float max_cone_angle = (cHalfHorizontalFoV-5)*M_PI/180; + + if(cone_angle < max_cone_angle && lRelC[i]->x > 0.2){ + list_known_markers.emplace_back(best_line->point[i], *lRelP[i], *lRelC[i]); + isInFoV[i] = true; + } + } + + //2nd: use real coordinates if point was recognized, otherwise push it to a valid position (& save segment and unknown markers) + + const Line6f field_line(best_line->point[0]->get_vector(), best_line->point[1]->get_vector(), best_line->length); + + sVector3d l_pt_d[2]; //final line segment points (double precision floating-points) + + for( int i=0; i<2; i++){ + if(isInFoV[i]){ + l_pt_d[i] = best_line->point[i]->pt; //set recognized point's abs coordinates + }else{ + Vector3f p = field_line.segmentPointClosestToCartPoint(*lAbs[i]); //push point to closest valid position + l_pt_d[i].set(p); //set unknown point's estimated coordinates + list_unknown_markers.emplace_back(best_line, l_pt_d[i], *lRelP[i], *lRelC[i]); + } + } + + //-------------- Save identified line segment + list_known_segments.emplace_back(sMarker(l_pt_d[0],*lRelP[0],*lRelC[0]),sMarker(l_pt_d[1],*lRelP[1],*lRelC[1]), l->length, best_line); + + } + } +} + +void Field::update_unknown_markers(const Matrix4D& tmatrix){ + + for(auto& u : list_unknown_markers){ + + //Transform marker to world frame + Vector3f raw_abs_pos = tmatrix * u.relPosCart; + + //Push marker to existing field segment + const Line6f field_seg( u.fieldSeg->point[0]->get_vector(), u.fieldSeg->point[1]->get_vector(), u.fieldSeg->length); + Vector3f fixed_abs_pos = field_seg.segmentPointClosestToCartPoint(raw_abs_pos); //push point to closest valid position + + u.absPos = sVector3d({fixed_abs_pos.x, fixed_abs_pos.y, fixed_abs_pos.z}); + } +} + + +//================================================================================================= +//================================================================================== Math utilities +//================================================================================================= + + +/** + * Field lines are on the ground (z=0), so the method is simplified + */ +float Field::fieldLineSegmentDistToCartPoint(const sFieldSegment& fLine, const Vector3f& cp){ + + //Line segment vector (start -> end) + float vx = fLine.point[1]->pt.x - fLine.point[0]->pt.x; + float vy = fLine.point[1]->pt.y - fLine.point[0]->pt.y; + + Vector3f w1(cp.x - fLine.point[0]->pt.x, cp.y - fLine.point[0]->pt.y, cp.z); // vector: (segment start -> point) + if (w1.x * vx + w1.y * vy <= 0) + return w1.length();// if angle between vectors is >=90deg, we return the distance to segment start + + Vector3f w2(cp.x - fLine.point[1]->pt.x, cp.y - fLine.point[1]->pt.y, cp.z); // vector: (segment end -> point) + if (w2.x * vx + w2.y * vy >= 0) + return w2.length(); //if angle between vectors is <=90deg, we return the distance to segment end + + Vector3f v_cross_w1(vy * w1.z, - vx * w1.z, vx * w1.y - vy * w1.x); + return v_cross_w1.length() / fLine.length; //distance line to point (area of parallelogram divided by base gives height) + +} + +/** + * Field lines are on the ground (z=0), so the method is simplified + */ +float Field::fieldLineSegmentDistToCart2DPoint(const sFieldSegment& fLine, const Vector& cp){ + + const Vector segment_start(fLine.point[0]->pt.x, fLine.point[0]->pt.y); + const Vector segment_end( fLine.point[1]->pt.x, fLine.point[1]->pt.y ); + + //Line segment vector (start -> end) + Vector v(segment_end-segment_start); + + Vector w1(cp - segment_start);// vector: (segment start -> point) + + if(w1.innerProduct(v) <= 0) + return w1.length();// if angle between vectors is >=90deg, we return the distance to segment start + + Vector w2(cp - segment_end); // vector: (segment end -> point) + if(w2.innerProduct(v) >= 0) + return w2.length(); //if angle between vectors is <=90deg, we return the distance to segment end + + return fabsf(v.crossProduct(w1)) / fLine.length; //distance line to point (area of parallelogram divided by base gives height) +} \ No newline at end of file diff --git a/cpp/localization/Field.h b/cpp/localization/Field.h new file mode 100644 index 0000000..9d75f8b --- /dev/null +++ b/cpp/localization/Field.h @@ -0,0 +1,503 @@ +/** + * FILENAME: Field + * DESCRIPTION: Field map + * AUTHOR: Miguel Abreu (m.abreu@fe.up.pt) + * DATE: 2021 + */ + +#pragma once +#include "Vector3f.h" +#include "Singleton.h" +#include "Matrix4D.h" +#include "Line6f.h" +#include +#include + +using namespace std; + + +class Field { + friend class Singleton; + +private: + + Field(){}; + void gather_ground_markers(); + +public: + +//================================================================================================= +//====================================================================================== Structures +//================================================================================================= + + struct sVector3d { + double x,y,z; + + //sVector3d(const Vector3f& v) : x(v.x), y(v.y), z(v.z) {} + + Vector3f get_vector() const { + return Vector3f(x,y,z); + } + + void set(const sVector3d &pt){ + x=pt.x; y=pt.y; z=pt.z; + } + + void set(const Vector3f &pt){ + x=pt.x; y=pt.y; z=pt.z; + } + + float dist(const Vector3f &other) const{ + float dx = x-other.x; + float dy = y-other.y; + float dz = z-other.z; + return sqrtf(dx*dx+dy*dy+dz*dz); + } + + }; + + struct sFieldPoint { + const sVector3d pt; + const char name[10]; + + Vector3f get_vector() const { + return Vector3f(pt.x,pt.y,pt.z); + } + }; + + struct sFieldSegment { + const sFieldPoint * const point[2]; + const double length; + const double angle; + const char name[8]; + }; + + + struct sMarker { + /** + * Estimated absolute position based on the transformation matrix and field knowledge + */ + sVector3d absPos; + + /** + * Pointer to corresponding field point (if reasonably inside the FoV) + * The coordinates are the same as "absPos" but it provides other features: + * - Name of the field point + * - Knowledge that this marker corresponds to a field point (nullptr otherwise) + * - The address of the fieldPt may be compared with field segment endpoints + */ + const sFieldPoint *fieldPt = nullptr; + + /** + * Pointer to corresponding field segment + * This variable is currently set only for unknown markers + * (i.e. those which are known to belong to a field line, but whose field point is unknown) + */ + const sFieldSegment *fieldSeg = nullptr; + + Vector3f relPosPolar; + Vector3f relPosCart; + + /** + * Default constructor + */ + sMarker() : absPos({0,0,0}), relPosPolar(Vector3f()), relPosCart(Vector3f()) {}; + + /** + * Constructor with absolute position and relative polar coordinates (the cartesian version is computed) + */ + sMarker(const sVector3d& absPos_, const Vector3f& relPosPolar_) + : absPos(absPos_), relPosPolar(relPosPolar_), relPosCart(relPosPolar_.toCartesian()) {}; + + /** + * Constructor with field point and relative polar coordinates (the cartesian version is computed) + */ + sMarker(const sFieldPoint* fieldPt_, const Vector3f& relPosPolar_) + : absPos(fieldPt_->pt), fieldPt(fieldPt_), relPosPolar(relPosPolar_), relPosCart(relPosPolar_.toCartesian()) {}; + + /** + * Constructor with float absolute position and relative polar coordinates (the cartesian version is computed) + */ + sMarker(const Vector3f& absPos_, const Vector3f& relPosPolar_) + : absPos(sVector3d({absPos_.x,absPos_.y,absPos_.z})), relPosPolar(relPosPolar_), relPosCart(relPosPolar_.toCartesian()) {}; + + /** + * Constructor with absolute position, relative polar & cartesian coordinates + */ + sMarker(const sVector3d& absPos_, const Vector3f& relPosPolar_, const Vector3f& relPosCart_) + : absPos(absPos_), relPosPolar(relPosPolar_), relPosCart(relPosCart_) {}; + + /** + * Constructor with field segment, absolute position, relative polar & cartesian coordinates (e.g. unknown marker) + */ + sMarker(const sFieldSegment* fieldSeg_, const sVector3d& absPos_, const Vector3f& relPosPolar_, const Vector3f& relPosCart_) + : fieldSeg(fieldSeg_), absPos(absPos_), relPosPolar(relPosPolar_), relPosCart(relPosCart_) {}; + + /** + * Constructor with field point, relative polar & cartesian coordinates + */ + sMarker(const sFieldPoint* fieldPt_, const Vector3f& relPosPolar_, const Vector3f& relPosCart_) + : absPos(fieldPt_->pt), fieldPt(fieldPt_), relPosPolar(relPosPolar_), relPosCart(relPosCart_) {}; + + + }; + + struct sSegment { + + /** + * Order of start and end is the same as the corresponding fieldSegment + * [0]-start, [1]-end + */ + sMarker point[2]; + + float length; //visible segment length + const sFieldSegment* fieldSegment; //Corresponding field segment if we had full visibility + + /** + * Constructor + */ + sSegment(const sMarker& start, const sMarker& end, float length_, const sFieldSegment* fieldSegment_) + : point{start,end}, length(length_), fieldSegment(fieldSegment_) {}; + + }; + + struct sFixedMarker { + bool visible; + Vector3f relPosPolar; + Vector3f relPosCart; + + /** + * Default constructor + */ + sFixedMarker() : relPosPolar(Vector3f()), relPosCart(Vector3f()), visible(false) {}; + + void set_relPos(Vector3f relPosPolar_){ + relPosPolar = relPosPolar_; + relPosCart = relPosPolar_.toCartesian(); + } + + }; + +//================================================================================================= +//================================================================================= Field constants +//================================================================================================= + + /** + * Constant field dimensions + */ + static constexpr double cFieldLength = 30.0, cFieldWidth = 20.0, cPenaltyLength = 1.8, cPenaltyWidth = 6.0; + static constexpr double cGoalWidth = 2.1, cGoalDepth = 0.6, cGoalHeight = 0.8; + + static constexpr double cHalfFielfLength = cFieldLength/2.0, cHalfFieldWidth = cFieldWidth/2.0; + static constexpr double cHalfGoalWidth = cGoalWidth/2.0, cHalfPenaltyLength = cPenaltyLength/2.0; + static constexpr double cHalfPenaltyWidth = cPenaltyWidth/2.0; + + static constexpr double cPenaltyBoxDistX = cHalfFielfLength-cPenaltyLength; + static constexpr double cRingLineLength = 1.2360679774997897; + + static constexpr float cHalfHorizontalFoV = 60; + static constexpr float cHalfVerticalFoV = 60; + + static constexpr float stdev_distance = 0.0965; //st. deviation of error ed (distance error=d/100*ed) + static constexpr float var_distance = 0.00931225; // variance of error ed (distance error=d/100*ed) + static constexpr float var_round_hundredth = 0.01*0.01/12; //variance of uniformly distributed random variable [-0.005,0.005] + + + class cFieldPoints{ + public: + /** + * Constant list of field points + * Notation + * "PT1-.-PT2" midpoint between PT1 and PT2 (2D/3D) + * "PT1-PT2" point between PT1 and PT2 (in 2D only) + */ + static constexpr std::array list {{ + {-cHalfFielfLength,-cHalfGoalWidth, cGoalHeight, "post--"}, {-cHalfFielfLength, cHalfGoalWidth, cGoalHeight, "post-+"}, //Goalposts x<0 + { cHalfFielfLength,-cHalfGoalWidth, cGoalHeight, "post+-"}, { cHalfFielfLength, cHalfGoalWidth, cGoalHeight, "post++"}, //Goalposts x>0 + {-cHalfFielfLength,-cHalfFieldWidth,0, "corner--"}, {-cHalfFielfLength, cHalfFieldWidth,0, "corner-+"}, //Corners x<0 + { cHalfFielfLength,-cHalfFieldWidth,0, "corner+-"}, { cHalfFielfLength, cHalfFieldWidth,0, "corner++"}, //Corners x>0 + {0,-cHalfFieldWidth, 0, "halfway-"}, // Halfway line ending y<0 + {0, cHalfFieldWidth, 0, "halfway+"}, // Halfway line ending y>0 + {-cHalfFielfLength, -cHalfPenaltyWidth, 0, "boxBack--"}, {-cHalfFielfLength, cHalfPenaltyWidth, 0, "boxBack-+"}, //Penalty box goal line corner x<0 + { cHalfFielfLength, -cHalfPenaltyWidth, 0, "boxBack+-"}, { cHalfFielfLength, cHalfPenaltyWidth, 0, "boxBack++"}, //Penalty box goal line corner x>0 + {-cPenaltyBoxDistX, -cHalfPenaltyWidth, 0, "boxFrnt--"}, {-cPenaltyBoxDistX, cHalfPenaltyWidth, 0, "boxFrnt-+"}, //Penalty box front corner x<0 + { cPenaltyBoxDistX, -cHalfPenaltyWidth, 0, "boxFrnt+-"}, { cPenaltyBoxDistX, cHalfPenaltyWidth, 0, "boxFrnt++"}, //Penalty box front corner x>0 + {2, 0, 0, "r0"}, { 1.6180339887498948, 1.1755705045849463, 0, "r36" }, //(18,19) Ring 0/36 deg + {0.61803398874989485, 1.9021130325903071, 0, "r72" }, {-0.61803398874989485, 1.9021130325903071, 0, "r108"}, //(20,21) Ring 72/108 deg + {-1.6180339887498948, 1.1755705045849463, 0, "r144"}, {-2, 0, 0, "r180"}, //(22,23) Ring 144/180 deg + {-1.6180339887498948, -1.1755705045849463, 0, "r216"}, {-0.61803398874989485, -1.9021130325903071, 0, "r252"}, //(24,25) Ring 216/252 deg + {0.61803398874989485, -1.9021130325903071, 0, "r288"}, { 1.6180339887498948, -1.1755705045849463, 0, "r324"} //(26,27) Ring 288/324 deg + }}; + + static constexpr const sFieldPoint &goal_mm = list[0]; //Goalpost x<0 y<0 + static constexpr const sFieldPoint &goal_mp = list[1]; //Goalpost x<0 y>0 + static constexpr const sFieldPoint &goal_pm = list[2]; //Goalpost x>0 y<0 + static constexpr const sFieldPoint &goal_pp = list[3]; //Goalpost x>0 y>0 + + static constexpr const sFieldPoint &corner_mm = list[4]; //Corner x<0 y<0 + static constexpr const sFieldPoint &corner_mp = list[5]; //Corner x<0 y>0 + static constexpr const sFieldPoint &corner_pm = list[6]; //Corner x>0 y<0 + static constexpr const sFieldPoint &corner_pp = list[7]; //Corner x>0 y>0 + + static constexpr const sFieldPoint &halfway_m = list[8]; //Halfway line ending y<0 + static constexpr const sFieldPoint &halfway_p = list[9]; //Halfway line ending y>0 + + static constexpr const sFieldPoint &boxgoal_mm = list[10]; //Penalty box goal line corner x<0 y<0 + static constexpr const sFieldPoint &boxgoal_mp = list[11]; //Penalty box goal line corner x<0 y>0 + static constexpr const sFieldPoint &boxgoal_pm = list[12]; //Penalty box goal line corner x>0 y<0 + static constexpr const sFieldPoint &boxgoal_pp = list[13]; //Penalty box goal line corner x>0 y>0 + + static constexpr const sFieldPoint &box_mm = list[14]; //Penalty box front corner x<0 y<0 + static constexpr const sFieldPoint &box_mp = list[15]; //Penalty box front corner x<0 y>0 + static constexpr const sFieldPoint &box_pm = list[16]; //Penalty box front corner x>0 y<0 + static constexpr const sFieldPoint &box_pp = list[17]; //Penalty box front corner x>0 y>0 + + static constexpr const sFieldPoint *rings = &list[18]; //iterator for 10 ring points + + }; + + + + /** + * Constant list of field line segments + * Each line segment has 3 characteristics: {startc, endc, length, angle, print name}, + * The angle is always positive, in [0,180[, and corresponds to the vector defined by (end-start) + * The print name should be used for printing purposes only, + * since the line segment can be identified by its constant index or address + */ + + class cFieldLineSegments{ + public: + static constexpr double c0deg = 0, c36deg = 0.62831853071795865, c72deg = 1.2566370614359173; + static constexpr double c90deg = 1.5707963267948966, c108deg = 1.8849555921538759, c144deg = 2.5132741228718346; + + static constexpr std::array list {{ + {&cFieldPoints::corner_mm, &cFieldPoints::corner_pm, cFieldLength, c0deg , "side-"}, // Sideline y<0 + {&cFieldPoints::corner_mp, &cFieldPoints::corner_pp, cFieldLength, c0deg , "side+"}, // Sideline y>0 + {&cFieldPoints::corner_mm, &cFieldPoints::corner_mp, cFieldWidth, c90deg , "goal-"}, // Goal line x<0 + {&cFieldPoints::corner_pm, &cFieldPoints::corner_pp, cFieldWidth, c90deg , "goal+"}, // Goal line x>0 + {&cFieldPoints::halfway_m, &cFieldPoints::halfway_p, cFieldWidth, c90deg , "halfway"},// Halfway line + {&cFieldPoints::boxgoal_mm, &cFieldPoints::box_mm, cPenaltyLength, c0deg , "box--"}, // Penalty box sideline x<0 y<0 + {&cFieldPoints::boxgoal_mp, &cFieldPoints::box_mp, cPenaltyLength, c0deg , "box-+"}, // Penalty box sideline x<0 y>0 + {&cFieldPoints::box_pm, &cFieldPoints::boxgoal_pm, cPenaltyLength, c0deg , "box+-"}, // Penalty box sideline x>0 y<0 + {&cFieldPoints::box_pp, &cFieldPoints::boxgoal_pp, cPenaltyLength, c0deg , "box++"}, // Penalty box sideline x>0 y>0 + {&cFieldPoints::box_mm, &cFieldPoints::box_mp, cPenaltyWidth, c90deg , "box-"}, // Penalty box front line x<0 + {&cFieldPoints::box_pm, &cFieldPoints::box_pp, cPenaltyWidth, c90deg , "box+"}, // Penalty box front line x>0 + {&cFieldPoints::rings[0], &cFieldPoints::rings[1], cRingLineLength, c108deg, "rL0"}, // Ring line 0 -> 36 + {&cFieldPoints::rings[1], &cFieldPoints::rings[2], cRingLineLength, c144deg, "rL1"}, // Ring line 36 -> 72 + {&cFieldPoints::rings[3], &cFieldPoints::rings[2], cRingLineLength, c0deg , "rL2"}, // Ring line 72 <- 108 + {&cFieldPoints::rings[4], &cFieldPoints::rings[3], cRingLineLength, c36deg , "rL3"}, // Ring line 108 <- 144 + {&cFieldPoints::rings[5], &cFieldPoints::rings[4], cRingLineLength, c72deg , "rL4"}, // Ring line 144 <- 180 + {&cFieldPoints::rings[6], &cFieldPoints::rings[5], cRingLineLength, c108deg, "rL5"}, // Ring line 180 <- 216 + {&cFieldPoints::rings[7], &cFieldPoints::rings[6], cRingLineLength, c144deg, "rL6"}, // Ring line 216 <- 252 + {&cFieldPoints::rings[7], &cFieldPoints::rings[8], cRingLineLength, c0deg , "rL7"}, // Ring line 252 -> 288 + {&cFieldPoints::rings[8], &cFieldPoints::rings[9], cRingLineLength, c36deg , "rL8"}, // Ring line 288 -> 324 + {&cFieldPoints::rings[9], &cFieldPoints::rings[0], cRingLineLength, c72deg , "rL9"} // Ring line 324 -> 0 + }}; + + static constexpr const sFieldSegment &side_m = list[0]; // Sideline y<0 + static constexpr const sFieldSegment &side_p = list[1]; // Sideline y>0 + static constexpr const sFieldSegment &goal_m = list[2]; // Goal line x<0 + static constexpr const sFieldSegment &goal_p = list[3]; // Goal line x>0 + + static constexpr const sFieldSegment &halfway = list[4]; //Halfway line + + static constexpr const sFieldSegment &box_mm = list[5]; // Penalty box sideline x<0 y<0 + static constexpr const sFieldSegment &box_mp = list[6]; // Penalty box sideline x<0 y>0 + static constexpr const sFieldSegment &box_pm = list[7]; // Penalty box sideline x>0 y<0 + static constexpr const sFieldSegment &box_pp = list[8]; // Penalty box sideline x>0 y>0 + + static constexpr const sFieldSegment &box_m = list[9]; // Penalty box front line x<0 + static constexpr const sFieldSegment &box_p = list[10]; // Penalty box front line x>0 + + static constexpr const sFieldSegment *rings = &list[11]; //iterator for 10 ring lines + + }; + + + + sSegment* get_known_segment(const sFieldSegment &id){ + for( auto& s : list_known_segments){ + if(s.fieldSegment == &id) return &s; + } + return nullptr; + } + +//================================================================================================= +//================================================================================= Control methods +//================================================================================================= + + /** + * Update markers, based on existing landmarks and lines + */ + void update(); + + /** + * Update markers, based on transformation matrix and existing lines + */ + void update_from_transformation(const Matrix4D& tmatrix); + + /** + * Update the absolute position of unknown markers, based on transformation matrix and existing lines + */ + void update_unknown_markers(const Matrix4D& tmatrix); + + /** + * Draw estimates of all visible lines, markers, self position and ball + */ + void draw_visible(const Matrix4D& headToFieldT, bool is_right_side) const; + + /** + * Draw estimates of all visible lines, markers, self position and ball, but switch field sides + */ + void draw_visible_switch(const Matrix4D& headToFieldT) const; + + +//================================================================================================= +//============================================================================= Visible collections +//================================================================================================= + + + /** + * Visible landmarks: corners + goalposts + */ + vector list_landmarks; + + /** + * Visible corners + */ + vector list_landmarks_corners; + + /** + * Visible goalposts + */ + vector list_landmarks_goalposts; + + /** + * Identified visible line segments + * Their start and endpoints' order is the same as the corresponding field segment to which they point + */ + vector list_known_segments; + + /** + * Identified visible line segment endpoints + landmarks + * Each marker has a reference to the corresponding field point + */ + vector list_known_markers; + + /** + * Endpoints (of identified visible line segments) whose corresponding field point is unknown + * Each marker has a reference to the corresponding field segment + * Note: list_known_markers + list_unknown_markers excludes points from unknown line segments + */ + vector list_unknown_markers; + + /** + * Visible line endpoints + foot contact points + corner flags (the absolute position is always (0,0,0)) + */ + vector list_ground_markers; + + /** + * Number of visible non-collinear (line endpoints + foot contact points) + * Note: collinearity between lines is impossible; between feet<->lines it is possible but unlikely + */ + int non_collinear_ground_markers; + + /** + * Same as list_ground_markers but closer points are repeated more often (proportional to distance) + */ + vector list_weighted_ground_markers; + + /** + * Feet contact points + */ + vector list_feet_contact_points; + + /** + * Visible line segments + */ + vector list_segments; + + /** + * Redundant list of all 8 landmarks' relative cartesian coordinates (to speed up lookups) + * It's different from world.landmarks since it is ordered by position, not by name, and it holds the cartesian relPos + * (this ordering difference is important when the teams switch sides) + */ + + class list_8_landmarks{ + friend class Field; + private: + static sFixedMarker list[8]; + //static std::array list; + static constexpr sFixedMarker &_corner_mm = list[0]; + static constexpr sFixedMarker &_corner_mp = list[1]; + static constexpr sFixedMarker &_corner_pm = list[2]; + static constexpr sFixedMarker &_corner_pp = list[3]; + static constexpr sFixedMarker &_goal_mm = list[4]; + static constexpr sFixedMarker &_goal_mp = list[5]; + static constexpr sFixedMarker &_goal_pm = list[6]; + static constexpr sFixedMarker &_goal_pp = list[7]; + public: + static constexpr const sFixedMarker &corner_mm = list[0]; + static constexpr const sFixedMarker &corner_mp = list[1]; + static constexpr const sFixedMarker &corner_pm = list[2]; + static constexpr const sFixedMarker &corner_pp = list[3]; + static constexpr const sFixedMarker &goal_mm = list[4]; + static constexpr const sFixedMarker &goal_mp = list[5]; + static constexpr const sFixedMarker &goal_pm = list[6]; + static constexpr const sFixedMarker &goal_pp = list[7]; + }; + + + +//================================================================================================= +//================================================================================== Math Utilities +//================================================================================================= + + /** + * Compute 3D distance between field line segment and cartesian point + * Field lines are on the ground (z=0), so the method is simplified + */ + static float fieldLineSegmentDistToCartPoint(const sFieldSegment& fLine, const Vector3f& cp); + + /** + * Compute 2D distance between field line segment and cartesian point + * Field lines are on the ground (z=0), so the method is simplified + */ + static float fieldLineSegmentDistToCart2DPoint(const sFieldSegment& fLine, const Vector& cp); + + /** + * Normalize angle between 2 lines + * @return angle between 0 and 90 deg + */ + static inline float normalize_line_angle_deg(float deg){ + return 90.f-fabsf(fmodf(fabsf(deg), 180.f) - 90.f); + } + + /** + * Normalize angle between 2 lines + * @return angle between 0 and pi/2 rad + */ + static inline float normalize_line_angle_rad(float rad){ + return 1.57079633f-fabsf(fmod(fabsf(rad), 3.14159265f) - 1.57079633f); + } + + /** + * Normalize angle between 2 vectors + * @return angle between 0 and 180 deg + */ + static inline float normalize_vector_angle_deg(float deg){ + return 180.f-fabsf(fmodf(fabsf(deg), 360.f) - 180.f); + } + + /** + * Normalize angle between 2 vectors + * @return angle between 0 and pi rad + */ + static inline float normalize_vector_angle_rad(float rad){ + return 3.14159265f-fabsf(fmod(fabsf(rad), 6.28318531f) - 3.14159265f); + } + +}; + +typedef Singleton SField; \ No newline at end of file diff --git a/cpp/localization/FieldNoise.cpp b/cpp/localization/FieldNoise.cpp new file mode 100644 index 0000000..9fb992f --- /dev/null +++ b/cpp/localization/FieldNoise.cpp @@ -0,0 +1,104 @@ +#include "FieldNoise.h" + +double FieldNoise::log_prob_r(double d, double r){ + double c1 = 100.0 * ((r-0.005)/d - 1); + double c2 = 100.0 * ((r+0.005)/d - 1); + return log_prob_normal_distribution(0, 0.0965, c1, c2); +} + +double FieldNoise::log_prob_h(double h, double phi){ + double c1 = phi - 0.005 - h; + double c2 = phi + 0.005 - h; + return log_prob_normal_distribution(0, 0.1225, c1, c2); +} + +double FieldNoise::log_prob_v(double v, double theta){ + double c1 = theta - 0.005 - v; + double c2 = theta + 0.005 - v; + return log_prob_normal_distribution(0, 0.1480, c1, c2); +} + + + +double FieldNoise::log_prob_normal_distribution(double mean, double std, double interval1, double interval2){ + const double std2 = std * sqrt(2); + double erf1_x = (mean - interval1)/std2; //lowest interval, highest expression + double erf2_x = (mean - interval2)/std2; //highest interval, lowest expression + + /** + * Computing erf(erf_x1) - erf(erf_x2) is the same as erf(erf_x1) + erf(-erf_x2). + * Intuitively, the former seems more natural. + * So, computationally, the former expression is accurate in the following situations: + * - erf_x1 * erf_x2 <= 0 + * - |erf_x1| < 3 _or_ |erf_x2| < 3 ('3' is just a ballpark figure, not really relevant) + * + * Known issues: erf(6.5)-erf(6.0) = 1-1 = 0 (actual result: 2.148e-17) + * Using 128b functions only mitigates the issue, which is quite common actually. + * + * For these cases, erf_aux(x) is used, although it is not precise for |x|<1. + * - erf_aux(x) allows the computation of erf(6.5)-erf(6.0) with 7 digits of precision + * - erf_aux(x) allows the computation of erf(8.5)-erf(8.0) with 3 digits of precision + * - erf(12.5)-erf(12) = 0 (which is not good for probability comparison) + * - erf_aux(x) allows the computation of log(erf(6.5)-erf(6.0)) with 8 digits of precision + * - erf_aux(x) allows the computation of log(erf(8.5)-erf(8.0)) with 5 digits of precision + * - log(erf(12.5)-erf(12)) = -4647 (real: -147) (not accurate but good for comparisons) + * + * The complete algorithm below that uses erf_aux(x) is almost as fast as the one which uses erf() from math.h (+30% runtime) + */ + + const double log05 = log(0.5); + + //If they have different sign or |erf1_x|<1 || |erf2_x|<1 + if( fabs(erf1_x) < 2 || fabs(erf2_x) < 2 || ((erf1_x > 0) ^ (erf2_x > 0))){ + return log( erf(erf1_x) - erf(erf2_x) ) + log05; // same but faster than log( 0.5 * (erf(erf1_x) - erf(erf2_x)) ) + } + + //Otherwise use erf_aux(x) + //At this point, erf1_x and erf2_x have the same sign and are both distant from 0 + + double erf1 = erf_aux(erf1_x); + double erf2 = erf_aux(erf2_x); + + //These operations are described in the documentation of erf_aux() + if(erf1_x > 0){ //both are positive + return log( 1.0 - exp(erf1-erf2) ) + erf2 + log05; + }else{ //both are negative + return log( 1.0 - exp(erf2-erf1) ) + erf1 + log05; + } + +} + + + +double FieldNoise::erf_aux(double a){ + double r, s, t, u; + + t = fabs (a); + s = a * a; + + r = fma (-5.6271698458222802e-018, t, 4.8565951833159269e-016); + u = fma (-1.9912968279795284e-014, t, 5.1614612430130285e-013); + r = fma (r, s, u); + r = fma (r, t, -9.4934693735334407e-012); + r = fma (r, t, 1.3183034417266867e-010); + r = fma (r, t, -1.4354030030124722e-009); + r = fma (r, t, 1.2558925114367386e-008); + r = fma (r, t, -8.9719702096026844e-008); + r = fma (r, t, 5.2832013824236141e-007); + r = fma (r, t, -2.5730580226095829e-006); + r = fma (r, t, 1.0322052949682532e-005); + r = fma (r, t, -3.3555264836704290e-005); + r = fma (r, t, 8.4667486930270974e-005); + r = fma (r, t, -1.4570926486272249e-004); + r = fma (r, t, 7.1877160107951816e-005); + r = fma (r, t, 4.9486959714660115e-004); + r = fma (r, t, -1.6221099717135142e-003); + r = fma (r, t, 1.6425707149019371e-004); + r = fma (r, t, 1.9148914196620626e-002); + r = fma (r, t, -1.0277918343487556e-001); + r = fma (r, t, -6.3661844223699315e-001); + r = fma (r, t, -1.2837929411398119e-001); + r = fma (r, t, -t); + + return r; +} \ No newline at end of file diff --git a/cpp/localization/FieldNoise.h b/cpp/localization/FieldNoise.h new file mode 100644 index 0000000..9d91c49 --- /dev/null +++ b/cpp/localization/FieldNoise.h @@ -0,0 +1,91 @@ +/** + * FILENAME: FieldNoise + * DESCRIPTION: efficient computation of relative probabilities (for the noise model of the RoboCup 3DSSL) + * AUTHOR: Miguel Abreu (m.abreu@fe.up.pt) + * DATE: 2021 + */ + +#pragma once +#include "math.h" + +class FieldNoise{ + public: + + /** + * Log probability of real distance d, given noisy radius r + */ + static double log_prob_r(double d, double r); + + /** + * Log probability of real horizontal angle h, given noisy angle phi + */ + static double log_prob_h(double h, double phi); + + /** + * Log probability of real vertical angle v, given noisy angle theta + */ + static double log_prob_v(double v, double theta); + + + private: + + FieldNoise(){}; //Disable construction + + /** + * Log probability of normally distributed random variable X from interval1 to interval2: + * Log Pr[interval1 < X < interval2] + * @param mean mean of random variable + * @param std standard deviation of random variable + * @param interval1 minimum value of random variable + * @param interval2 maximum value of random variable + */ + static double log_prob_normal_distribution(double mean, double std, double interval1, double interval2); + + /** + * This function returns ln(1-sgn(a)*erf(a)), but sgn(a)*erf(a) = |erf(a)|, because sgn(a) == sgn(erf(a)) + * So, it returns: ln(1-|erf(a)|), which is <=0 + * + * NOTE: condition to guarantee high precision: |a|>= 1 + * + * how to compute erf(a) ? + * erf(a) = sgn(a)(1-e^erf_aux(a)) + * + * how to compute erf(a)+erf(b) ? + * erf(a)+erf(b) = sgn(a)(1-e^erf_aux(a)) + sgn(b)(1-e^erf_aux(b)) + * assuming a<0 and b>0: + * = e^erf_aux(a) -1 + 1 - e^erf_aux(b) + * = e^erf_aux(a) - e^erf_aux(b) + * + * example: erf(-7)+erf(7.1) + * if we computed it directly: + * erf(-7)+erf(7.1) = -0.9999999(...) + 0.9999999(...) = -1+1 = 0 (due to lack of precision, even if using double) + * if we use the proposed method: + * e^erf_aux(-7) - e^erf_aux(7.1) = -1.007340e-23 - -4.183826e-23 = 3.176486E-23 + * + * how to compute ln(erf(a)+erf(b)) ? + * assuming a<0 and b>0: + * ln(erf(a)+erf(b)) = ln( exp(erf_aux(a)) - exp(erf_aux(b)) ) + * = ln( exp(erf_aux(a)-k) - exp(erf_aux(b)-k) ) + k + * where k = min(erf_aux(a), erf_aux(b)) + * + * how to compute ln(erf(a)-erf(b)) ? (the difference is just the assumption) + * assuming a*b >= 0 + * + * ln(erf(a)-erf(b)) = ln( sgn(a)(1-e^erf_aux(a)) - sgn(a)(1-e^erf_aux(b)) ), note that sgn(a)=sgn(b) + * + * rule: log( exp(a) - exp(b) ) = log( exp(a-k) - exp(b-k) ) + k + * + * if(a>0) + * ln(erf(a)-erf(b)) = ln( 1 - e^erf_aux(a) - 1 + e^erf_aux(b)) + * = ln( exp(erf_aux(b)) - exp(erf_aux(a)) ) + * = ln( exp(erf_aux(b)-erf_aux(b)) - exp(erf_aux(a)-erf_aux(b)) ) + erf_aux(b) + * = ln( 1 - exp(erf_aux(a)-erf_aux(b)) ) + erf_aux(b) + * if(a<0) + * ln(erf(a)-erf(b)) = ln( -1 + e^erf_aux(a) + 1 - e^erf_aux(b)) + * = ln( exp(erf_aux(a)) - exp(erf_aux(b)) ) + * = ln( exp(erf_aux(a)-erf_aux(a)) - exp(erf_aux(b)-erf_aux(a)) ) + erf_aux(a) + * = ln( 1 - exp(erf_aux(b)-erf_aux(a)) ) + erf_aux(a) + * + */ + static double erf_aux(double a); +}; \ No newline at end of file diff --git a/cpp/localization/Geometry.cpp b/cpp/localization/Geometry.cpp new file mode 100644 index 0000000..79f19ac --- /dev/null +++ b/cpp/localization/Geometry.cpp @@ -0,0 +1,344 @@ +#include "Geometry.h" + + +/** + * This function returns the cosine of a given angle in degrees using the + * built-in cosine function that works with angles in radians. + * + * @param x an angle in degrees + * @return the cosine of the given angle + */ +float Cos(float x) { + return ( cos(x * M_PI / 180)); +} + +/** + * This function returns the sine of a given angle in degrees using the + * built-in sine function that works with angles in radians. + * + * @param x an angle in degrees + * @return the sine of the given angle + */ +float Sin(float x) { + return ( sin(x * M_PI / 180)); +} + +/** + * This function returns the principal value of the arc tangent of y/x in + * degrees using the signs of both arguments to determine the quadrant of the + * return value. For this the built-in 'atan2' function is used which returns + * this value in radians. + * + * @param x a float value + * @param y a float value + * @return the arc tangent of y/x in degrees taking the signs of x and y into + * account + */ +float ATan2(float x, float y) { + if (fabs(x) < EPSILON && fabs(y) < EPSILON) + return ( 0.0); + + return ( atan2(x, y) * 180 / M_PI ); +} + + + + +/************************************************************************/ +/******************* CLASS VECTOR ***********************************/ +/************************************************************************/ + +/*! Constructor for the Vector class. Arguments x and y + denote the x- and y-coordinates of the new position. + \param x the x-coordinate of the new position + \param y the y-coordinate of the new position + \return the Vector corresponding to the given arguments */ +Vector::Vector(float vx, float vy) : x(vx), y(vy) {} + +/*! Overloaded version of unary minus operator for Vectors. It returns the + negative Vector, i.e. both the x- and y-coordinates are multiplied by + -1. The current Vector itself is left unchanged. + \return a negated version of the current Vector */ +Vector Vector::operator-() const{ + return ( Vector(-x, -y)); +} + +/*! Overloaded version of the binary plus operator for adding a given float + value to a Vector. The float value is added to both the x- and + y-coordinates of the current Vector. The current Vector itself is + left unchanged. + \param d a float value which has to be added to both the x- and + y-coordinates of the current Vector + \return the result of adding the given float value to the current + Vector */ +Vector Vector::operator+(const float &d) const{ + return ( Vector(x + d, y + d)); +} + +/*! Overloaded version of the binary plus operator for Vectors. It returns + the sum of the current Vector and the given Vector by adding their + x- and y-coordinates. The Vectors themselves are left unchanged. + \param p a Vector + \return the sum of the current Vector and the given Vector */ +Vector Vector::operator+(const Vector &p) const{ + return ( Vector(x + p.x, y + p.y)); +} + +/*! Overloaded version of the binary minus operator for subtracting a + given float value from a Vector. The float value is + subtracted from both the x- and y-coordinates of the current + Vector. The current Vector itself is left unchanged. + \param d a float value which has to be subtracted from both the x- and + y-coordinates of the current Vector + \return the result of subtracting the given float value from the current + Vector */ +Vector Vector::operator-(const float &d) const{ + return ( Vector(x - d, y - d)); +} + +/*! Overloaded version of the binary minus operator for + Vectors. It returns the difference between the current + Vector and the given Vector by subtracting their x- and + y-coordinates. The Vectors themselves are left unchanged. + + \param p a Vector + \return the difference between the current Vector and the given + Vector */ +Vector Vector::operator-(const Vector &p) const { + return ( Vector(x - p.x, y - p.y)); +} + +/*! Overloaded version of the multiplication operator for multiplying a + Vector by a given float value. Both the x- and y-coordinates of the + current Vector are multiplied by this value. The current Vector + itself is left unchanged. + \param d the multiplication factor + \return the result of multiplying the current Vector by the given + float value */ +Vector Vector::operator*(const float &d) const{ + return ( Vector(x * d, y * d)); +} + +/*! Overloaded version of the multiplication operator for + Vectors. It returns the product of the current Vector + and the given Vector by multiplying their x- and + y-coordinates. The Vectors themselves are left unchanged. + + \param p a Vector + \return the product of the current Vector and the given Vector */ +Vector Vector::operator*(const Vector &p) const{ + return ( Vector(x * p.x, y * p.y)); +} + +/*! Overloaded version of the division operator for dividing a + Vector by a given float value. Both the x- and y-coordinates + of the current Vector are divided by this value. The current + Vector itself is left unchanged. + + \param d the division factor + \return the result of dividing the current Vector by the given float + value */ +Vector Vector::operator/(const float &d) const{ + return ( Vector(x / d, y / d)); +} + +/*! Overloaded version of the division operator for Vectors. It + returns the quotient of the current Vector and the given + Vector by dividing their x- and y-coordinates. The + Vectors themselves are left unchanged. + + \param p a Vector + \return the quotient of the current Vector and the given one */ +Vector Vector::operator/(const Vector &p) const{ + return ( Vector(x / p.x, y / p.y)); +} + +/*! Overloaded version of the assignment operator for assigning a given float + value to both the x- and y-coordinates of the current Vector. This + changes the current Vector itself. + \param d a float value which has to be assigned to both the x- and + y-coordinates of the current Vector */ +void Vector::operator=(const float &d) { + x = d; + y = d; +} + +/*! Overloaded version of the sum-assignment operator for Vectors. It + returns the sum of the current Vector and the given Vector by + adding their x- and y-coordinates. This changes the current Vector + itself. + \param p a Vector which has to be added to the current Vector */ +void Vector::operator+=(const Vector &p) { + x += p.x; + y += p.y; +} + +/*! Overloaded version of the sum-assignment operator for adding a given float + value to a Vector. The float value is added to both the x- and + y-coordinates of the current Vector. This changes the current + Vector itself. + \param d a float value which has to be added to both the x- and + y-coordinates of the current Vector */ +void Vector::operator+=(const float &d) { + x += d; + y += d; +} + +/*! Overloaded version of the difference-assignment operator for + Vectors. It returns the difference between the current + Vector and the given Vector by subtracting their x- and + y-coordinates. This changes the current Vector itself. + + \param p a Vector which has to be subtracted from the current + Vector */ +void Vector::operator-=(const Vector &p) { + x -= p.x; + y -= p.y; +} + +/*! Overloaded version of the difference-assignment operator for + subtracting a given float value from a Vector. The float + value is subtracted from both the x- and y-coordinates of the + current Vector. This changes the current Vector itself. + + \param d a float value which has to be subtracted from both the x- and + y-coordinates of the current Vector */ +void Vector::operator-=(const float &d) { + x -= d; + y -= d; +} + +/*! Overloaded version of the multiplication-assignment operator for + Vectors. It returns the product of the current Vector + and the given Vector by multiplying their x- and + y-coordinates. This changes the current Vector itself. + + \param p a Vector by which the current Vector has to be + multiplied */ +void Vector::operator*=(const Vector &p) { + x *= p.x; + y *= p.y; +} + +/*! Overloaded version of the multiplication-assignment operator for + multiplying a Vector by a given float value. Both the x- and + y-coordinates of the current Vector are multiplied by this + value. This changes the current Vector itself. + + \param d a float value by which both the x- and y-coordinates of the + current Vector have to be multiplied */ +void Vector::operator*=(const float &d) { + x *= d; + y *= d; +} + +/*! Overloaded version of the division-assignment operator for + Vectors. It returns the quotient of the current Vector + and the given Vector by dividing their x- and + y-coordinates. This changes the current Vector itself. + + \param p a Vector by which the current Vector is divided */ +void Vector::operator/=(const Vector &p) { + x /= p.x; + y /= p.y; +} + +/*! Overloaded version of the division-assignment operator for + dividing a Vector by a given float value. Both the x- and + y-coordinates of the current Vector are divided by this + value. This changes the current Vector itself. + + \param d a float value by which both the x- and y-coordinates of the + current Vector have to be divided */ +void Vector::operator/=(const float &d) { + x /= d; + y /= d; +} + +/*! Overloaded version of the inequality operator for Vectors. It + determines whether the current Vector is unequal to the given + Vector by comparing their x- and y-coordinates. + + \param p a Vector + \return true when either the x- or y-coordinates of the given Vector + and the current Vector are different; false otherwise */ +bool Vector::operator!=(const Vector &p) { + return ( (x != p.x) || (y != p.y)); +} + +/*! Overloaded version of the inequality operator for comparing a + Vector to a float value. It determines whether either the x- + or y-coordinate of the current Vector is unequal to the given + float value. + + \param d a float value with which both the x- and y-coordinates of the + current Vector have to be compared. + \return true when either the x- or y-coordinate of the current Vector + is unequal to the given float value; false otherwise */ +bool Vector::operator!=(const float &d) { + return ( (x != d) || (y != d)); +} + +/*! Overloaded version of the equality operator for Vectors. It + determines whether the current Vector is equal to the given + Vector by comparing their x- and y-coordinates. + + \param p a Vector + \return true when both the x- and y-coordinates of the given + Vector and the current Vector are equal; false + otherwise */ +bool Vector::operator==(const Vector &p) { + return ( (x == p.x) && (y == p.y)); +} + +/*! Overloaded version of the equality operator for comparing a + Vector to a float value. It determines whether both the x- + and y-coordinates of the current Vector are equal to the + given float value. + + \param d a float value with which both the x- and y-coordinates of the + current Vector have to be compared. + \return true when both the x- and y-coordinates of the current Vector + are equal to the given float value; false otherwise */ +bool Vector::operator==(const float &d) { + return ( (x == d) && (y == d)); +} + + +/*! This method determines the distance between the current + Vector and a given Vector. This is equal to the + magnitude (length) of the vector connecting the two positions + which is the difference vector between them. + + \param p a Vecposition + \return the distance between the current Vector and the given + Vector */ +float Vector::getDistanceTo(const Vector p) { + return ( (*this -p).length()); +} + + +/*! This method determines the magnitude (length) of the vector + corresponding with the current Vector using the formula of + Pythagoras. + + \return the length of the vector corresponding with the current + Vector */ +float Vector::length() const { + return ( sqrt(x * x + y * y)); +} + +float Vector::crossProduct(const Vector p) { + return this->x*p.y - this->y*p.x; +} + + +/** + * This methods returns the inner product of this vector with another + * + * @param other the other vector + * @return inner product + */ +float Vector::innerProduct(const Vector& other) const { + return x * other.x + y * other.y; +} \ No newline at end of file diff --git a/cpp/localization/Geometry.h b/cpp/localization/Geometry.h new file mode 100644 index 0000000..c4afa07 --- /dev/null +++ b/cpp/localization/Geometry.h @@ -0,0 +1,71 @@ +#ifndef GEOMETRY_H +#define GEOMETRY_H + +#include +#include + +using namespace std; + +#define EPSILON 1e-10 + +/** + * Useful functions to operate with angles in degrees + */ +float Cos(float x); +float Sin(float x); +float ATan2(float x, float y); + + +/** + * @class Vector + * + * @brief This class represents a position in the 2d space + * + * A position is represented by a x-axis coordinate and a + * y-axis coordinate or in polar coordinates (r, phi) + * + * @author Hugo Picado (hugopicado@ua.pt) + * @author Nuno Almeida (nuno.alm@ua.pt) + * Adapted - Miguel Abreu + */ +class Vector { +public: + Vector(float vx = 0, float vy = 0); + + // overloaded arithmetic operators + Vector operator-() const; + Vector operator+(const float &d) const; + Vector operator+(const Vector &p) const; + Vector operator-(const float &d) const; + Vector operator-(const Vector &p) const; + Vector operator*(const float &d) const; + Vector operator*(const Vector &p) const; + Vector operator/(const float &d) const; + Vector operator/(const Vector &p) const; + void operator=(const float &d); + void operator+=(const Vector &p); + void operator+=(const float &d); + void operator-=(const Vector &p); + void operator-=(const float &d); + void operator*=(const Vector &p); + void operator*=(const float &d); + void operator/=(const Vector &p); + void operator/=(const float &d); + bool operator!=(const Vector &p); + bool operator!=(const float &d); + bool operator==(const Vector &p); + bool operator==(const float &d); + + float getDistanceTo(const Vector p); + float crossProduct(const Vector p); + float length() const; + float innerProduct(const Vector &p) const; + +public: + float x; + float y; +}; + + + +#endif // GEOMETRY_H diff --git a/cpp/localization/Line6f.cpp b/cpp/localization/Line6f.cpp new file mode 100644 index 0000000..ad41754 --- /dev/null +++ b/cpp/localization/Line6f.cpp @@ -0,0 +1,232 @@ +#include "Line6f.h" + + + +Line6f::Line6f(const Vector3f &polar_s, const Vector3f &polar_e) : + startp(polar_s), endp(polar_e), startc(polar_s.toCartesian()), endc(polar_e.toCartesian()), length(startc.dist(endc)) {}; + +Line6f::Line6f(const Vector3f &cart_s, const Vector3f &cart_e, float length) : + startp(cart_s.toPolar()), endp(cart_e.toPolar()), startc(cart_s), endc(cart_e), length(length) {}; + +Line6f::Line6f(const Line6f &obj) : + startp(obj.startp), endp(obj.endp), startc(obj.startc), endc(obj.endc), length(obj.length) {}; + +Vector3f Line6f::linePointClosestToCartPoint(const Vector3f &cp) const{ + + /** + * Equation of this line: (we want to find t, such that (cp-p) is perpendicular to this line) + * p = startc + t*(endc - startc) + * + * Let vecp=(cp-start) and vecline=(end-start) + * Scalar projection of vecp in the direction of vecline: + * sp = vecp.vecline/|vecline| + * Find the ratio t by dividing the scalar projection by the length of vecline: + * t = sp / |vecline| + * So the final expression becomes: + * t = vecp.vecline/|vecline|^2 + */ + + + Vector3f lvec(endc-startc); //this line's vector + float t = (cp-startc).innerProduct(lvec) / (length*length); + + return startc + lvec*t; +} + +Vector3f Line6f::linePointClosestToPolarPoint(const Vector3f &pp) const{ + return linePointClosestToCartPoint(pp.toCartesian()); +} + +float Line6f::lineDistToCartPoint(const Vector3f &cp) const{ + return ((cp-startc).crossProduct(cp-endc)).length() / (endc-startc).length(); +} + +float Line6f::lineDistToPolarPoint(const Vector3f &pp) const{ + return lineDistToCartPoint(pp.toCartesian()); +} + +//source: http://geomalgorithms.com/a07-_distance.html#dist3D_Line_to_Line() +float Line6f::lineDistToLine(const Line6f &l) const{ + Vector3f u = endc - startc; + Vector3f v = l.endc - l.startc; + Vector3f w = startc - l.startc; + float a = u.innerProduct(u); // always >= 0 + float b = u.innerProduct(v); + float c = v.innerProduct(v); // always >= 0 + float d = u.innerProduct(w); + float e = v.innerProduct(w); + float D = a*c - b*b; // always >= 0 + float sc, tc; + + // compute the line parameters of the two closest points + if (D < 1e-8f) { // the lines are almost parallel + sc = 0.0; + tc = (b>c ? d/b : e/c); // use the largest denominator + } + else { + sc = (b*e - c*d) / D; + tc = (a*e - b*d) / D; + } + + // get the difference of the two closest points + Vector3f dP = w + (u * sc) - (v * tc); // = L1(sc) - L2(tc) + + return dP.length(); // return the closest distance +} + +Vector3f Line6f::segmentPointClosestToCartPoint(const Vector3f &cp) const{ + + /** + * Equation of this line: (we want to find t, such that (cp-p) is perpendicular to this line) + * p = startc + t*(endc - startc) + * + * Let vecp=(cp-start) and vecline=(end-start) + * Scalar projection of vecp in the direction of vecline: + * sp = vecp.vecline/|vecline| + * Find the ratio t by dividing the scalar projection by the length of vecline: + * t = sp / |vecline| + * So the final expression becomes: + * t = vecp.vecline/|vecline|^2 + * Since this version requires that p belongs to the line segment, there is an additional restriction: + * 0 < t < 1 + */ + + Vector3f lvec(endc-startc); //this line's vector + float t = (cp-startc).innerProduct(lvec) / (length*length); + + if(t<0) t=0; + else if(t>1) t=1; + + return startc + lvec*t; +} + +Vector3f Line6f::segmentPointClosestToPolarPoint(const Vector3f &pp) const{ + return segmentPointClosestToCartPoint(pp.toCartesian()); +} + +float Line6f::segmentDistToCartPoint(const Vector3f &cp) const{ + + Vector3f v = endc - startc; //line segment vector + Vector3f w1 = cp - startc; + + if ( w1.innerProduct(v) <= 0) return w1.length(); + + Vector3f w2 = cp - endc; + + if ( w2.innerProduct(v) >= 0) return w2.length(); + + return v.crossProduct(w1).length() / this->length; + +} + +float Line6f::segmentDistToPolarPoint(const Vector3f &pp) const{ + return segmentDistToCartPoint(pp.toCartesian()); +} + +//source: http://geomalgorithms.com/a07-_distance.html#dist3D_Segment_to_Segment() +float Line6f::segmentDistToSegment(const Line6f &other) const{ + + Vector3f u = endc - startc; + Vector3f v = other.endc - other.startc; + Vector3f w = startc - other.startc; + float a = u.innerProduct(u); // always >= 0 + float b = u.innerProduct(v); + float c = v.innerProduct(v); // always >= 0 + float d = u.innerProduct(w); + float e = v.innerProduct(w); + float D = a*c - b*b; // always >= 0 + float sc, sN, sD = D; // sc = sN / sD, default sD = D >= 0 + float tc, tN, tD = D; // tc = tN / tD, default tD = D >= 0 + + // compute the line parameters of the two closest points + if (D < 1e-8f) { // the lines are almost parallel + sN = 0.0; // force using point start on segment S1 + sD = 1.0; // to prevent possible division by 0.0 later + tN = e; + tD = c; + } + else { // get the closest points on the infinite lines + sN = (b*e - c*d); + tN = (a*e - b*d); + if (sN < 0.0) { // sc < 0 => the s=0 edge is visible + sN = 0.0; + tN = e; + tD = c; + } + else if (sN > sD) { // sc > 1 => the s=1 edge is visible + sN = sD; + tN = e + b; + tD = c; + } + } + + if (tN < 0.0) { // tc < 0 => the t=0 edge is visible + tN = 0.0; + // recompute sc for this edge + if (-d < 0.0) + sN = 0.0; + else if (-d > a) + sN = sD; + else { + sN = -d; + sD = a; + } + } + else if (tN > tD) { // tc > 1 => the t=1 edge is visible + tN = tD; + // recompute sc for this edge + if ((-d + b) < 0.0) + sN = 0; + else if ((-d + b) > a) + sN = sD; + else { + sN = (-d + b); + sD = a; + } + } + // finally do the division to get sc and tc + sc = (abs(sN) < 1e-8f ? 0.0 : sN / sD); + tc = (abs(tN) < 1e-8f ? 0.0 : tN / tD); + + // get the difference of the two closest points + Vector3f dP = w + (u * sc) - (v * tc); // = S1(sc) - S2(tc) + + return dP.length(); // return the closest distance + +} + + +Vector3f Line6f::midPointCart() const{ + return (startc+endc)/2; +} + +Vector3f Line6f::midPointPolar() const{ + return midPointCart().toPolar(); +} + + +bool Line6f::operator==(const Line6f& other) const { + return (startp == other.startp) && (endp == other.endp); +} + +const Vector3f &Line6f::get_polar_pt(const int index) const{ + if(index==0) return startp; + return endp; +} + +const Vector3f &Line6f::get_cart_pt(const int index) const{ + if(index==0) return startc; + return endc; +} + +Vector3f Line6f::get_polar_vector() const{ + return endp-startp; +} + +Vector3f Line6f::get_cart_vector() const{ + return endc-startc; +} + + + + diff --git a/cpp/localization/Line6f.h b/cpp/localization/Line6f.h new file mode 100644 index 0000000..03efecb --- /dev/null +++ b/cpp/localization/Line6f.h @@ -0,0 +1,185 @@ +/** + * FILENAME: Line6f + * DESCRIPTION: Simple line (segment) class + * AUTHOR: Miguel Abreu (m.abreu@fe.up.pt) + * DATE: 2021 + * + * Immutable Class for: + * 3D Line composed of 2 Vector3f points + * Optimized for: + * - regular access to cartesian coordinates + * - regular access to line segment length + **/ + +#pragma once +#include "Vector3f.h" + + +class Line6f { +public: + + /** + * Polar start coordinate + */ + const Vector3f startp; + + /** + * Polar end coordinate + */ + const Vector3f endp; + + /** + * Cartesian start coordinate + */ + const Vector3f startc; + + /** + * Cartesian end coordinate + */ + const Vector3f endc; + + /** + * Length of line segment limited by (start,end) + */ + const float length; + + /** + * Constructor + * @param polar_s polar start point + * @param polar_e polar end point + */ + Line6f(const Vector3f &polar_s, const Vector3f &polar_e); + + /** + * Constructor + * @param cart_s cartesian start point + * @param cart_e cartesian end point + * @param length line segment length + */ + Line6f(const Vector3f &cart_s, const Vector3f &cart_e, float length); + + /** + * Copy Constructor + * @param obj another line + */ + Line6f(const Line6f &obj); + + /** + * Find point in line defined by (start,end) closest to given point + * @param cp cartesian point + * @return point in this infinite line closest to given point + */ + Vector3f linePointClosestToCartPoint(const Vector3f &cp) const; + + /** + * Find point in line defined by (start,end) closest to given point + * @param pp polar point + * @return point in this infinite line closest to given point + */ + Vector3f linePointClosestToPolarPoint(const Vector3f &pp) const; + + /** + * Find distance between line defined by (start,end) and given point + * @param cp cartesian point + * @return distance between line defined by (start,end) and given point + */ + float lineDistToCartPoint(const Vector3f &cp) const; + + /** + * Find distance between line defined by (start,end) and given point + * @param pp polar point + * @return distance between line defined by (start,end) and given point + */ + float lineDistToPolarPoint(const Vector3f &pp) const; + + /** + * Find distance between line defined by (start,end) and given line + * @param l line + * @return distance between line defined by (start,end) and given line + */ + float lineDistToLine(const Line6f &l) const; + + /** + * Find point in line defined by (start,end) closest to given point + * @param cp cartesian point + * @return point in this infinite line closest to given point + */ + Vector3f segmentPointClosestToCartPoint(const Vector3f &cp) const; + + /** + * Find point in line defined by (start,end) closest to given point + * @param pp polar point + * @return point in this infinite line closest to given point + */ + Vector3f segmentPointClosestToPolarPoint(const Vector3f &pp) const; + + /** + * Find distance between line segment limited by (start,end) and given point + * @param cp cartesian point + * @return distance between line segment limited by (start,end) and given point + */ + float segmentDistToCartPoint(const Vector3f &cp) const; + + /** + * Find distance between line segment limited by (start,end) and given point + * @param pp polar point + * @return distance between line segment limited by (start,end) and given point + */ + float segmentDistToPolarPoint(const Vector3f &pp) const; + + /** + * Find distance between line segment limited by (start,end) and given line segment + * @param other line segment + * @return distance between line segment limited by (start,end) and given line segment + */ + float segmentDistToSegment(const Line6f &other) const; + + /** + * Find midpoint of line segment limited by (start,end) + * @return cartesian midpoint of line segment limited by (start,end) + */ + Vector3f midPointCart() const; + + /** + * Find midpoint of line segment limited by (start,end) + * @return polar midpoint of line segment limited by (start,end) + */ + Vector3f midPointPolar() const; + + /** + * Operator == + * @param other line + * @return true if both lines are the same + */ + bool operator==(const Line6f& other) const; + + /** + * Get polar line ending by index + * @param index (0)->start or (1)->end + * @return polar line ending according to given index + */ + const Vector3f &get_polar_pt(const int index) const; + + /** + * Get cartesian line ending by index + * @param index (0)->start or (1)->end + * @return cartesian line ending according to given index + */ + const Vector3f &get_cart_pt(const int index) const; + + /** + * Get polar vector (end-start) + * @return polar vector (end-start) + */ + Vector3f get_polar_vector() const; + + /** + * Get cartesian vector (end-start) + * @return cartesian vector (end-start) + */ + Vector3f get_cart_vector() const; + + + + +}; \ No newline at end of file diff --git a/cpp/localization/LocalizerV2.cpp b/cpp/localization/LocalizerV2.cpp new file mode 100644 index 0000000..5971e83 --- /dev/null +++ b/cpp/localization/LocalizerV2.cpp @@ -0,0 +1,1249 @@ +#include "LocalizerV2.h" +#include "math.h" +#include "iostream" +#include "World.h" + +using namespace std; + +static World& world = SWorld::getInstance(); + + +/** + * Compute 3D position and 3D orientation + * */ +void LocalizerV2::run(){ + + Field& fd = SField::getInstance(); + + stats_change_state(RUNNING); + + //------------------ WORKFLOW: 0 + + _is_uptodate = false; + _is_head_z_uptodate = false; + _steps_since_last_update++; + + prelim_reset(); //reset preliminary transformation matrix + + fd.update(); //update visible collections + int lines_no = fd.list_segments.size(); + int landmarks_no = fd.list_landmarks.size(); + + if( (landmarks_no == 0 && lines_no < 2) || (lines_no == 0) ){ + if(lines_no==0 && landmarks_no==0){ stats_change_state(BLIND); } else { stats_change_state(MINFAIL); } + return; + } + + //------------------ WORKFLOW: 1-2 + + if( ! find_z_axis_orient_vec() ){ return; } + + //------------------ WORKFLOW: 3-4 + + if(!( landmarks_no >1 ? find_xy() : guess_xy() )){ return; } + + //------------------ Update public variables + + commit_everything(); + + stats_change_state(DONE); + + //------------------ Statistics + + //Ball position stats + if(world.ball_seen){ + counter_ball += stats_sample_position_error(prelimHeadToField * world.ball_rel_pos_cart, + world.ball_cheat_abs_cart_pos, errorSum_ball); + } + + //print_report(); //uncomment to enable report (average errors + solution analysis) + +} + + + +//================================================================================================= +//=================================================================================== GSL Utilities +//================================================================================================= + + +void add_gsl_regression_sample(gsl_matrix* m, gsl_vector* v, int sample_no, const Vector3f& relativeCoord, double absoluteCoord, double translCoeffMult=1){ + + gsl_matrix_set(m, sample_no, 0, relativeCoord.x); + gsl_matrix_set(m, sample_no, 1, relativeCoord.y); + gsl_matrix_set(m, sample_no, 2, relativeCoord.z); + gsl_matrix_set(m, sample_no, 3, translCoeffMult); + gsl_vector_set(v, sample_no, absoluteCoord ); + +} + +template +gsl_vector* create_gsl_vector(const std::array &content){ + + gsl_vector* v = gsl_vector_alloc (SIZE); + + for(int i=0; i 0){ //if vec is (0,0,1) or (0,0,-1) we keep the default rotation axis + gx = -vec.y / aux; + gy = vec.x / aux; + } + + return Vector(gx,gy); +} + + +/** + * Rotate vector around ground axis defined as u=(0,0,1)x(Zvec)/|Zvec| + * Direction of rotation from Zvec to (0,0,1) + * To invert the rotation direction, invert Zvec.x and Zvec.y + * @param v vector to be rotated + * @param Zvec unit normal vector of rotated ground plane + */ +Vector3f fast_rotate_around_ground_axis(Vector3f v, Vector3f Zvec){ + + Vector u = get_ground_unit_vec_perpendicular_to(Zvec); + + //Angle between unit normal vector of original plane and unit normal vector of rotated plane: + //cos(a) = (ov.rv)/(|ov||rv|) = ((0,0,1).(rvx,rvy,rvz))/(1*1) = rvz + float& cos_a = Zvec.z; + //assert(cos_a <= 1); + if(cos_a > 1) cos_a = 1; //Fix: it happens rarely, no cause was yet detected + float sin_a = -sqrtf(1 - cos_a*cos_a); //invert sin_a to invert a (direction was defined in method description) + + const float i_cos_a = 1-cos_a; + const float uxuy_i = u.x*u.y*i_cos_a; + const float uxux_i = u.x*u.x*i_cos_a; + const float uyuy_i = u.y*u.y*i_cos_a; + const float uxsin_a = u.x*sin_a; + const float uysin_a = u.y*sin_a; + + float x = (cos_a + uxux_i ) * v.x + uxuy_i * v.y + uysin_a * v.z; + float y = uxuy_i * v.x + (cos_a + uyuy_i ) * v.y - uxsin_a * v.z; + float z = - uysin_a * v.x + uxsin_a * v.y + cos_a * v.z; + + return Vector3f(x,y,z); + +} + + +/** + * Compute Xvec and Yvec from Zvec and angle + * @param Zvec input z-axis orientation vector + * @param angle input rotation of Xvec around Zvec (rads) + * @param Xvec output x-axis orientation vector + * @param Yvec output y-axis orientation vector + */ +void fast_compute_XYvec_from_Zvec(const Vector3f& Zvec, float agent_angle, Vector3f& Xvec, Vector3f& Yvec){ + /** + * There are two coordinate systems being considered in this method: + * - The actual agent's vision (RELATIVE system -> RELsys) + * - A rotated perspective where the agent's seen Zvec is the real Zvec (ROTATED system -> ROTsys) + * (the agent's optical axis / line of sight is parallel to ground ) + * + * SUMMARY: + * I provide an angle which defines the agent's rotation around Zvec (in the ROTsys) + * E.g. suppose the agent is rotated 5deg, then in the ROTsys, the agent sees Xvec as being rotated -5deg + * I then rotate Xvec to the RELsys, and compute the Yvec using cross product + * + * STEPS: + * 1st. Compute ROTsys, by finding rotation of plane defined by normal vector Zvec, in relation to seen XY plane + * (whose seen normal vector is (0,0,1)). We need: axis of rotation (unit vector lying on XY plane) and angle (rads): + * rotation axis: + * u = (0,0,1)x(Zvec) (vector perpendicular to XY plane and Zvec) + * = (-ZvecY,ZvecX,0) (rotation will be counterclockwise when u points towards the observer) + * (so a negative angle will bring the agent to the ROTsys) + * angle between Zvec and (0,0,1): + * a = acos( ((0,0,1).(Zvec)) / (|(0,0,1)|*|Zvec|) ) + * = acos( ((0,0,1).(Zvec)) ) + * = acos( ZvecZ ) + * + * 2nd. Establish Xvec in ROTsys: + * Let agent_angle be the agent's angle. Then Xvec's angle is (b=-agent_angle). + * Xvec = (cos(b),sin(b),0) + * + * 3rd. Rotate Xvec to RELsys: + * Let R be the rotation matrix that rotates from ROTsys to RELsys (positive angle using u): + * Xvec = R * Xvec + * = R * (cos(b),sin(b),0) + * = (R00 * cos(b) + R01 * sin(b), R10 * cos(b) + R11 * sin(b), R20 * cos(b) + R21 * sin(b)) + * where R is: (rotation matrix from axis and angle https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle) + * R00 = cos(a) + ux*ux(1-cos(a)) R01 = ux*uy(1-cos(a)) + * R10 = uy*ux(1-cos(a)) R11 = cos(a) + uy*uy(1-cos(a)) + * R20 = -uy*sin(a) R21 = ux*sin(a) + * so Xvec becomes: + * XvecX = cos(a)*cos(b) + (1-cos(a))*(ux*ux*cos(b) + ux*uy*sin(b)) + * XvecY = cos(a)*sin(b) + (1-cos(a))*(uy*uy*sin(b) + ux*uy*cos(b)) + * XvecZ = sin(a)*(ux*sin(b) - uy*cos(b)) + * + * 4th. To find Yvec we have two options: + * A. add pi/2 to b and compute Yvec with the same expression used for Xvec + * B. Yvec = Zvec x Xvec (specifically in this order for original coordinate system) + */ + + Vector u = get_ground_unit_vec_perpendicular_to(Zvec); + + const float& cos_a = Zvec.z; + const float sin_a = sqrtf(1 - cos_a*cos_a); + const float uxuy = u.x * u.y; + const float b = -agent_angle; //Xvec's angle + const float cos_b = cosf(b); + const float sin_b = sinf(b); + const float i_cos_a = 1-cos_a; + + Xvec.x = cos_a * cos_b + i_cos_a * ( u.x*u.x*cos_b + uxuy*sin_b ); + Xvec.y = cos_a * sin_b + i_cos_a * ( u.y*u.y*sin_b + uxuy*cos_b ); + Xvec.z = sin_a * ( u.x*sin_b - u.y*cos_b ); + + Yvec = Zvec.crossProduct(Xvec); //Using original coordinate system +} + + +//================================================================================================= +//================================================================================== Main algorithm +//================================================================================================= + +/** + * WORKFLOW: 1 + * See workflow description on header + */ +bool LocalizerV2::find_z_axis_orient_vec(){ + + Field& fd = SField::getInstance(); + + const int goalNo = fd.list_landmarks_goalposts.size(); + + if(fd.non_collinear_ground_markers >= 3){ + fit_ground_plane(); + return true; + } + + /** + * At this point we have > 0 lines. Having 1 line is the most common option. + * But having 2 lines is also possible if at least one of them is too short to create 2 reference points. + * But those 2 lines can be both too short, which would not be ideal for the algorithms below. + * Steps: find largest line, check if it is large enough + */ + const Line6f* l = &fd.list_segments.front(); + if(fd.list_segments.size() > 1){ + for(const auto& ll: fd.list_segments){ + if(ll.length > l->length) l = ≪ + } + } + + if(l->length < 1){ //larger line is too short + stats_change_state(FAILzLine); + return false; + } + + if(goalNo == 0){ //Case e.g.: 1 corner and 1 line (or 2 short lines) + stats_change_state(FAILzNOgoal); + return false; + } + + + //------------------------------ Prepare variables for solution A & B + + // Get any crossbar point + Vector3f crossbar_pt; + auto& glist = fd.list_landmarks_goalposts; + if(goalNo == 1){ + crossbar_pt = glist.front().relPosCart; + }else{ + if(glist[0].absPos.x == glist[1].absPos.x){ + crossbar_pt = Vector3f::determineMidpoint( glist[0].relPosCart, glist[1].relPosCart); + }else{ + //extremely rare: there are other solutions when goalNo>2 but cost/benefit not worth it + crossbar_pt = glist.front().relPosCart; + } + } + + //------------------------------ Identify and apply solution A & B + + //Get line point closest to crossbar point + Vector3f p = l->linePointClosestToCartPoint( crossbar_pt ); + Vector3f possibleZvec = crossbar_pt - p; + float possibleZvecLength = possibleZvec.length(); + + if(fabsf(possibleZvecLength - 0.8) < 0.05){ //Solution A & B + Vector3f unit_zvec = possibleZvec / possibleZvecLength; + + // save as the new z axis orientation vector + prelimHeadToField.set(2,0,unit_zvec.x); + prelimHeadToField.set(2,1,unit_zvec.y); + prelimHeadToField.set(2,2,unit_zvec.z); + + find_z(unit_zvec); + return true; + } + + //------------------------------ Apply solution C + + Vector3f crossbar_left_vec, crossbar_midp; //this crossbar vector points left if seen from the midfield (this is important for the cross product) + + const auto& goal_mm = Field::list_8_landmarks::goal_mm; + const auto& goal_mp = Field::list_8_landmarks::goal_mp; + const auto& goal_pm = Field::list_8_landmarks::goal_pm; + const auto& goal_pp = Field::list_8_landmarks::goal_pp; + + if( goal_mm.visible && goal_mp.visible){ + crossbar_left_vec = goal_mm.relPosCart - goal_mp.relPosCart; + crossbar_midp = (goal_mm.relPosCart + goal_mp.relPosCart)/2; + }else if( goal_pp.visible && goal_pm.visible){ + crossbar_left_vec = goal_pp.relPosCart - goal_pm.relPosCart; + crossbar_midp = (goal_pp.relPosCart + goal_pm.relPosCart)/2; + } + + /** + * Check if angle between line and crossbar is between 45deg and 135deg + * 45deg < acos(line.crossbar / |line||crossbar|) < 135deg <=> + * | line.crossbar / |line||crossbar| | < cos(45deg) <=> + * | line.crossbar | < cos(45deg) * |line| * ~2.1 <=> + * | line.crossbar | < 1.485 * |line| + */ + Vector3f lvec = l->get_cart_vector(); + if( goalNo > 1 && fabsf(lvec.innerProduct(crossbar_left_vec)) < 1.485 * l->length ){ + Vector3f Zvec; + if(l->startc.dist(crossbar_midp) > l->endc.dist(crossbar_midp)){ + Zvec = lvec.crossProduct(crossbar_left_vec); + }else{ + Zvec = crossbar_left_vec.crossProduct(lvec); + } + Zvec = Zvec.normalize(); // get unit vector + + // save as the new z axis orientation vector + prelimHeadToField.set(2,0,Zvec.x); + prelimHeadToField.set(2,1,Zvec.y); + prelimHeadToField.set(2,2,Zvec.z); + + find_z(Zvec); + return true; + } + + + + stats_change_state(FAILz); + return false; //no solution was found +} + +/** + * Find the best fitting ground plane's normal vector using Singular Value Decomposition + * Also compute the agent's height based on the ground references' centroid + * Dependency: at least 3 ground references + */ +void LocalizerV2::fit_ground_plane(){ + + Field& fd = SField::getInstance(); + + const auto& ground_markers = fd.list_weighted_ground_markers; + const int ground_m_size = ground_markers.size(); + + //------------------------------------ Compute groundmarks plane (if we have at least 3 groundmarks) + + gsl_matrix *A, *V; + gsl_vector *S, *work; + + A = gsl_matrix_alloc(ground_m_size, 3); + V = gsl_matrix_alloc(3, 3); + S = gsl_vector_alloc(3); + work = gsl_vector_alloc(3); + + // Find the centroid + Vector3f centroid(0,0,0); + for(const auto& g : ground_markers){ // Insert all weighted groundmarks in matrix + centroid += g.relPosCart; + } + centroid /= (float)ground_m_size; + + // Insert all groundmarks in matrix after subtracting the centroid + for(int i=0; i 0 (replacing x=0, y=0, z=0) + * d < 0 + * + * However, if the agent is lying down, the optimized plane may be slightly above its head due to vision errors + * So, if we have a better reference point, such as a goal post, we use it + */ + + if(!fd.list_landmarks_goalposts.empty()){ //If there are visible goal posts + const Vector3f& aerialpt = fd.list_landmarks_goalposts.front().relPosCart; //random aerial point (goal post) + if( a*aerialpt.x + b*aerialpt.y + c*aerialpt.z < d ){ //the goalpost is on the negative side, so we invert the normal vector + a=-a; b=-b; c=-c; + } + }else{ //If there are no visible goal posts, we rely on the agent's head + if(d > 0){// the normal vectors points down, so we invert it (we ignore 'd' from this point forward) + a=-a; b=-b; c=-c; + } + } + + + // save the ground plane's normal vector as the new z axis orientation vector + prelimHeadToField.set(2,0,a); + prelimHeadToField.set(2,1,b); + prelimHeadToField.set(2,2,c); + + // compute the agent's height + float h = max( -centroid.x*a - centroid.y*b - centroid.z*c ,0.064); + prelimHeadToField.set(2,3, h ); + + //Set public independent coordinate z (Not a problem: may be out of sync with transf matrix) + last_z = final_z; + final_z = h; + _is_head_z_uptodate = true; + +} + +/** + * Compute translation in z (height) + * Note: Apparently there's no real benefit in involving goalposts (weighted or not), only when the + * visible objects are below 5/6, and even then the difference is minimal. + */ +void LocalizerV2::find_z(const Vector3f& Zvec){ + + Field& fd = SField::getInstance(); + + Vector3f zsum; + for(const auto& g: fd.list_weighted_ground_markers){ + zsum += g.relPosCart; + } + + //Minimum height: 0.064m + float z = max( -(zsum/fd.list_weighted_ground_markers.size()).innerProduct(Zvec) ,0.064f); + + prelimHeadToField.set( 2,3,z ); + + //Set public independent coordinate z (Not a problem: may be out of sync with transf matrix) + last_z = final_z; + final_z = z; + _is_head_z_uptodate = true; + +} + + +/** + * Computes mapping error using distance probabilities + * @return negative log of ["normalized" probability = (p1*p2*p3*...*pn)^(1/n)] + */ +double LocalizerV2::map_error_logprob(const gsl_vector *v, void *params){ + + float angle; + Field& fd = SField::getInstance(); + + //Get angle from optimization vector, or from params (as a constant) + if(v->size == 3){ + angle = gsl_vector_get(v,2); + }else{ + angle = *(float *)params; + } + + Matrix4D& transfMat = SLocalizerV2::getInstance().prelimHeadToField; + Vector3f Zvec(transfMat.get(2,0), transfMat.get(2,1), transfMat.get(2,2)); + + Vector3f Xvec, Yvec; + fast_compute_XYvec_from_Zvec(Zvec, angle, Xvec, Yvec ); + + //These are the transformation coefficients that are being optimized + transfMat.set(0,0,Xvec.x); + transfMat.set(0,1,Xvec.y); + transfMat.set(0,2,Xvec.z); + transfMat.set(0,3,gsl_vector_get(v, 0)); + transfMat.set(1,0,Yvec.x); + transfMat.set(1,1,Yvec.y); + transfMat.set(1,2,Yvec.z); + transfMat.set(1,3,gsl_vector_get(v, 1)); + + Matrix4D inverseTransMat = transfMat.inverse_tranformation_matrix(); + + + double total_logprob = 0; + int total_err_cnt =0; + + //Add log probability of unknown markers (with known corresponding field segment) + for(const auto& u : fd.list_unknown_markers){ + + + //We know the closest field segment, so we can bring it to the agent's frame + Vector3f rel_field_s_start = inverseTransMat * u.fieldSeg->point[0]->get_vector(); + Vector3f rel_field_s_end = inverseTransMat * u.fieldSeg->point[1]->get_vector(); + + Line6f rel_field_s(rel_field_s_start, rel_field_s_end, u.fieldSeg->length); //Convert to Line6f + + Vector3f closest_polar_pt = rel_field_s.segmentPointClosestToCartPoint(u.relPosCart).toPolar(); + + total_logprob += FieldNoise::log_prob_r(closest_polar_pt.x, u.relPosPolar.x); + total_logprob += FieldNoise::log_prob_h(closest_polar_pt.y, u.relPosPolar.y); + total_logprob += FieldNoise::log_prob_v(closest_polar_pt.z, u.relPosPolar.z); + total_err_cnt++; + + } + + //Add log probability of known markers + for(const auto& k : fd.list_known_markers){ + + //Bring marker to agent's frame + Vector3f rel_k = (inverseTransMat * k.absPos.get_vector()).toPolar(); + + total_logprob += FieldNoise::log_prob_r(rel_k.x, k.relPosPolar.x); + total_logprob += FieldNoise::log_prob_h(rel_k.y, k.relPosPolar.y); + total_logprob += FieldNoise::log_prob_v(rel_k.z, k.relPosPolar.z); + total_err_cnt++; + + } + + //return log of "normalized" probability = (p1*p2*p3*...*pn)^(1/n) + //negative because the optimization method minimizes the loss function + double logNormProb = -total_logprob / total_err_cnt; + + if(!gsl_finite(logNormProb)) return 1e6; //fix + + return logNormProb; + +} + + + +/** + * Computes mapping error using 2d euclidian distances + * @return average distance + */ +double LocalizerV2::map_error_2d(const gsl_vector *v, void *params){ + + float angle; + Field& fd = SField::getInstance(); + + //Get angle from optimization vector, or from params (as a constant) + if(v->size == 3){ + angle = gsl_vector_get(v,2); + }else{ + angle = *(float *)params; + } + + Matrix4D& transfMat = SLocalizerV2::getInstance().prelimHeadToField; + Vector3f Zvec(transfMat.get(2,0), transfMat.get(2,1), transfMat.get(2,2)); + + Vector3f Xvec, Yvec; + fast_compute_XYvec_from_Zvec(Zvec, angle, Xvec, Yvec ); + + + //These are the transformation coefficients that are being optimized + transfMat.set(0,0,Xvec.x); + transfMat.set(0,1,Xvec.y); + transfMat.set(0,2,Xvec.z); + transfMat.set(0,3,gsl_vector_get(v, 0)); + transfMat.set(1,0,Yvec.x); + transfMat.set(1,1,Yvec.y); + transfMat.set(1,2,Yvec.z); + transfMat.set(1,3,gsl_vector_get(v, 1)); + + + float total_err = 0; + int total_err_cnt =0; + for(const Line6f& l : fd.list_segments){ + + //Compute line absolute coordinates according to current transformation + Vector3f ls = transfMat * l.startc; + Vector3f le = transfMat * l.endc; + + //Compute line angle and establish a tolerance + float l_angle = 0; + float l_angle_tolerance = 10; //default full tolerance (no point in being larger than pi/2 but no harm either) + + if(l.length > 0.8){ + //This is the easy case: find the angle and establish a small tolerance (which allows some visual rotation attempts) + l_angle = atan2f(le.y - ls.y, le.x - ls.x); + if(l_angle < 0) { l_angle += 3.14159265f; } //this is a line, not a vector, so positive angles are enough + l_angle_tolerance = 0.35f; //20 degrees + + } else if(fd.list_segments.size() <= 3) { + //It gets to a point where the cost/benefit is not very inviting. If there are many lines (>3), + //the small ones are not as decisive for the mapping error. Otherwise, we proceed: + + //If the small line is touching a big line, they have different orientations (it's a characteristic from the field lines) + + for(const Line6f& lbig : fd.list_segments){ + if(lbig.length < 2 || &lbig == &l ) continue; //check if line is big and different from current + + if(lbig.segmentDistToSegment(l)<0.5){ + //this would only generate false positives with the halfway line and 4 ring lines (if enough vision error) + //but even then, their orientation would be very different, so the method still holds + + //---------- get angle perpendicular to bigline (that is either the small line's angle, or at least close enough) + + //get bigline angle + Vector3f lbigs = transfMat * lbig.startc; + Vector3f lbige = transfMat * lbig.endc; + l_angle = atan2f(lbige.y - lbigs.y, lbige.x - lbigs.x); + + // add 90deg while keeping the angle between 0-180deg (same logic used when l.length > 0.8) + if (l_angle < -1.57079632f){ l_angle += 4.71238898f; } //Q3 -> add pi*3/2 + else if(l_angle < 0 ){ l_angle += 1.57079632f; } //Q4 -> add pi/2 + else if(l_angle < 1.57079632f ){ l_angle += 1.57079632f; } //Q1 -> add pi/2 + else { l_angle -= 1.57079632f; } //Q2 -> subtract pi/2 + + //This large tolerance means that this small line can be matched with almost everything except perpendicular lines + l_angle_tolerance = 1.22f; //70 deg tolerance + break; //end search for close big lines + } + } + } + + + //this default error of 1e6f is applied when there is no match (which means the transf. matrix's Xvec/Yvec are wrong) + float min_err = 1e6f; + for(const auto& s : Field::cFieldLineSegments::list){ //find distance to closest field line + + //Skip field line if seen line is substantially larger + if( l.length > (s.length + 0.7) ){ continue; } + + //Skip field line if orientation does not match + float angle_difference = fabsf(l_angle - s.angle); + if(angle_difference > 1.57079632f) angle_difference = 3.14159265f - angle_difference; + if(angle_difference > l_angle_tolerance) continue; + + //Error is the sum of the distance of a single line segment to both endpoints of seen line + float err = Field::fieldLineSegmentDistToCart2DPoint(s,ls.to2d()); + if(err < min_err) err += Field::fieldLineSegmentDistToCart2DPoint(s,le.to2d()); + + if(err < min_err) min_err = err; + } + + total_err += min_err; + total_err_cnt+=2; //a line has 2 points, double the weight of a single landmark + + } + + + for(const Field::sMarker& m : fd.list_landmarks){ + + Vector3f lpt = transfMat * m.relPosCart; //compute absolute coordinates according to current transformation + + float err = lpt.to2d().getDistanceTo(Vector(m.absPos.x,m.absPos.y)); + total_err += err > 0.5 ? err * 100 : err; + total_err_cnt++; + + } + + double avg_error = total_err / total_err_cnt; + + if(!gsl_finite(avg_error)) return 1e6; //fix + + return avg_error; //return average error +} + + + + +/** + * Apply fine tuning directly on the prelimHeadToField matrix + * 1st - improve map fitting + * 2nd - identify line segments and their endpoints + * 3rd - fine tune again using known markers + * @param initial_angle initial angle of Xvec around Zvec + * @param initial_x initial translation in x + * @param initial_y initial translation in y + */ +bool LocalizerV2::fine_tune(float initial_angle, float initial_x, float initial_y){ + + Field& fd = SField::getInstance(); + + //Statistics before fine tune + counter_fineTune += stats_sample_position_error(Vector3f(initial_x,initial_y,prelimHeadToField.get(11)), world.my_cheat_abs_cart_pos, errorSum_fineTune_before); + + //Fine tune, changing the initial parameters directly + if(!fine_tune_aux(initial_angle, initial_x, initial_y, false)) return false; + + //Statistics for 1st fine tune + stats_sample_position_error(Vector3f(initial_x,initial_y,prelimHeadToField.get(11)), world.my_cheat_abs_cart_pos, errorSum_fineTune_euclidianDist); + + //Identify new markers + fd.update_from_transformation(prelimHeadToField); + + //Probabilistic fine tune + fine_tune_aux(initial_angle, initial_x, initial_y, true); + + //Statistics for 2nd fine tune + stats_sample_position_error(prelimHeadToField.toVector3f(), world.my_cheat_abs_cart_pos, errorSum_fineTune_probabilistic); + + //Update unknown markers absolute position based on refined transformation matrix + fd.update_unknown_markers(prelimHeadToField); + + return true; +} + + +/** + * Apply fine tuning: + * - directly on: initial_angle, initial_x, initial_y (if use_probabilities == false) + * - directly on the prelimHeadToField matrix using probabilities (if use_probabilities == true) + * @param initial_angle initial angle of Xvec around Zvec + * @param initial_x initial translation in x + * @param initial_y initial translation in y + */ +bool LocalizerV2::fine_tune_aux(float &initial_angle, float &initial_x, float &initial_y, bool use_probabilities){ + + int status, iter=0; + gsl_vector* x = create_gsl_vector<3>({initial_x, initial_y, initial_angle}); // Initial transformation + gsl_vector* ss = create_gsl_vector<3>({0.02, 0.02, 0.03}); // Set initial step sizes + gsl_multimin_function minex_func = {map_error_2d, 3, nullptr}; // error func, variables no., params + if(use_probabilities) minex_func.f = map_error_logprob; // probablity-based error function + + const gsl_multimin_fminimizer_type *T = gsl_multimin_fminimizer_nmsimplex2; // algorithm type + gsl_multimin_fminimizer *s = gsl_multimin_fminimizer_alloc (T, 3); // allocate workspace + gsl_multimin_fminimizer_set (s, &minex_func, x, ss); // set workspace + + float best_x, best_y, best_ang; + + + do{ + iter++; + status = gsl_multimin_fminimizer_iterate(s); + + //*s holds the best solution, not the last solution + best_x = gsl_vector_get (s->x, 0); + best_y = gsl_vector_get (s->x, 1); + best_ang = gsl_vector_get (s->x, 2); + + if (status) break; + + double size = gsl_multimin_fminimizer_size (s); //minimizer-specific characteristic size + status = gsl_multimin_test_size (size, 1e-3); //This size can be used as a stopping criteria, as the simplex contracts itself near the minimum + + } + while ((status == GSL_CONTINUE || use_probabilities) && iter < 40); + + float best_map_error = s->fval; + + gsl_vector_free(x); + gsl_vector_free(ss); + gsl_multimin_fminimizer_free (s); + + if(!use_probabilities){ + if(best_map_error > 0.10){ + stats_change_state(FAILtune); + return false; + }else{ + initial_angle = best_ang; + initial_x = best_x; + initial_y = best_y; + return true; + } + } + + /** + * At this point, use_probabilities is true + * Note: The transformations are directly tested on prelimHeadToField but it currently + * holds the last test, so we set it manually here to the best found solution + */ + + //Convert angle into Xvec and Yvec + Vector3f Zvec(prelimHeadToField.get(2,0), prelimHeadToField.get(2,1), prelimHeadToField.get(2,2)); + Vector3f Xvec, Yvec; + fast_compute_XYvec_from_Zvec(Zvec, best_ang, Xvec, Yvec ); + + prelimHeadToField.set(0,0, Xvec.x); + prelimHeadToField.set(0,1, Xvec.y); + prelimHeadToField.set(0,2, Xvec.z); + prelimHeadToField.set(0,3, best_x); + prelimHeadToField.set(1,0, Yvec.x); + prelimHeadToField.set(1,1, Yvec.y); + prelimHeadToField.set(1,2, Yvec.z); + prelimHeadToField.set(1,3, best_y); + + return true; +} + +/** + * Find XY translation/rotation + * A unique solution is guaranteed if Zvec points in the right direction + * Requirement: 2 visible landmarks + */ +bool LocalizerV2::find_xy(){ + + Field& fd = SField::getInstance(); + + Vector3f Zvec(prelimHeadToField.get(2,0), prelimHeadToField.get(2,1), prelimHeadToField.get(2,2)); + + Field::sMarker *m1 = nullptr, *m2 = nullptr; + + //Get as many corners as possible + if(fd.list_landmarks_corners.size()>1){ + m1 = &fd.list_landmarks_corners[0]; + m2 = &fd.list_landmarks_corners[1]; + }else if(fd.list_landmarks_corners.size()==1){ + m1 = &fd.list_landmarks_corners[0]; + m2 = &fd.list_landmarks_goalposts[0]; + }else{ + m1 = &fd.list_landmarks_goalposts[0]; + m2 = &fd.list_landmarks_goalposts[1]; + } + + Vector3f realVec(m2->absPos.x - m1->absPos.x, m2->absPos.y - m1->absPos.y, m2->absPos.z - m1->absPos.z); + float real_angle = atan2f(realVec.y, realVec.x); //angle of real vector + + Vector3f seenVec(m2->relPosCart - m1->relPosCart); + Vector3f rotated_abs_vec = fast_rotate_around_ground_axis(seenVec, Zvec); + float seen_angle = atan2f(rotated_abs_vec.y, rotated_abs_vec.x); //angle of real vector + + float AgentAngle = real_angle - seen_angle; //no normalization is needed + + + Vector3f Xvec, Yvec; + fast_compute_XYvec_from_Zvec(Zvec, AgentAngle, Xvec, Yvec ); + + /** + * Let m be a landmark, rel:(mx,my,mz), abs:(mabsx, mabsy, mabsz) + * XvecX*mx + XvecY*my + XvecZ*mz + AgentX = mabsx + * AgentX = mabsx - (XvecX*mx + XvecY*my + XvecZ*mz) + * AgentX = mabsx - (XvecX . m) + * + * Generalizing for N estimates: + * AgentX = sum( mabsx - (XvecX . m) )/N + */ + float initial_x = 0, initial_y = 0; + for(const Field::sMarker& m : fd.list_landmarks){ + initial_x += m.absPos.x - Xvec.innerProduct(m.relPosCart); + initial_y += m.absPos.y - Yvec.innerProduct(m.relPosCart); + } + + initial_x /= fd.list_landmarks.size(); + initial_y /= fd.list_landmarks.size(); + + + return fine_tune(AgentAngle, initial_x, initial_y); + +} + +bool LocalizerV2::guess_xy(){ + Field& fd = SField::getInstance(); + + //Get Zvec from previous steps + Vector3f Zvec(prelimHeadToField.get(2,0), prelimHeadToField.get(2,1), prelimHeadToField.get(2,2)); + Vector last_known_position(head_position.x, head_position.y); + + //------------------------------------------------------------ Get longest line and use it as X or Y vector + + const Line6f* longestLine = &fd.list_segments.front(); + for(const Line6f& l : fd.list_segments){ + if(l.length > longestLine->length) longestLine = &l; + } + + if(longestLine->length < 1.6){ + stats_change_state(FAILguessLine); + return false; //largest line is too short, it could be mistaken for a ring line + } + + //Rotate line to real ground plane, where it loses the 3rd dimension + Vector3f longestLineVec = longestLine->endc - longestLine->startc; + Vector3f rotated_abs_line = fast_rotate_around_ground_axis(longestLineVec, Zvec); + + //The line can be aligned with X or Y, positively or negatively (these angles don't need to be normalized) + float fixed_angle[4]; + fixed_angle[0] = -atan2f(rotated_abs_line.y,rotated_abs_line.x); //if longestLineVec is Xvec + fixed_angle[1] = fixed_angle[0] + 3.14159265f; //if longestLineVec is -Xvec + fixed_angle[2] = fixed_angle[0] + 1.57079633f; //if longestLineVec is Yvec + fixed_angle[3] = fixed_angle[0] - 1.57079633f; //if longestLineVec is -Yvec + + //------------------------------------------------------------ Get initial translation + + //if we see 1 landmark, we use it, if not, we get the last position + + float initial_x[4], initial_y[4]; + bool noLandmarks = fd.list_landmarks.empty(); + + if(noLandmarks){ + + for(int i=0; i<4; i++){ + initial_x[i] = last_known_position.x; + initial_y[i] = last_known_position.y; + } + + } else { + + Vector3f Xvec = longestLineVec / longestLine->length; + Vector3f Yvec(Zvec.crossProduct(Xvec)); + + /** + * Let m be a landmark, rel:(mx,my,mz), abs:(mabsx, mabsy, mabsz) + * XvecX*mx + XvecY*my + XvecZ*mz + AgentX = mabsx + * AgentX = mabsx - (XvecX*mx + XvecY*my + XvecZ*mz) + * AgentX = mabsx - (XvecX . m) + */ + + const Field::sMarker& m = fd.list_landmarks.front(); + const float x_aux = Xvec.innerProduct(m.relPosCart); + const float y_aux = Yvec.innerProduct(m.relPosCart); + + initial_x[0] = m.absPos.x - x_aux; + initial_y[0] = m.absPos.y - y_aux; + initial_x[1] = m.absPos.x + x_aux; //2nd version: X is inverted + initial_y[1] = m.absPos.y + y_aux; //2nd version: Y is inverted + initial_x[2] = m.absPos.x + y_aux; //3rd version: X is inverted Y + initial_y[2] = m.absPos.y - x_aux; //3rd version: Y is X + initial_x[3] = m.absPos.x - y_aux; //4th version: X is Y + initial_y[3] = m.absPos.y + x_aux; //4th version: Y is inverted X + + } + + + //------------------------------------------------------------ Optimize XY rotation for each possible orientation + + + const gsl_multimin_fminimizer_type *T = gsl_multimin_fminimizer_nmsimplex2; + gsl_multimin_fminimizer *s[4] = {nullptr,nullptr,nullptr,nullptr}; + gsl_vector *ss[4], *x[4]; + gsl_multimin_function minex_func[4]; + + size_t iter = 0; + int status; + double size; + + for(int i=0; i<4; i++){ + x[i] = create_gsl_vector<2>({initial_x[i], initial_y[i]}); // Initial transformation + ss[i] = create_gsl_vector<2>({1, 1}); //Set initial step sizes to 1 + + /* Initialize method */ + minex_func[i].n = 2; + minex_func[i].f = map_error_2d; + minex_func[i].params = &fixed_angle[i]; + + s[i] = gsl_multimin_fminimizer_alloc (T, 2); + gsl_multimin_fminimizer_set (s[i], &minex_func[i], x[i], ss[i]); + } + + /* start iterating */ + bool running[4] = {true,true,true,true}; + float current_error[4] = {1e6,1e6,1e6,1e6}; + float lowest_error = 1e6; + Vector best_xy[4]; + const int maximum_iterations = 50; + bool plausible_solution[4] = {false,false,false,false}; + do{ + iter++; + for(int i=0; i<4; i++){ + if(!running[i]) continue; + + status = gsl_multimin_fminimizer_iterate(s[i]); + + current_error[i] = s[i]->fval; + if(current_error[i] < lowest_error) lowest_error = current_error[i]; + + // Possible errors: + // GSL_ERROR ("incompatible size of x", GSL_EINVAL); This should only be a concern during code design + // GSL_ERROR ("contraction failed", GSL_EFAILED); Evaluation function produced non finite value + if (status) { + running[i]=false; //This is not a valid solution + continue; + } + + size = gsl_multimin_fminimizer_size (s[i]); //minimizer-specific characteristic size + status = gsl_multimin_test_size (size, 1e-2); //This size can be used as a stopping criteria, as the simplex contracts itself near the minimum + + if(status != GSL_CONTINUE || (lowest_error * 50 < current_error[i])) { //finished or aborted + best_xy[i].x = gsl_vector_get (s[i]->x, 0); + best_xy[i].y = gsl_vector_get (s[i]->x, 1); + running[i]=false; + plausible_solution[i]=(status == GSL_SUCCESS); //only valid if it converged to local minimum + continue; + } + + + } + + + } while (iter < maximum_iterations && (running[0] || running[1] || running[2] || running[3])); + + for(int i=0; i<4; i++){ + gsl_vector_free(x[i]); + gsl_vector_free(ss[i]); + gsl_multimin_fminimizer_free (s[i]); + } + + + //At this point, a solution is plausible if it converged to a local minimum + //So, we apply the remaining criteria for plausiblity + int plausible_count = 0; + int last_i; + for(int i=0; i<4; i++){ + if(!plausible_solution[i]) continue; + bool isDistanceOk = (!noLandmarks) || last_known_position.getDistanceTo(best_xy[i]) < 0.5; // distance to last known position + if(current_error[i] < 0.12 && isDistanceOk){ // mapping error + plausible_count++; + last_i = i; + } + } + + // If there is 1 landmark, and multiple options, the distance to last known pos is now used to eliminate candidates + if(!noLandmarks && plausible_count>1){ + plausible_count = 0; + for(int i=0; i<4; i++){ + if(plausible_solution[i] && last_known_position.getDistanceTo(best_xy[i]) < 0.5){ // distance to last known position + plausible_count++; + last_i = i; + } + } + } + + //Check if best solution is good if all others are not even plausible + if(plausible_count==0){ + stats_change_state(FAILguessNone); + return false; + }else if(plausible_count>1){ + stats_change_state(FAILguessMany); + return false; + }else if(current_error[last_i] > 0.06 || (noLandmarks && last_known_position.getDistanceTo(best_xy[last_i]) > 0.3)){ // mapping error / distance to last known position + stats_change_state(FAILguessTest); + return false; + } + + return fine_tune(fixed_angle[last_i],best_xy[last_i].x, best_xy[last_i].y); + +} + + + +/** + * Called to update every public variable (rotation + translation) + */ +void LocalizerV2::commit_everything(){ + + final_headTofieldTransform = prelimHeadToField; //Full transformation (relative to absolute) + + final_headTofieldTransform.inverse_tranformation_matrix( final_fieldToheadTransform ); //Full transformation (absolute to relative) + + for(int i=0; i<3; i++){ + + //Rotation (relative to absolute) + final_headTofieldRotate.set(i ,final_headTofieldTransform.get(i )); //Copy rotation line 1 + final_headTofieldRotate.set(i+4,final_headTofieldTransform.get(i+4)); //Copy rotation line 2 + final_headTofieldRotate.set(i+8,final_headTofieldTransform.get(i+8)); //Copy rotation line 3 + + //Rotation (absolute to relative) + final_fieldToheadRotate.set(i ,final_fieldToheadTransform.get(i )); //Copy rotation line 1 + final_fieldToheadRotate.set(i+4,final_fieldToheadTransform.get(i+4)); //Copy rotation line 2 + final_fieldToheadRotate.set(i+8,final_fieldToheadTransform.get(i+8)); //Copy rotation line 3 + } + + final_translation = final_headTofieldTransform.toVector3f(); + + _is_uptodate = true; + + _steps_since_last_update = 0; + + //Add current 3D position to history + position_history[position_history_ptr++] = final_translation; + if(position_history_ptr >= position_history.size()) position_history_ptr=0; + +} + + +Vector3f LocalizerV2::relativeToAbsoluteCoordinates(const Vector3f relativeCoordinates) const{ + return headTofieldTransform * relativeCoordinates; +} + +Vector3f LocalizerV2::absoluteToRelativeCoordinates(const Vector3f absoluteCoordinates) const{ + return fieldToheadTransform * absoluteCoordinates; +} + + + +/** + * Reset preliminary matrix with a constant for the first 3 rows + */ +void LocalizerV2::prelim_reset(){ + //Since it was initialized as identity matrix we never need to change the last row + for(int i=0; i<12; i++){ + prelimHeadToField.set(i,9999); //flag unchanged cells with a constant + } +} + +//================================================================================================= +//=============================================================================== useful statistics +//================================================================================================= + +Vector3f LocalizerV2::get_velocity(unsigned int n) const{ + //assert(n > 0 && n < position_history.size() && "LocalizerV2::get_velocity(unsigned int n) -> n must be between 1 and 9!"); + + int l = position_history.size() - 1; + + Vector3f current_pos = position_history[(position_history_ptr + l) % position_history.size()]; + Vector3f last_pos = position_history[(position_history_ptr + l - n) % position_history.size()]; + + return current_pos - last_pos; +} + +//================================================================================================= +//================================================================================ debug statistics +//================================================================================================= + + +void LocalizerV2::print_report() const{ + + if(counter_fineTune == 0){ + cout << "LocalizerV2 Report - Check if the server is providing cheat data.\n"; + return; + } + + if(counter_fineTune < 2) return; //otherwise, c-1=0 + const int &c = counter_fineTune; + const int &cb = counter_ball; + const int c1 = c-1; + const int cb1 = cb-1; + + const double* ptr = errorSum_fineTune_before; + float e1_2d_var = (ptr[4] - (ptr[3]*ptr[3]) / c) / c1; + float e1_3d_var = (ptr[6] - (ptr[5]*ptr[5]) / c) / c1; + float e1[] = { ptr[3]/c, sqrt(e1_2d_var), ptr[5]/c, sqrt(e1_3d_var), ptr[0]/c, ptr[1]/c, ptr[2]/c }; + + ptr = errorSum_fineTune_euclidianDist; + float e2_2d_var = (ptr[4] - (ptr[3]*ptr[3]) / c) / c1; + float e2_3d_var = (ptr[6] - (ptr[5]*ptr[5]) / c) / c1; + float e2[] = { ptr[3]/c, sqrt(e2_2d_var), ptr[5]/c, sqrt(e2_3d_var), ptr[0]/c, ptr[1]/c, ptr[2]/c }; + + ptr = errorSum_fineTune_probabilistic; + float e3_2d_var = (ptr[4] - (ptr[3]*ptr[3]) / c) / c1; + float e3_3d_var = (ptr[6] - (ptr[5]*ptr[5]) / c) / c1; + float e3[] = { ptr[3]/c, sqrt(e3_2d_var), ptr[5]/c, sqrt(e3_3d_var), ptr[0]/c, ptr[1]/c, ptr[2]/c }; + + ptr = errorSum_ball; + float e4_2d_var=0, e4_3d_var=0; + if(cb1 > 0){ + e4_2d_var = (ptr[4] - (ptr[3]*ptr[3]) / cb) / cb1; + e4_3d_var = (ptr[6] - (ptr[5]*ptr[5]) / cb) / cb1; + } + float e4[] = { ptr[3]/cb, sqrt(e4_2d_var), ptr[5]/cb, sqrt(e4_3d_var), ptr[0]/cb, ptr[1]/cb, ptr[2]/cb }; + + const int* st = state_counter; + printf("---------------------------------- LocalizerV2 Report ----------------------------------\n"); + printf("SAMPLING STAGE 2D-MAE 2D-STD 3D-MAE 3D-STD x-MBE y-MBE z-MBE\n"); + printf("Before fine-tune: %.4f %.4f %.4f %.4f %7.4f %7.4f %7.4f\n", e1[0],e1[1],e1[2],e1[3],e1[4],e1[5],e1[6]); + printf("After Euclidian dist. fit: %.4f %.4f %.4f %.4f %7.4f %7.4f %7.4f\n", e2[0],e2[1],e2[2],e2[3],e2[4],e2[5],e2[6]); + printf("After probabilistic fit: %.4f %.4f %.4f %.4f %7.4f %7.4f %7.4f\n", e3[0],e3[1],e3[2],e3[3],e3[4],e3[5],e3[6]); + printf("Ball: %.4f %.4f %.4f %.4f %7.4f %7.4f %7.4f\n\n", e4[0],e4[1],e4[2],e4[3],e4[4],e4[5],e4[6]); + printf("* MBE(Mean Bias Error) MAE(Mean Abs Error) STD(Standard Deviation)\n"); + printf("* Note: the cheat positions should be active in server (preferably with >2 decimal places)\n\n"); + printf("------------------LocalizerV2::run calls analysis:\n"); + printf("- Total: %i \n", st[RUNNING]); + printf("- Successful: %i \n", st[DONE]); + printf("- Blind agent: %i \n", st[BLIND]); + printf("- Almost blind: %i \n", st[MINFAIL] + st[FAILzNOgoal] + st[FAILzLine] + st[FAILz]); + printf("- Guess location fail: %i \n", st[FAILguessLine] + st[FAILguessNone] + st[FAILguessMany] + st[FAILguessTest]); + printf("--- Lines too short: %i \n", st[FAILguessLine]); + printf("--- No solution: %i \n", st[FAILguessNone]); + printf("--- >1 solution: %i \n", st[FAILguessMany]); + printf("--- Weak solution: %i \n", st[FAILguessTest]); + printf("- Eucl. tune fail: %i \n", st[FAILtune]); //Euclidian distance tune error above 6cm + printf("----------------------------------------------------------------------------------------\n"); + +} + +void LocalizerV2::stats_reset(){ + + counter_fineTune = 0; + for(int i=0; i= 3 noncollinear ground references (z=0) + * ASSUMPTION: the ground references are not collinear. Why? + * If we see 2 lines their endpoints are never collinear. + * If we see one and we are on top of it, the feet contact points can cause collinearity but it is very rare. + * SOLUTION: Find the best fitting ground plane's normal vector using Singular Value Decomposition + * + * 1.2. there are < 3 noncollinear ground references (z=0) + * + * Possible combinations: + * If there is 1 corner flag, either we have >= 3 ground references, or it is impossible. + * So, below, we assume there are 0 corner flags. + * + * | 0 lines + 0/1/2 feet | 1 line + 0 feet | + * -------------|------------------------|------------------| + * 0 goalposts | ----- | ----- | (Only 1 line: there is no way of identifying it) + * 1 goalpost | ----- | A,C | (1 goalpost and 0/1/2 feet: infinite solutions) + * 2 goalposts | * | B,C | + * + * + * If it sees 1 or 2 goalposts and only 1 line, we assume for A & B that it is the endline (aka goal line) + * + * SOLUTIONS: + * 1.2.A. IF IT IS THE GOALLINE. Find the line's nearest point (p) to the goalpost (g), Zvec = (g-p) / |Zvec| + * 1.2.B. IF IT IS THE GOALLINE. Find the line's nearest point (p) to the goalposts (g1,g2) midpoint (m), Zvec = (m-p) / |Zvec| + * (This solution is more accurate than using only 1 goalpost. Even m is more accurate, on average, than g1 or g2.) + * 1.2.C. IF IT IS NOT THE GOALLINE. There are 3 options: + * I - There are 2 goalposts (crossbar line) and an orthogonal line: + * Zvec = crossbar x line (or) line x crossbar (depending of the direction of both vectors) + * II - Other situation if the z translation coordinate was externally provided through machine learning: + * Find any horizontal line (e.g. line between 2 goalposts, or ground line) + * Let M be any mark with known absolute z, and let Z be the externally provided z coordinate: + * Find Zvec such that (HorLine.Zvec=0) and (Zvec.Mrel=Mabsz-Z) + * III - If z was not provided: + * Skip to last step. + * 1.2.*. This scenario was tested and it is not valid. In certain body positions there are two solutions, and even though + * one is correct and generally yields lower error, the other one is a local optimum outside the field. One could + * exclude the out-of-field solution with some mildly expensive modifications to the optimization's error function, + * but the out-of-field scenario is not unrealistic, so this is not the way. Adding an external z source could help + * increasing the error of the wrong local optimum, but it may not be enough. Another shortcoming of this scenario is + * when we see the goalposts from the opposite goal, creating large visual error. + * + * 1.3. impossible / not implemented: in this case skip to last step + * + * ----------------------------------------------------------------------------------- + * 2. Compute z: + * + * Here's what we know about the transformation matrix so far: + * | - | - | - | - | + * | - | - | - | - | + * | zx | zy | zz | ? | We want to know the translation in z + * | 0 | 0 | 0 | 1 | + * + * Given a random point (p) with known relative coordinates and known absolute z coordinate, + * we can find the translation in z: + * p.relx * zx + p.rely * zy + p.relz * zz + ? = p.absz + * + * If we do this for every point, we can then average z + * + * ----------------------------------------------------------------------------------- + * 3. Compute a rough estimate for entire transformation (2 first rows): + * + * Solution quality for possible combinations: + * + * short line (length < 0.5m) *hard to detect orientation, *generated displacement error is insuficient for optimization + * long line (length >= 0.5m) + * + * | 0 landmarks | 1 goalpost | 1 corner | >= 2 landmarks | + * -----------------|---------------|--------------|------------|----------------| + * 0 long lines | --- | --- | --- | A | + * 1 long line | --- | B+ | B | A | + * 2 long lines | B | B+ | B++ | A | + * + * SOLUTIONS: + * A - the solution is unique + * STEPS: + * - Compute the X-axis and Y-axis orientation from 2 landmarks + * - Average the translation for every visible landmark + * - Fine-tune XY translation/rotation + * B - there is more than 1 solution, so we use the last known position as reference + * Minimum Requirements: + * - longest line must be >1.6m so that we can extract the orientation (hor/ver) while not being mistaken for a ring line + * - there is exactly 1 plausible solution + * Notes: + * B+ (the solution is rarely unique) + * B++ (the solution is virtually unique but cannot be computed with the algorithm for A scenarios) + * STEPS: + * - Find 4 possible orientations based on the longest line (which should be either aligned with X or Y) + * - Determine reasonable initial translation: + * - If the agent sees 1 landmark: compute XY translation for each of the 4 orientations based on that 1 landmark + * - If the agent sees 0 landmarks: use last known position + * Note: Why not use always last known position if that is a criterion in the end? + * Because in this stage it would only delay the optimization. + * - Optimize the X/Y translation for every possible orientation + * - Perform quality assessment + * Plausible solution if: + * - Optimization converged to local minimum + * - Distance to last known position <50cm (applicable if no visible landmarks) + * - Mapping error <0.12m/point + * - Given the agent's FOV, inverse mapping error <0.2m/point (disabled in V2) + * NOTE: If there is 1 landmark, plausibility is defined by mapping errors, not distance to last known pos. So if + * one guess has the only acceptable mapping error, but is the farthest from previous position, it is still chosen. + * However, if >1 guess has acceptable mapping error, the 0.5m threshold is used to eliminate candidates. + * Likely if: + * - Plausible + * - Distance to last known position <30cm (applicable if no visible landmarks) + * - Mapping error <0.06m/point + * - Given the agent's FOV, inverse mapping error <0.1m/point (not currently active) + * - Choose likely solution if all others are not even plausible + * - Fine-tune XY translation/rotation + * + * ----------------------------------------------------------------------------------- + * 4. Identify visible elements and perform 2nd fine tune based on distance probabilites + * + * ----------------------------------------------------------------------------------- + * Last step. Analyze preliminary transformation matrix to update final matrices + * + * For the reasons stated in the beginning (see warning), if the preliminary matrix was not entirely set, the + * actual transformation matrix will not be changed. But the head position will always reflect the latest changes. + * + * + * =================================================================================== + * LOCALIZATION BASED ON PROBABILITY DENSITIES + * =================================================================================== + * ================================PROBABILITY DENSITY================================ + * + * For 1 distance measurement from RCSSSERVER3D: + * + * Error E = d/100 * A~N(0,0.0965^2) + B~U(-0.005,0.005) + * PDF[d/100 * A](w) = PDF[N(0,(d/100 * 0.0965)^2)](w) + * PDF[E](w) = PDF[N(0,(d/100 * 0.0965)^2)](w) convoluted with PDF[U(-0.005,0.005)](w) + * + * where d is the distance from a given [p]oint (px,py,pz) to the [o]bject (ox,oy,oz) + * and w is the [m]easurement error: w = d-measurement = sqrt((px-ox)^2+(py-oy)^2+(pz-oz)^2) - measurement + * + * PDF[E](w) -> PDF[E](p,o,m) + * --------------------------------------------------------------- + * For n independent measurements: + * + * PDF[En](p,on,mn) = PDF[E1](p,o1,m1) * PDF[E2](p,o2,m2) * PDF[E3](p,o3,m3) * ... + * --------------------------------------------------------------- + * Adding z estimation: + * + * PDF[zE](wz) = PDF[N(mean,std^2)](wz) + * where wz is the zError = estz - pz + * + * PDF[zE](wz) -> PDF[zE](pz,estz) + * PDF[En](p,on,mn,estz) = PDF[En](p,on,mn) * PDF[zE](pz,estz) + * =================================================================================== + * =====================================GRADIENT====================================== + * + * Grad(PDF[En](p,on,mn,estz)) wrt p = Grad( PDF[E1](p,o1,m1) * ... * PDF[E2](p,on,mn) * PDF[zE](pz,estz)) wrt {px,py,pz} + * + * Generalizing the product rule for n factors, we have: + * + * Grad(PDF[En](p,on,mn)) wrt p = sum(gradPDF[Ei] / PDF[Ei]) * prod(PDF[Ei]) + * Grad(PDF[En](p,on,mn)) wrt p = sum(gradPDF[Ei] / PDF[Ei]) * PDF[En](p,on,mn) + * + * note that: gradPDF[zE](pz,estz) wrt {px,py,pz} = {0,0,d/d_pz} + * =================================================================================== + * */ + +#pragma once +#include "Singleton.h" +#include "Field.h" +#include "Matrix4D.h" +#include "FieldNoise.h" + +#include //Linear least-squares fitting +#include //Singular value decomposition +#include //Multidimensional minimization + +class LocalizerV2 { + friend class Singleton; + +public: + + /** + * Compute 3D position and 3D orientation + * sets "is_uptodate" to true if there is new information available (rotation+translation) + * If no new information is available, the last known position is provided + */ + void run(); + + /** + * Print report (average errors + solution analysis) + */ + void print_report() const; + + /** + * Transformation matrices + * They are initialized as 4x4 identity matrices + */ + const Matrix4D &headTofieldTransform = final_headTofieldTransform; // rotation + translation + const Matrix4D &headTofieldRotate = final_headTofieldRotate; // rotation + const Matrix4D &fieldToheadTransform = final_fieldToheadTransform; // rotation + translation + const Matrix4D &fieldToheadRotate = final_fieldToheadRotate; // rotation + + /** + * Head position + * translation part of headTofieldTransform + */ + const Vector3f &head_position = final_translation; + + /** + * True if head_position and the transformation matrices are up to date + * (false if this is not a visual step, or not enough elements are visible) + */ + const bool &is_uptodate = _is_uptodate; + + /** + * Number of simulation steps since last update (see is_uptodate) + * If (is_uptodate==true) then "steps_since_last_update" is zero + */ + const unsigned int &steps_since_last_update = _steps_since_last_update; + + /** + * Head z coordinate + * This variable is often equivalent to head_position.z, but sometimes it differs. + * There are situations in which the rotation and translation cannot be computed, + * but the z-coordinate can still be found through vision. + * It should be used in applications which rely on z as an independent coordinate, + * such as detecting if the robot has fallen, or as machine learning observations. + * It should not be used for 3D transformations. + */ + const float &head_z = final_z; + + /** + * Since head_z can be computed in situations where self-location is impossible, + * this variable is set to True when head_z is up to date + */ + const bool &is_head_z_uptodate = _is_head_z_uptodate; + + /** + * Transform relative to absolute coordinates using headTofieldTransform + * @return absolute coordinates + */ + Vector3f relativeToAbsoluteCoordinates(const Vector3f relativeCoordinates) const; + + /** + * Transform absolute to relative coordinates using fieldToheadTransform + * @return relative coordinates + */ + Vector3f absoluteToRelativeCoordinates(const Vector3f absoluteCoordinates) const; + + /** + * Get 3D velocity (based on last n 3D positions) + * @param n number of last positions to evaluate (min 1, max 9) + * Example for n=3: + * current position: p0 (current time step) + * last position: p1 (typically* 3 time steps ago) + * position before: p2 (typically* 6 time steps ago) + * position before: p3 (typically* 9 time steps ago) + * RETURN value: p0-p3 + * *Note: number of actual time steps depends on server configuration and whether + * the agent was able to self-locate on the last n visual steps + * @return 3D velocity vector + */ + Vector3f get_velocity(unsigned int n) const; + + /** + * Get last known head z coordinate + * Note: this variable is based on head_z. It can be used as an independent coordinate, + * but it should not be used for 3D transformations, as it may be out of sync with + * the x and y coordinates. (see head_z) + * @return last known head z coordinate + */ + float get_last_head_z() const {return last_z;} + + +private: + + //================================================================================================= + //============================================================================ main private methods + //================================================================================================= + + bool find_z_axis_orient_vec(); //returns true if successful + void fit_ground_plane(); + + void find_z(const Vector3f& Zvec); + bool find_xy(); + bool guess_xy(); + + bool fine_tune_aux(float &initial_angle, float &initial_x, float &initial_y, bool use_probabilities); + bool fine_tune(float initial_angle, float initial_x, float initial_y); + + static double map_error_logprob(const gsl_vector *v, void *params); + static double map_error_2d(const gsl_vector *v, void *params); + + void commit_everything(); + + + //================================================================================================= + //=================================================================== private transformation matrix + //================================================================================================= + + //PRELIMINARY MATRIX - where all operations are stored + //if the algorithm is not successful, the final matrix is not modified + void prelim_reset(); + Matrix4D prelimHeadToField = Matrix4D(); //initialized as identity matrix + + //FINAL MATRIX - the user has access to a public const reference of the variables below + Matrix4D final_headTofieldTransform; // rotation + translation + Matrix4D final_headTofieldRotate; // rotation + Matrix4D final_fieldToheadTransform; // rotation + translation + Matrix4D final_fieldToheadRotate; // rotation + + Vector3f final_translation; //translation + + float final_z; //independent z translation (may be updated more often) + + //================================================================================================= + //=============================================================================== useful statistics + //================================================================================================= + + std::array position_history; + unsigned int position_history_ptr = 0; + + float last_z = 0.5; + + unsigned int _steps_since_last_update = 0; + bool _is_uptodate = false; + bool _is_head_z_uptodate = false; + + //================================================================================================= + //================================================================================ debug statistics + //================================================================================================= + + int stats_sample_position_error(const Vector3f sample, const Vector3f& cheat, double error_placeholder[]); + void stats_reset(); + + double errorSum_fineTune_before[7] = {0}; //[0,1,2]- xyz err sum, [3]-2D err sum, [4]-2D err sq sum, [5]-3D err sum, [6]-3D err sq sum + double errorSum_fineTune_euclidianDist[7] = {0}; //[0,1,2]- xyz err sum, [3]-2D err sum, [4]-2D err sq sum, [5]-3D err sum, [6]-3D err sq sum + double errorSum_fineTune_probabilistic[7] = {0}; //[0,1,2]- xyz err sum, [3]-2D err sum, [4]-2D err sq sum, [5]-3D err sum, [6]-3D err sq sum + double errorSum_ball[7] = {0}; //[0,1,2]- xyz err sum, [3]-2D err sum, [4]-2D err sq sum, [5]-3D err sum, [6]-3D err sq sum + int counter_fineTune = 0; + int counter_ball = 0; + + enum STATE{NONE, RUNNING, MINFAIL, BLIND, FAILzNOgoal, FAILzLine, FAILz, FAILtune, FAILguessLine, FAILguessNone, FAILguessMany, FAILguessTest, DONE, ENUMSIZE}; + STATE state = NONE; + + void stats_change_state(enum STATE s); + int state_counter[STATE::ENUMSIZE] = {0}; + +}; + + +typedef Singleton SLocalizerV2; \ No newline at end of file diff --git a/cpp/localization/Makefile b/cpp/localization/Makefile new file mode 100644 index 0000000..d37469f --- /dev/null +++ b/cpp/localization/Makefile @@ -0,0 +1,15 @@ +src = $(wildcard *.cpp) +obj = $(src:.c=.o) + +LDFLAGS = -lgsl -lgslcblas +CFLAGS = -O3 -shared -std=c++11 -fPIC -Wall $(PYBIND_INCLUDES) + +all: $(obj) + g++ $(CFLAGS) -o localization.so $^ $(LDFLAGS) + +debug: $(filter-out lib_main.cpp,$(obj)) + g++ -O0 -std=c++14 -Wall -g -o debug.bin debug_main.cc $^ $(LDFLAGS) + +.PHONY: clean +clean: + rm -f $(obj) all diff --git a/cpp/localization/Matrix4D.cpp b/cpp/localization/Matrix4D.cpp new file mode 100644 index 0000000..217d08e --- /dev/null +++ b/cpp/localization/Matrix4D.cpp @@ -0,0 +1,303 @@ +#include "Matrix4D.h" + + + +Matrix4D::Matrix4D() { + // identity matrix + const float tmp[M_LENGTH] = { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + }; + + for (int i = 0; i < M_LENGTH; i++) + content[i] = tmp[i]; +} + +Matrix4D::Matrix4D(const float a[M_LENGTH]) { + // creates a matrix using a vector of floats + for (int i = 0; i < M_LENGTH; i++) + content[i] = a[i]; +} + +Matrix4D::Matrix4D(const Matrix4D& other) { + // creates a matrix using another matrix + for (int i = 0; i < M_LENGTH; i++) + content[i] = other.content[i]; +} + +Matrix4D::Matrix4D(const Vector3f& v) { + float x = v.getX(); + float y = v.getY(); + float z = v.getZ(); + + // gets a translation matrix from xyz coordinates + const float tmp[M_LENGTH] = { + 1, 0, 0, x, + 0, 1, 0, y, + 0, 0, 1, z, + 0, 0, 0, 1 + }; + + for (int i = 0; i < M_LENGTH; i++) + content[i] = tmp[i]; +} + +Matrix4D::~Matrix4D() { + // nothing to do +} + +void Matrix4D::set(unsigned i, float value) { + content[i] = value; +} + +void Matrix4D::set(unsigned i, unsigned j, float value) { + content[M_COLS * i + j] = value; +} + +float Matrix4D::get(unsigned i) const{ + return content[i]; +} + +float Matrix4D::get(unsigned i, unsigned j) const{ + return content[M_COLS * i + j]; +} + +Matrix4D Matrix4D::operator+(const Matrix4D& other) const { + // sums two matrices + float tmp[M_LENGTH]; + + for (int i = 0; i < M_LENGTH; i++) + tmp[i] = this->content[i] + other.content[i]; + + return Matrix4D(tmp); +} + +Matrix4D Matrix4D::operator-(const Matrix4D& other) const { + // subtracts a matrix from another + float tmp[M_LENGTH]; + + for (int i = 0; i < M_LENGTH; i++) + tmp[i] = this->content[i] - other.content[i]; + + return Matrix4D(tmp); +} + +Matrix4D Matrix4D::operator*(const Matrix4D& other) const { + + // multiplies two matrices + float tmp[M_LENGTH]; + + for (int i = 0; i < M_ROWS; i++) { + for (int j = 0; j < M_COLS; j++) { + tmp[M_COLS * i + j] = 0; + + for (int k = 0; k < M_COLS; k++) + tmp[M_COLS * i + j] += this->content[M_COLS * i + k] * other.content[M_COLS * k + j]; + } + } + + return Matrix4D(tmp); +} + +Vector3f Matrix4D::operator*(const Vector3f& vec) const { + // multiplies this matrix by a vector of four floats + // the first three are from vec and the remaining float is 1.0 + float x = 0; + float y = 0; + float z = 0; + + x = this->content[0] * vec.getX(); + x += this->content[1] * vec.getY(); + x += this->content[2] * vec.getZ(); + x += this->content[3]; + + y = this->content[4] * vec.getX(); + y += this->content[5] * vec.getY(); + y += this->content[6] * vec.getZ(); + y += this->content[7]; + + z = this->content[8] * vec.getX(); + z += this->content[9] * vec.getY(); + z += this->content[10] * vec.getZ(); + z += this->content[11]; + + return Vector3f(x, y, z); +} + +void Matrix4D::operator=(const Matrix4D& other) { + // assigns another matrix to this one + for (int i = 0; i < M_LENGTH; i++) + content[i] = other.content[i]; +} + +bool Matrix4D::operator==(const Matrix4D& other) const { + // checks whether this matrix is equal to another + for (int i = 0; i < M_LENGTH; i++) + if (content[i] != other.content[i]) + return false; + + return true; +} + +void Matrix4D::operator+=(const Matrix4D& other) { + // sums this matrix to another and returns the result + for (int i = 0; i < M_LENGTH; i++) + content[i] += other.content[i]; +} + +void Matrix4D::operator-=(const Matrix4D& other) { + // subtracts this matrix from another and returns the result + for (int i = 0; i < M_LENGTH; i++) + content[i] -= other.content[i]; +} + +float& Matrix4D::operator[](const unsigned pos) { + // gets a value from position + return content[pos]; +} + +Vector3f Matrix4D::toVector3f() const { + // gets the translation vector from the matrix + float x = get(0, M_COLS - 1); + float y = get(1, M_COLS - 1); + float z = get(2, M_COLS - 1); + + return Vector3f(x, y, z); +} + +Matrix4D Matrix4D::transpose() { + // returns the transpose of this matrix + Matrix4D result; + + for (int i = 0; i < M_ROWS; i++) + for (int j = 0; j < M_COLS; j++) + result.set(j, i, get(i, j)); + + return result; +} + +Matrix4D Matrix4D::inverse_tranformation_matrix() const { + + Matrix4D inv; //Initialized as identity matrix + inverse_tranformation_matrix(inv); + return inv; +} + +void Matrix4D::inverse_tranformation_matrix(Matrix4D& inv) const { + + //Rotation + inv[0] = content[0]; inv[1] = content[4]; inv[2] = content[8]; + inv[4] = content[1]; inv[5] = content[5]; inv[6] = content[9]; + inv[8] = content[2]; inv[9] = content[6]; inv[10] = content[10]; + + //Translation + inv[3] = -content[0]*content[3] - content[4]*content[7] - content[8]*content[11]; + inv[7] = -content[1]*content[3] - content[5]*content[7] - content[9]*content[11]; + inv[11] = -content[2]*content[3] - content[6]*content[7] - content[10]*content[11]; + +} + +bool Matrix4D::inverse(Matrix4D& inverse_out) const{ + // returns the inverse of this matrix + + float inv[16], det; + const float* m = content; + int i; + + inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10]; + inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10]; + inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9]; + inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9]; + inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10]; + inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10]; + inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9]; + inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9]; + inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6]; + inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6]; + inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5]; + inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5]; + inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - m[9] * m[2] * m[7] + m[9] * m[3] * m[6]; + inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + m[8] * m[2] * m[7] - m[8] * m[3] * m[6]; + inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5]; + inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5]; + + det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12]; + + if (det == 0) + return false; + + det = 1.0 / det; + + for (i = 0; i < 16; i++) + inverse_out.set(i, inv[i] * det); + + return true; +} + + +Matrix4D Matrix4D::rotationX(float angle) { + // rotates this matrix around the x-axis + const float tmp[M_LENGTH] = { + 1, 0, 0, 0, + 0, Cos(angle), -Sin(angle), 0, + 0, Sin(angle), Cos(angle), 0, + 0, 0, 0, 1 + }; + + return Matrix4D(tmp); +} + +Matrix4D Matrix4D::rotationY(float angle) { + // rotates this matrix around the y-axis + const float tmp[M_LENGTH] = { + Cos(angle), 0, Sin(angle), 0, + 0, 1, 0, 0, + -Sin(angle), 0, Cos(angle), 0, + 0, 0, 0, 1 + }; + + return Matrix4D(tmp); +} + +Matrix4D Matrix4D::rotationZ(float angle) { + // rotates this matrix around the z axis + const float tmp[M_LENGTH] = { + Cos(angle), -Sin(angle), 0, 0, + Sin(angle), Cos(angle), 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + }; + + return Matrix4D(tmp); +} + +Matrix4D Matrix4D::rotation(Vector3f axis, float angle) { + // assuming that axis is a unit vector + float x = axis.getX(); + float y = axis.getY(); + float z = axis.getZ(); + + const float tmp[M_LENGTH] = { + (x * x * (1 - Cos(angle)) + Cos(angle)), (x * y * (1 - Cos(angle)) - z * Sin(angle)), (x * z * (1 - Cos(angle)) + y * Sin(angle)), 0, + (x * y * (1 - Cos(angle)) + z * Sin(angle)), (y * y * (1 - Cos(angle)) + Cos(angle)), (y * z * (1 - Cos(angle)) - x * Sin(angle)), 0, + (x * z * (1 - Cos(angle)) - y * Sin(angle)), (y * z * (1 - Cos(angle)) + x * Sin(angle)), (z * z * (1 - Cos(angle)) + Cos(angle)), 0, + 0, 0, 0, 1 + }; + + return Matrix4D(tmp); +} + +Matrix4D Matrix4D::translation(float x, float y, float z) { + // gets a translation matrix from xyz coordinates + const float tmp[M_LENGTH] = { + 1, 0, 0, x, + 0, 1, 0, y, + 0, 0, 1, z, + 0, 0, 0, 1 + }; + + return Matrix4D(tmp); +} + diff --git a/cpp/localization/Matrix4D.h b/cpp/localization/Matrix4D.h new file mode 100644 index 0000000..927b6e4 --- /dev/null +++ b/cpp/localization/Matrix4D.h @@ -0,0 +1,224 @@ +#ifndef MATRIX4D_H_ +#define MATRIX4D_H_ + +#include "Vector3f.h" + +#define M_ROWS 4 +#define M_COLS 4 +#define M_LENGTH (M_ROWS * M_COLS) + +/** + * @class Matrix4D + * + * This class represents a 4x4 matrix and contains methods + * to operate on it + * + * @author Nuno Almeida (nuno.alm@ua.pt) + * Adapted - Miguel Abreu + */ +class Matrix4D { + +public: + float content[M_LENGTH]; // content of the matrix, vector-like + + /** + * Default constructor returns the identity matrix + */ + Matrix4D(); + + /** + * Constructor returns a matrix from a vector of floats + */ + Matrix4D(const float[]); + + /** + * Copy constructor + */ + Matrix4D(const Matrix4D& other); + + /** + * Constructor returns a translation matrix + */ + Matrix4D(const Vector3f& v); + + /** + * Destructor + */ + ~Matrix4D(); + + /** + * Sets a value in some position (vector-like) + */ + void set(unsigned i, float value); + + /** + * Sets a value in some position (matrix-like) + */ + void set(unsigned i, unsigned j, float value); + + /** + * Gets a value from some position (vector-like) + */ + float get(unsigned i) const; + + /** + * Gets a value from some position (matrix-like) + */ + float get(unsigned i, unsigned j) const; + + /** + * Assigns another matrix to this one + * + * @param other Another matrix + */ + void operator=(const Matrix4D& other); + + /** + * Gets the sum of another vector with this one + * + * @param other Another matrix + */ + Matrix4D operator+(const Matrix4D& other) const; + + /** + * Sums this matrix to another + * + * @param other Another matrix + * @return Sum of this matrix with another + */ + void operator+=(const Matrix4D& other); + + /** + * Gets the subtraction of another vector from this one + * + * @param other Another matrix + */ + Matrix4D operator-(const Matrix4D&) const; + + /** + * Subtracts another matrix from this one + * + * @param other Another matrix + * @return This matrix minus another + */ + void operator-=(const Matrix4D& other); + + /** + * Multiplies two matrices + * + * @param other Another matrix + * @return Multiplication matrix + */ + Matrix4D operator*(const Matrix4D& other) const; + + /** + * Multiplies a matrix with a vector + * + * @param other Another matrix + * @return Multiplication vector + */ + Vector3f operator*(const Vector3f& other) const; + + /** + * Checks whether this matrix is equal to another + * + * @param other Another matrix + * @return true/false + */ + bool operator==(const Matrix4D&) const; + + /** + * Gets the content of the position i (in vector representation) of this + * matrix + * + * @param pos Position + * @return Value in the position + */ + float& operator[](const unsigned pos); + + /** + * Gets the translation vector from this matrix + * + * @return Translation vector + */ + Vector3f toVector3f() const; + + /** + * Gets the transpose of this matrix + * + * @return Transpose + */ + Matrix4D transpose(); + + /** + * Gets the inverse of this matrix (m.abreu@2020) + * + * @param inverse_out inverse matrix + * @return true if it exists + */ + bool inverse(Matrix4D& inverse_out) const; + + /** + * Gets the inverse of this matrix, (m.abreu@2020) + * assuming that it represents an affine transformation with only translation and rotation + * This method creates a new matrix + * + * @return inverse matrix + */ + Matrix4D inverse_tranformation_matrix() const; + + /** + * Gets the inverse of this matrix, (m.abreu@2020) + * assuming that it represents an affine transformation with only translation and rotation + * This method overwrites the given matrix + * + * @param inverse_out inverse matrix output + */ + void inverse_tranformation_matrix(Matrix4D& inverse_out) const; + + + /** + * Gets the rotation matrix around x-axis + * + * @param angle Angle (degrees) + */ + static Matrix4D rotationX(float angle); + + /** + * Gets the rotation matrix around y-axis + * + * @param angle Angle (degrees) + */ + static Matrix4D rotationY(float angle); + + /** + * Gets the rotation matrix around z-axis + * + * @param angle Angle (degrees) + */ + static Matrix4D rotationZ(float angle); + + /** + * Gets the rotation matrix around an arbitrary axis + * + * @param axis Axis (x, y and z) + * @param angle Angle (degrees) + */ + static Matrix4D rotation(Vector3f axis, float angle); + + /** + * Gets the translation matrix + * + * @param x x-axis coordinate + * @param y y-axis coordinate + * @param z z-axis coordinate + */ + static Matrix4D translation(Vector3f v) { + return translation(v.getX(), v.getY(), v.getZ()); + } + static Matrix4D translation(float x, float y, float z); + + +}; + +#endif // MATRIX4D_H_ diff --git a/cpp/localization/RobovizLogger.cpp b/cpp/localization/RobovizLogger.cpp new file mode 100644 index 0000000..bf274d1 --- /dev/null +++ b/cpp/localization/RobovizLogger.cpp @@ -0,0 +1,131 @@ +#include +#include "RobovizLogger.h" +#include "robovizdraw.h" + +#define ROBOVIZ_HOST "localhost" +#define ROBOVIZ_PORT "32769" + + +RobovizLogger* RobovizLogger::Instance() { + static RobovizLogger instance; + return &instance; +} + +RobovizLogger::RobovizLogger() {} + +RobovizLogger::~RobovizLogger() { + destroy(); +} + +int RobovizLogger::init() { + if (is_initialized) return 0; + struct addrinfo hints; + int rv; + int numbytes; + + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + + if ((rv = getaddrinfo(ROBOVIZ_HOST, ROBOVIZ_PORT, &hints, &servinfo)) != 0) { + //fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); + return 1; + } + + // loop through all the results and make a socket + for (p = servinfo; p != NULL; p = p->ai_next) { + if ((sockfd = socket(p->ai_family, p->ai_socktype, + p->ai_protocol)) == -1) { + perror("socket"); + continue; + } + break; + } + + if (p == NULL) { + return 2; + } + + is_initialized = true; + return 0; +} + +void RobovizLogger::destroy() { + freeaddrinfo(servinfo); + servinfo=NULL; + close(sockfd); +} + +void RobovizLogger::swapBuffers(const string* setName) { + int bufSize = -1; + unsigned char* buf = newBufferSwap(setName, &bufSize); + sendto(sockfd, buf, bufSize, 0, p->ai_addr, p->ai_addrlen); + delete[] buf; +} + +void RobovizLogger::drawLine(float x1, float y1, float z1, float x2, float y2, float z2, + float thickness, float r, float g, float b, const string* setName) { + float pa[3] = {x1, y1, z1}; + float pb[3] = {x2, y2, z2}; + float color[3] = {r, g, b}; + + int bufSize = -1; + unsigned char* buf = newLine(pa, pb, thickness, color, setName, &bufSize); + sendto(sockfd, buf, bufSize, 0, p->ai_addr, p->ai_addrlen); + delete[] buf; +} + +void RobovizLogger::drawCircle(float x, float y, float radius, float thickness, + float r, float g, float b, const string* setName) { + float center[2] = {x, y}; + float color[3] = {r, g, b}; + + int bufSize = -1; + unsigned char* buf = newCircle(center, radius, thickness, color, setName, &bufSize); + sendto(sockfd, buf, bufSize, 0, p->ai_addr, p->ai_addrlen); + delete[] buf; +} + +void RobovizLogger::drawSphere(float x, float y, float z, float radius, + float r, float g, float b, const string* setName) { + float center[3] = {x, y, z}; + float color[3] = {r, g, b}; + + int bufSize = -1; + unsigned char* buf = newSphere(center, radius, color, setName, &bufSize); + sendto(sockfd, buf, bufSize, 0, p->ai_addr, p->ai_addrlen); + delete[] buf; +} + +void RobovizLogger::drawPoint(float x, float y, float z, float size, + float r, float g, float b, const string* setName) { + float center[3] = {x, y, z}; + float color[3] = {r, g, b}; + + int bufSize = -1; + unsigned char* buf = newPoint(center, size, color, setName, &bufSize); + sendto(sockfd, buf, bufSize, 0, p->ai_addr, p->ai_addrlen); + delete[] buf; +} + +void RobovizLogger::drawPolygon(const float* v, int numVerts, float r, float g, float b, + float a, const string* setName) { + float color[4] = {r, g, b, a}; + + int bufSize = -1; + unsigned char* buf = newPolygon(v, numVerts, color, setName, &bufSize); + sendto(sockfd, buf, bufSize, 0, p->ai_addr, p->ai_addrlen); + delete[] buf; +} + +void RobovizLogger::drawAnnotation(const string* text, float x, float y, float z, float r, + float g, float b, const string* setName) { + float color[3] = {r, g, b}; + float pos[3] = {x, y, z}; + + int bufSize = -1; + unsigned char* buf = newAnnotation(text, pos, color, setName, &bufSize); + sendto(sockfd, buf, bufSize, 0, p->ai_addr, p->ai_addrlen); + delete[] buf; +} + diff --git a/cpp/localization/RobovizLogger.h b/cpp/localization/RobovizLogger.h new file mode 100644 index 0000000..a858725 --- /dev/null +++ b/cpp/localization/RobovizLogger.h @@ -0,0 +1,58 @@ +/* + * RobovizLogger.h + * + * Created on: 2013/06/24 + * Author: Rui Ferreira + */ + +#ifndef _ROBOVIZLOGGER_H_ +#define _ROBOVIZLOGGER_H_ + +#define RobovizLoggerInstance RobovizLogger::Instance() + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class RobovizLogger { +private: + RobovizLogger(); + RobovizLogger(const RobovizLogger&); + RobovizLogger& operator=(const RobovizLogger&); + virtual ~RobovizLogger(); + bool is_initialized = false; + +public: + static RobovizLogger* Instance(); + + int init(); + void destroy(); + void swapBuffers(const std::string* setName); + + void drawLine(float x1, float y1, float z1, float x2, float y2, float z2, + float thickness, float r, float g, float b, const std::string* setName); + void drawCircle(float x, float y, float radius, float thickness, + float r, float g, float b, const std::string* setName); + void drawSphere(float x, float y, float z, float radius, + float r, float g, float b, const std::string* setName); + void drawPoint(float x, float y, float z, float size, + float r, float g, float b, const std::string* setName); + void drawPolygon(const float* v, int numVerts, float r, float g, float b, + float a, const std::string* setName); + void drawAnnotation(const std::string* text, float x, float y, float z, float r, + float g, float b, const std::string* setName); + +private: + int sockfd; + struct addrinfo* p; + struct addrinfo* servinfo; +}; + +#endif // _ROBOVIZLOGGER_H_ diff --git a/cpp/localization/Singleton.h b/cpp/localization/Singleton.h new file mode 100644 index 0000000..12bdf8e --- /dev/null +++ b/cpp/localization/Singleton.h @@ -0,0 +1,20 @@ +#ifndef SINGLETON_H +#define SINGLETON_H + +template +class Singleton { +public: + + static T& getInstance() { + static T instance; + return instance; + } + +private: + Singleton(); + ~Singleton(); + Singleton(Singleton const&); + Singleton& operator=(Singleton const&); +}; + +#endif // SINGLETON_H diff --git a/cpp/localization/Vector3f.cpp b/cpp/localization/Vector3f.cpp new file mode 100644 index 0000000..c317a7f --- /dev/null +++ b/cpp/localization/Vector3f.cpp @@ -0,0 +1,197 @@ +#include "Vector3f.h" + + +using namespace std; + +Vector3f::Vector3f() { + x = 0.0; + y = 0.0; + z = 0.0; +} + +Vector3f::Vector3f(float x, float y, float z) { + this->x = x; + this->y = y; + this->z = z; +} + +Vector3f::Vector3f(const Vector3f& other) { + x = other.x; + y = other.y; + z = other.z; +} + +Vector3f::Vector3f(const Vector& other) { + x = other.x; + y = other.y; + z = 0.0; +} + +Vector3f::~Vector3f() { +} + +float Vector3f::getX() const { + return x; +} + +void Vector3f::setX(float x) { + this->x = x; +} + +float Vector3f::getY() const { + return y; +} + +void Vector3f::setY(float y) { + this->y = y; +} + +float Vector3f::getZ() const { + return z; +} + +void Vector3f::setZ(float z) { + this->z = z; +} + +float Vector3f::operator[](const int index) const { + float val=0.0; + switch (index) { + case 0: val = x; + break; + case 1: val = y; + break; + case 2: val = z; + break; + } + return val; +} + +Vector3f Vector3f::operator+(const Vector3f& other) const { + return Vector3f(x + other.x, y + other.y, z + other.z); +} + +Vector3f Vector3f::operator-(const Vector3f& other) const { + return Vector3f(x - other.x, y - other.y, z - other.z); +} + +Vector3f Vector3f::operator-() const { + return Vector3f() - * this; +} + +Vector3f Vector3f::operator*(const Vector3f& other) const { + return Vector3f(x * other.x, y * other.y, z * other.z); +} + +Vector3f Vector3f::operator/(const Vector3f& other) const { + return Vector3f(x / other.x, y / other.y, z / other.z); +} + +bool Vector3f::operator==(const Vector3f& other) const { + return x == other.x && y == other.y && z == other.z; +} + +Vector3f Vector3f::operator/(float factor) const { + return Vector3f(x / factor, y / factor, z / factor); +} + +Vector3f Vector3f::operator+(float factor) const { + return Vector3f(x + factor, y + factor, z + factor); +} + +Vector3f Vector3f::operator%(float factor) const { + return Vector3f(fmod(x, factor), fmod(y, factor), fmod(z, factor)); +} + +Vector3f Vector3f::operator*(float factor) const { + return Vector3f(x * factor, y * factor, z * factor); +} + +Vector3f Vector3f::operator+=(const Vector3f& other) { + x += other.x; + y += other.y; + z += other.z; + + return *this; +} + +Vector3f Vector3f::operator+=(float factor) { + x += factor; + y += factor; + z += factor; + + return *this; +} + +Vector3f Vector3f::operator-=(const Vector3f& other) { + x -= other.x; + y -= other.y; + z -= other.z; + + return *this; +} + +Vector3f Vector3f::operator-=(float factor) { + x -= factor; + y -= factor; + z -= factor; + + return *this; +} + +Vector3f Vector3f::operator/=(float factor) { + x /= factor; + y /= factor; + z /= factor; + + return *this; +} + + +float Vector3f::innerProduct(const Vector3f& other) const { + return x * other.x + y * other.y + z * other.z; +} + +Vector3f Vector3f::crossProduct(const Vector3f& other) const { + Vector3f aux; + + aux.x = this->y * other.z - this->z * other.y; + aux.y = this->z * other.x - this->x * other.z; + aux.z = this->x * other.y - this->y * other.x; + + return aux; +} + +float Vector3f::length() const { + return sqrt(x * x + y * y + z * z); +} + + +Vector3f Vector3f::normalize(float len) const { + return (*this) * (len / this->length()); +} + +Vector3f Vector3f::toCartesian() const { + // x = distance + // y = theta + // z = phi + return Vector3f(x * Cos(z) * Cos(y), x * Cos(z) * Sin(y), x * Sin(z)); +} + +Vector3f Vector3f::toPolar() const { + return Vector3f(this->length(), // distance + ATan2(y, x), // theta + ATan2(z, sqrt(x * x + y * y))); // phi +} + +float Vector3f::dist(const Vector3f &other) const { + return (*this -other).length(); +} + +Vector Vector3f::to2d() const { + return Vector(x, y); +} + +Vector3f Vector3f::determineMidpoint(Vector3f a, Vector3f b) { + return (a+b)/2; /* m.abreu@2020 */ +} diff --git a/cpp/localization/Vector3f.h b/cpp/localization/Vector3f.h new file mode 100644 index 0000000..4dfd7c8 --- /dev/null +++ b/cpp/localization/Vector3f.h @@ -0,0 +1,187 @@ +#ifndef VECTOR3F_H +#define VECTOR3F_H + +#include +#include "Geometry.h" +#include +//! Describes a vector of three floats + +/*! + * \author Hugo Picado (hugopicado@ua.pt) + * \author Nuno Almeida (nuno.alm@ua.pt) + * Adapted - Miguel Abreu + */ +class Vector3f { +public: + //! Default constructor + Vector3f(); + + /*! + * Constructor + * + * \param x x-axis coordinate + * \param y y-axis coordinate + * \param z z-axis coordinate + */ + Vector3f(float x, float y, float z); + + //! Copy constructor + Vector3f(const Vector3f& other); + + Vector3f(const Vector& other); + + //! Destructor + ~Vector3f(); + + //! getX + float getX() const; + void setX(float x); + + //! getY + float getY() const; + void setY(float y); + + //! getZ + float getZ() const; + void setZ(float z); + + //! Access X Y Z through indexes 0 1 2 + float operator[](const int) const; + + //! Sums this vector to another + Vector3f operator+(const Vector3f& other) const; + + //! Subtracts another vector from this + Vector3f operator-(const Vector3f& other) const; + + //! Negates the vector + Vector3f operator-() const; + + //! Multiples this vector by another + Vector3f operator*(const Vector3f& other) const; + + //! Divides this vector by another + Vector3f operator/(const Vector3f& other) const; + + bool operator==(const Vector3f& other) const; + + /*! + * Multiples this vector by a scalar + * + * \param factor Scalar number + */ + Vector3f operator*(float factor) const; + + /*! + * Divides this vector by a scalar + * + * \param factor Scalar number + */ + Vector3f operator/(float factor) const; + + /*! + * Add this vector to a scalar + * + * \param factor Scalar number + */ + Vector3f operator+(float factor) const; + + /*! + * Integer remainder this vector by a scalar + * + * \param factor Scalar number + */ + Vector3f operator%(float factor) const; + + /** + * Sums this vector to another assuming the value of the result + * + * \param other Vector3f + */ + Vector3f operator+=(const Vector3f& other); + + /** + * Add this vector to a scalar assuming the value of the result + * + * \param factor Scalar number + */ + Vector3f operator+=(float factor); + + /** + * Subtracts other vector from this vector assuming the value of the result + * + * \param other Vector3f + */ + Vector3f operator-=(const Vector3f& other); + + /** + * Subtracts a scalar from this vector assuming the value of the result + * + * \param factor Scalar number + */ + Vector3f operator-=(float factor); + + /*! + * Divides this vector by a scalar assuming the value of the result + * + * \param factor Scalar number + */ + Vector3f operator/=(float factor); + + /*! + * Computes the inner product of this vector with another + * + * \param other Vector to compute the inner product + * \return Resultant vector + */ + float innerProduct(const Vector3f& other) const; + + /*! + * Computes the cross product of this vector with another + * + * \param other Vector to compute the cross product + * \return Resultant vector + */ + Vector3f crossProduct(const Vector3f& other) const; + + /*! + * Gets the length of the vector + * + * \return Length of the vector + */ + float length() const; + + /*! + * Normalizes the vector to an arbitrary length (default is 1) + * + * \param len Length + */ + Vector3f normalize(float len = 1) const; + /*! + * Converts the vector coordinates from polar to cartesian + * It is assumed that the vector has the angular coordinates in degrees + */ + Vector3f toCartesian() const; + + //! Converts the vector coordinates from cartesian to polar + Vector3f toPolar() const; + + //! Converts the 3d vector to a 2d vector + Vector to2d() const; + + //! Gets the distance between this vector and another + float dist(const Vector3f& other) const; + + /* !Determines the midpoint between 2 points in a 3D space + * + **/ + static Vector3f determineMidpoint(Vector3f a, Vector3f b); + +public: + float x; // x-axis coordinate + float y; // y-axis coordinate + float z; // z-axis coordinate + +}; + +#endif // VECTOR3F_H diff --git a/cpp/localization/World.h b/cpp/localization/World.h new file mode 100644 index 0000000..5a62b5f --- /dev/null +++ b/cpp/localization/World.h @@ -0,0 +1,56 @@ +/** + * FILENAME: World + * DESCRIPTION: World data from Python + * AUTHOR: Miguel Abreu (m.abreu@fe.up.pt) + * DATE: 2021 + */ + +#pragma once +#include "Vector3f.h" +#include "Singleton.h" +#include "Matrix4D.h" +#include "Line6f.h" +#include +#include + +using namespace std; + + +class World { + friend class Singleton; + +private: + + World(){}; + +public: + + //Feet variables: (0) left, (1) right + bool foot_touch[2]; // is foot touching ground + Vector3f foot_contact_rel_pos[2]; // foot_transform * translation(foot_contact_pt) + + bool ball_seen; + Vector3f ball_rel_pos_cart; + Vector3f ball_cheat_abs_cart_pos; + Vector3f my_cheat_abs_cart_pos; + + struct sLMark { + bool seen; + bool isCorner; + Vector3f pos; + Vector3f rel_pos; + }; + + sLMark landmark[8]; + + struct sLine { + Vector3f start, end; + sLine(const Vector3f& s, const Vector3f& e) : start(s), end(e) {}; + }; + + vector lines_polar; + + +}; + +typedef Singleton SWorld; \ No newline at end of file diff --git a/cpp/localization/debug_main.cc b/cpp/localization/debug_main.cc new file mode 100644 index 0000000..423a7f2 --- /dev/null +++ b/cpp/localization/debug_main.cc @@ -0,0 +1,196 @@ +#include +#include "Geometry.h" +#include "Vector3f.h" +#include "Matrix4D.h" +#include "FieldNoise.h" +#include "Line6f.h" +#include "World.h" +#include "Field.h" +#include "LocalizerV2.h" + +using namespace std; + +static LocalizerV2& loc = SLocalizerV2::getInstance(); + +void print_python_data(){ + + static World &world = SWorld::getInstance(); + + cout << "Foot touch: " << world.foot_touch[0] << " " << world.foot_touch[1] << endl; + cout << "LFoot contact rpos: " << world.foot_contact_rel_pos[0].x << " " << world.foot_contact_rel_pos[0].y << " " << world.foot_contact_rel_pos[0].z << endl; + cout << "RFoot contact rpos: " << world.foot_contact_rel_pos[1].x << " " << world.foot_contact_rel_pos[1].y << " " << world.foot_contact_rel_pos[1].z << endl; + cout << "Ball seen: " << world.ball_seen << endl; + cout << "Ball rpos cart: " << world.ball_rel_pos_cart.x << " " << world.ball_rel_pos_cart.y << " " << world.ball_rel_pos_cart.z << endl; + cout << "Ball cheat: " << world.ball_cheat_abs_cart_pos.x << " " << world.ball_cheat_abs_cart_pos.y << " " << world.ball_cheat_abs_cart_pos.z << endl; + cout << "Me cheat: " << world.my_cheat_abs_cart_pos.x << " " << world.my_cheat_abs_cart_pos.y << " " << world.my_cheat_abs_cart_pos.z << endl; + + for(int i=0; i<8; i++){ + cout << "Landmark " << i << ": " << + world.landmark[i].seen << " " << + world.landmark[i].isCorner << " " << + world.landmark[i].pos.x << " " << + world.landmark[i].pos.y << " " << + world.landmark[i].pos.z << " " << + world.landmark[i].rel_pos.x << " " << + world.landmark[i].rel_pos.y << " " << + world.landmark[i].rel_pos.z << endl; + } + + for(int i=0; i +#include "Geometry.h" +#include "Vector3f.h" +#include "Matrix4D.h" +#include "FieldNoise.h" +#include "Line6f.h" +#include "World.h" +#include "Field.h" +#include "LocalizerV2.h" +#include +#include + +namespace py = pybind11; +using namespace std; + +static LocalizerV2& loc = SLocalizerV2::getInstance(); + +void print_python_data(){ + + static World &world = SWorld::getInstance(); + + cout << "Foot touch: " << world.foot_touch[0] << " " << world.foot_touch[1] << endl; + cout << "LFoot contact rpos: " << world.foot_contact_rel_pos[0].x << " " << world.foot_contact_rel_pos[0].y << " " << world.foot_contact_rel_pos[0].z << endl; + cout << "RFoot contact rpos: " << world.foot_contact_rel_pos[1].x << " " << world.foot_contact_rel_pos[1].y << " " << world.foot_contact_rel_pos[1].z << endl; + cout << "Ball seen: " << world.ball_seen << endl; + cout << "Ball rpos cart: " << world.ball_rel_pos_cart.x << " " << world.ball_rel_pos_cart.y << " " << world.ball_rel_pos_cart.z << endl; + cout << "Ball cheat: " << world.ball_cheat_abs_cart_pos.x << " " << world.ball_cheat_abs_cart_pos.y << " " << world.ball_cheat_abs_cart_pos.z << endl; + cout << "Me cheat: " << world.my_cheat_abs_cart_pos.x << " " << world.my_cheat_abs_cart_pos.y << " " << world.my_cheat_abs_cart_pos.z << endl; + + for(int i=0; i<8; i++){ + cout << "Landmark " << i << ": " << + world.landmark[i].seen << " " << + world.landmark[i].isCorner << " " << + world.landmark[i].pos.x << " " << + world.landmark[i].pos.y << " " << + world.landmark[i].pos.z << " " << + world.landmark[i].rel_pos.x << " " << + world.landmark[i].rel_pos.y << " " << + world.landmark[i].rel_pos.z << endl; + } + + for(int i=0; i compute( + bool lfoot_touch, bool rfoot_touch, + py::array_t feet_contact, + bool ball_seen, py::array_t ball_pos, + py::array_t me_pos, + py::array_t landmarks, + py::array_t lines){ + + // ================================================= 1. Parse data + + static World &world = SWorld::getInstance(); + world.foot_touch[0] = lfoot_touch; + world.foot_touch[1] = rfoot_touch; + + //Structure of feet_contact {lfoot_contact_pt, rfoot_contact_pt, lfoot_contact_rel_pos, rfoot_contact_rel_pos} + + py::buffer_info feet_contact_buf = feet_contact.request(); + double *feet_contact_ptr = (double *) feet_contact_buf.ptr; + world.foot_contact_rel_pos[0].x = feet_contact_ptr[0]; + world.foot_contact_rel_pos[0].y = feet_contact_ptr[1]; + world.foot_contact_rel_pos[0].z = feet_contact_ptr[2]; + world.foot_contact_rel_pos[1].x = feet_contact_ptr[3]; + world.foot_contact_rel_pos[1].y = feet_contact_ptr[4]; + world.foot_contact_rel_pos[1].z = feet_contact_ptr[5]; + + world.ball_seen = ball_seen; + + //Structure of ball_pos {ball_rel_pos_cart, ball_cheat_abs_cart_pos} + + py::buffer_info ball_pos_buf = ball_pos.request(); + double *ball_pos_ptr = (double *) ball_pos_buf.ptr; + world.ball_rel_pos_cart.x = ball_pos_ptr[0]; + world.ball_rel_pos_cart.y = ball_pos_ptr[1]; + world.ball_rel_pos_cart.z = ball_pos_ptr[2]; + world.ball_cheat_abs_cart_pos.x = ball_pos_ptr[3]; + world.ball_cheat_abs_cart_pos.y = ball_pos_ptr[4]; + world.ball_cheat_abs_cart_pos.z = ball_pos_ptr[5]; + + py::buffer_info me_pos_buf = me_pos.request(); + double *me_pos_ptr = (double *) me_pos_buf.ptr; + world.my_cheat_abs_cart_pos.x = me_pos_ptr[0]; + world.my_cheat_abs_cart_pos.y = me_pos_ptr[1]; + world.my_cheat_abs_cart_pos.z = me_pos_ptr[2]; + + py::buffer_info landmarks_buf = landmarks.request(); + double *landmarks_ptr = (double *) landmarks_buf.ptr; + + for(int i=0; i<8; i++){ + world.landmark[i].seen = (bool) landmarks_ptr[0]; + world.landmark[i].isCorner = (bool) landmarks_ptr[1]; + world.landmark[i].pos.x = landmarks_ptr[2]; + world.landmark[i].pos.y = landmarks_ptr[3]; + world.landmark[i].pos.z = landmarks_ptr[4]; + world.landmark[i].rel_pos.x = landmarks_ptr[5]; + world.landmark[i].rel_pos.y = landmarks_ptr[6]; + world.landmark[i].rel_pos.z = landmarks_ptr[7]; + landmarks_ptr += 8; + } + + py::buffer_info lines_buf = lines.request(); + int lines_len = lines_buf.shape[0]; + double *lines_ptr = (double *) lines_buf.ptr; + world.lines_polar.clear(); + + for(int i=0; i retval = py::array_t(35); //allocate + py::buffer_info buff = retval.request(); + float *ptr = (float *) buff.ptr; + + for(int i=0; i<16; i++){ + ptr[i] = loc.headTofieldTransform.content[i]; + } + ptr += 16; + for(int i=0; i<16; i++){ + ptr[i] = loc.fieldToheadTransform.content[i]; + } + ptr += 16; + + ptr[0] = (float) loc.is_uptodate; + ptr[1] = loc.head_z; + ptr[2] = (float) loc.is_head_z_uptodate; + + + return retval; +} + +void print_report(){ + loc.print_report(); +} + +void draw_visible_elements(bool is_right_side){ + Field& fd = SField::getInstance(); + fd.draw_visible(loc.headTofieldTransform, is_right_side); +} + + +using namespace pybind11::literals; //to add informative argument names as -> "argname"_a + +PYBIND11_MODULE(localization, m) { //the python module name, m is the interface to create bindings + m.doc() = "Probabilistic 6D localization algorithm"; // optional module docstring + + //optional arguments names + m.def("compute", &compute, "Compute the 6D pose based on visual information and return transformation matrices and other relevant data", + "lfoot_touch"_a, + "rfoot_touch"_a, + "feet_contact"_a, + "ball_seen"_a, + "ball_pos"_a, + "me_pos"_a, + "landmarks"_a, + "lines"_a); + + m.def("print_python_data", &print_python_data, "Print data received from Python"); + m.def("print_report", &print_report, "Print localization report"); + m.def("draw_visible_elements", &draw_visible_elements, "Draw all visible elements in RoboViz", "is_right_side"_a); + +} + diff --git a/cpp/localization/robovizdraw.h b/cpp/localization/robovizdraw.h new file mode 100644 index 0000000..fe0f49d --- /dev/null +++ b/cpp/localization/robovizdraw.h @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2011 Justin Stoecker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _ROBOVIZDRAW_H_ +#define _ROBOVIZDRAW_H_ + +#include +#include +#include + +using namespace std; + +inline int writeCharToBuf(unsigned char* buf, unsigned char value) { + *buf = value; + return 1; +} + +inline int writeFloatToBuf(unsigned char* buf, float value) { + char temp[20]; + sprintf(temp, "%6f", value); + memcpy(buf, temp, 6); + return 6; +} + +inline int writeColorToBuf(unsigned char* buf, const float* color, int channels) { + int i; + for (i = 0; i < channels; i++) + writeCharToBuf(buf + i, (unsigned char) (color[i]*255)); + return i; +} + +inline int writeStringToBuf(unsigned char* buf, const string* text) { + long i = 0; + if (text != NULL) + i += text->copy((char*) buf + i, text->length(), 0); + i += writeCharToBuf(buf + i, 0); + return i; +} + +unsigned char* newBufferSwap(const string* name, int* bufSize) { + *bufSize = 3 + ((name != NULL) ? name->length() : 0); + unsigned char* buf = new unsigned char[*bufSize]; + + long i = 0; + i += writeCharToBuf(buf + i, 0); + i += writeCharToBuf(buf + i, 0); + i += writeStringToBuf(buf + i, name); + + return buf; +} + +unsigned char* newCircle(const float* center, float radius, float thickness, + const float* color, const string* setName, int* bufSize) { + + *bufSize = 30 + ((setName != NULL) ? setName->length() : 0); + unsigned char* buf = new unsigned char[*bufSize]; + + long i = 0; + i += writeCharToBuf(buf + i, 1); + i += writeCharToBuf(buf + i, 0); + i += writeFloatToBuf(buf + i, center[0]); + i += writeFloatToBuf(buf + i, center[1]); + i += writeFloatToBuf(buf + i, radius); + i += writeFloatToBuf(buf + i, thickness); + i += writeColorToBuf(buf + i, color, 3); + i += writeStringToBuf(buf + i, setName); + + return buf; +} + +unsigned char* newLine(const float* a, const float* b, float thickness, + const float* color, const string* setName, int* bufSize) { + + *bufSize = 48 + ((setName != NULL) ? setName->length() : 0); + unsigned char* buf = new unsigned char[*bufSize]; + + long i = 0; + i += writeCharToBuf(buf + i, 1); + i += writeCharToBuf(buf + i, 1); + i += writeFloatToBuf(buf + i, a[0]); + i += writeFloatToBuf(buf + i, a[1]); + i += writeFloatToBuf(buf + i, a[2]); + i += writeFloatToBuf(buf + i, b[0]); + i += writeFloatToBuf(buf + i, b[1]); + i += writeFloatToBuf(buf + i, b[2]); + i += writeFloatToBuf(buf + i, thickness); + i += writeColorToBuf(buf + i, color, 3); + i += writeStringToBuf(buf + i, setName); + + return buf; +} + +unsigned char* newPoint(const float* p, float size, const float* color, + const string* setName, int* bufSize) { + + *bufSize = 30 + ((setName != NULL) ? setName->length() : 0); + unsigned char* buf = new unsigned char[*bufSize]; + + long i = 0; + i += writeCharToBuf(buf + i, 1); + i += writeCharToBuf(buf + i, 2); + i += writeFloatToBuf(buf + i, p[0]); + i += writeFloatToBuf(buf + i, p[1]); + i += writeFloatToBuf(buf + i, p[2]); + i += writeFloatToBuf(buf + i, size); + i += writeColorToBuf(buf + i, color, 3); + i += writeStringToBuf(buf + i, setName); + + return buf; +} + +unsigned char* newSphere(const float* p, float radius, const float* color, + const string* setName, int* bufSize) { + + *bufSize = 30 + ((setName != NULL) ? setName->length() : 0); + unsigned char* buf = new unsigned char[*bufSize]; + + long i = 0; + i += writeCharToBuf(buf + i, 1); + i += writeCharToBuf(buf + i, 3); + i += writeFloatToBuf(buf + i, p[0]); + i += writeFloatToBuf(buf + i, p[1]); + i += writeFloatToBuf(buf + i, p[2]); + i += writeFloatToBuf(buf + i, radius); + i += writeColorToBuf(buf + i, color, 3); + i += writeStringToBuf(buf + i, setName); + + return buf; +} + +unsigned char* newPolygon(const float* v, int numVerts, const float* color, + const string* setName, int* bufSize) { + + *bufSize = 18 * numVerts + 8 + ((setName != NULL) ? setName->length() : 0); + unsigned char* buf = new unsigned char[*bufSize]; + + long i = 0; + i += writeCharToBuf(buf + i, 1); + i += writeCharToBuf(buf + i, 4); + i += writeCharToBuf(buf + i, numVerts); + i += writeColorToBuf(buf + i, color, 4); + + for (int j = 0; j < numVerts; j++) { + i += writeFloatToBuf(buf + i, v[j * 3 + 0]); + i += writeFloatToBuf(buf + i, v[j * 3 + 1]); + i += writeFloatToBuf(buf + i, v[j * 3 + 2]); + } + + i += writeStringToBuf(buf + i, setName); + + return buf; +} + +unsigned char* newAnnotation(const string* text, const float* p, + const float* color, const string* setName, int* bufSize) { + + *bufSize = 25 + text->length() + ((setName != NULL) ? setName->length() : 0); + unsigned char* buf = new unsigned char[*bufSize]; + + long i = 0; + i += writeCharToBuf(buf + i, 2); + i += writeCharToBuf(buf + i, 0); + i += writeFloatToBuf(buf + i, p[0]); + i += writeFloatToBuf(buf + i, p[1]); + i += writeFloatToBuf(buf + i, p[2]); + i += writeColorToBuf(buf + i, color, 3); + i += writeStringToBuf(buf + i, text); + i += writeStringToBuf(buf + i, setName); + + return buf; +} + +unsigned char* newAgentAnnotation(const string* text, bool leftTeam, + int agentNum, const float* color, int* bufSize) { + + *bufSize = (text == NULL) ? 3 : 7 + text->length(); + unsigned char* buf = new unsigned char[*bufSize]; + + long i = 0; + i += writeCharToBuf(buf + i, 2); + + if (text == NULL) { + i += writeCharToBuf(buf + i, 2); + i += writeCharToBuf(buf + i, (leftTeam ? agentNum - 1 : agentNum + 127)); + } else { + i += writeCharToBuf(buf + i, 1); + i += writeCharToBuf(buf + i, (leftTeam ? agentNum - 1 : agentNum + 127)); + i += writeColorToBuf(buf + i, color, 3); + i += writeStringToBuf(buf + i, text); + } + + return buf; +} + +#endif diff --git a/example.py b/example.py new file mode 100644 index 0000000..15ece14 --- /dev/null +++ b/example.py @@ -0,0 +1,30 @@ +from agent.Base_Agent import Base_Agent as Agent +from math_ops.Math_Ops import Math_Ops as M +from scripts.commons.Script import Script + +script = Script() +a = script.args + +# Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name +player = Agent(a.i, a.p, a.m, a.u, a.r, a.t, enable_draw=True) +w = player.world +player.scom.unofficial_beam((-3,0,w.robot.beam_height), 0) +getting_up = False +while True: + player_2d = w.robot.loc_head_position[:2] + ball_2d = w.ball_abs_pos[:2] + goal_dir = M.vector_angle( (15,0)-player_2d ) # Goal direction + if player.behavior.is_ready("Get_Up") or getting_up: + getting_up = not player.behavior.execute("Get_Up") # True on completion + else: + if ball_2d[0] > 0: # kick if ball is on opponent's side (x>0) + player.behavior.execute("Basic_Kick", goal_dir) + elif M.distance_point_to_segment(player_2d,ball_2d, ball_2d+ M.normalize_vec( ball_2d-(15,0) ) ) > 0.1: # not aligned + next_pos, next_ori, dist = player.path_manager.get_path_to_ball(x_ori=goal_dir, x_dev=-0.3, torso_ori=goal_dir) + player.behavior.execute("Walk", next_pos, True, next_ori, True, dist) + else: # Robot is aligned + player.behavior.execute("Walk", (15,0), True, goal_dir, True, 0.5) + player.scom.commit_and_send( w.robot.get_command() ) + player.scom.receive() +w.draw.annotation((*player_2d,0.6),"Hello!",w.draw.Color.white,"my_info",flush=False) +w.draw.line(player_2d, ball_2d, 3, w.draw.Color.yellow, "my_info", flush=True) \ No newline at end of file diff --git a/kill.sh b/kill.sh new file mode 100755 index 0000000..c941e1f --- /dev/null +++ b/kill.sh @@ -0,0 +1,2 @@ +#!/bin/bash +pkill -9 -e -f "python3 ./Run_" diff --git a/logs/Logger.py b/logs/Logger.py new file mode 100644 index 0000000..2eb3152 --- /dev/null +++ b/logs/Logger.py @@ -0,0 +1,48 @@ +from pathlib import Path +from datetime import datetime +import random +from string import ascii_uppercase + +class Logger(): + _folder = None + + def __init__(self, is_enabled:bool, topic:str) -> None: + self.no_of_entries = 0 + self.enabled = is_enabled + self.topic = topic + + def write(self, msg:str, timestamp:bool=True, step:int=None) -> None: + ''' + Write `msg` to file named `self.topic` + + Parameters + ---------- + msg : str + message to be written + step : int + simulation step is written before the message to provide additional information + default is `None` (nothing is written before the message) + ''' + if not self.enabled: return + + # The log folder is only created if needed + if Logger._folder is None: + rnd = ''.join(random.choices(ascii_uppercase, k=6)) # Useful if multiple processes are running in parallel + Logger._folder = "./logs/" + datetime.now().strftime("%Y-%m-%d_%H.%M.%S__") + rnd + "/" + print("\nLogger Info: see",Logger._folder) + Path(Logger._folder).mkdir(parents=True, exist_ok=True) + + self.no_of_entries += 1 + + with open(Logger._folder + self.topic + ".log", 'a+') as f: + prefix = "" + write_step = step is not None + if timestamp or write_step: + prefix = "{" + if timestamp: + prefix += datetime.now().strftime("%a %H:%M:%S") + if write_step: prefix += " " + if write_step: + prefix += f'Step:{step}' + prefix += "} " + f.write(prefix + msg + "\n") \ No newline at end of file diff --git a/math_ops/Inverse_Kinematics.py b/math_ops/Inverse_Kinematics.py new file mode 100644 index 0000000..02446e6 --- /dev/null +++ b/math_ops/Inverse_Kinematics.py @@ -0,0 +1,242 @@ +from math import asin, atan, atan2, pi, sqrt +from math_ops.Matrix_3x3 import Matrix_3x3 +from math_ops.Math_Ops import Math_Ops as M +import numpy as np + +class Inverse_Kinematics(): + + # leg y deviation, upper leg height, upper leg depth, lower leg length, knee extra angle, max ankle z + NAO_SPECS_PER_ROBOT = ((0.055, 0.12, 0.005, 0.1, atan(0.005/0.12), -0.091), + (0.055, 0.13832, 0.005, 0.11832, atan(0.005/0.13832), -0.106), + (0.055, 0.12, 0.005, 0.1, atan(0.005/0.12), -0.091), + (0.072954143,0.147868424, 0.005, 0.127868424, atan(0.005/0.147868424), -0.114), + (0.055, 0.12, 0.005, 0.1, atan(0.005/0.12), -0.091)) + + TORSO_HIP_Z = 0.115 # distance in the z-axis, between the torso and each hip (same for all robots) + TORSO_HIP_X = 0.01 # distance in the x-axis, between the torso and each hip (same for all robots) (hip is 0.01m to the back) + + def __init__(self, robot) -> None: + self.robot = robot + self.NAO_SPECS = Inverse_Kinematics.NAO_SPECS_PER_ROBOT[robot.type] + + def torso_to_hip_transform(self, coords, is_batch=False): + ''' + Convert cartesian coordinates that are relative to torso to coordinates that are relative the center of both hip joints + + Parameters + ---------- + coords : array_like + One 3D position or list of 3D positions + is_batch : `bool` + Indicates if coords is a batch of 3D positions + + Returns + ------- + coord : `list` or ndarray + A numpy array is returned if is_batch is False, otherwise, a list of arrays is returned + ''' + if is_batch: + return [c + (Inverse_Kinematics.TORSO_HIP_X, 0, Inverse_Kinematics.TORSO_HIP_Z) for c in coords] + else: + return coords + (Inverse_Kinematics.TORSO_HIP_X, 0, Inverse_Kinematics.TORSO_HIP_Z) + + + def head_to_hip_transform(self, coords, is_batch=False): + ''' + Convert cartesian coordinates that are relative to head to coordinates that are relative the center of both hip joints + + Parameters + ---------- + coords : array_like + One 3D position or list of 3D positions + is_batch : `bool` + Indicates if coords is a batch of 3D positions + + Returns + ------- + coord : `list` or ndarray + A numpy array is returned if is_batch is False, otherwise, a list of arrays is returned + ''' + coords_rel_torso = self.robot.head_to_body_part_transform( "torso", coords, is_batch ) + return self.torso_to_hip_transform(coords_rel_torso, is_batch) + + def get_body_part_pos_relative_to_hip(self, body_part_name): + ''' Get body part position relative to the center of both hip joints ''' + bp_rel_head = self.robot.body_parts[body_part_name].transform.get_translation() + return self.head_to_hip_transform(bp_rel_head) + + def get_ankle_pos_relative_to_hip(self, is_left): + ''' Internally calls get_body_part_pos_relative_to_hip() ''' + return self.get_body_part_pos_relative_to_hip("lankle" if is_left else "rankle") + + def get_linear_leg_trajectory(self, is_left:bool, p1, p2=None, foot_ori3d=(0,0,0), dynamic_pose:bool=True, resolution=100): + ''' + Compute leg trajectory so that the ankle moves linearly between two 3D points (relative to hip) + + Parameters + ---------- + is_left : `bool` + set to True to select left leg, False to select right leg + p1 : array_like, length 3 + if p2 is None: + p1 is the target position (relative to hip), and the initial point is given by the ankle's current position + if p2 is not None: + p1 is the initial point (relative to hip) + p2 : array_like, length 3 / `None` + target position (relative to hip) or None (see p1) + foot_ori3d : array_like, length 3 + rotation around x,y,z (rotation around x & y are biases, relative to a vertical pose, or dynamic pose, if enabled) + dynamic_pose : `bool` + enable dynamic feet rotation to be parallel to the ground, based on IMU + resolution : int + interpolation resolution; more resolution is always better, but it takes more time to compute; + having more points does not make the movement slower, because if there are excessive points they are removed + during the analytical optimization + + Returns + ------- + trajecory : `tuple` + indices, [[values_1,error_codes_1], [values_2,error_codes_2], ...] + See leg() for further details + ''' + + if p2 is None: + p2 = np.asarray(p1, float) + p1 = self.get_body_part_pos_relative_to_hip('lankle' if is_left else 'rankle') + else: + p1 = np.asarray(p1, float) + p2 = np.asarray(p2, float) + + vec = (p2 - p1) / resolution + + + hip_points = [p1 + vec * i for i in range(1,resolution+1)] + interpolation = [self.leg(p, foot_ori3d, is_left, dynamic_pose) for p in hip_points] + + indices = [2,4,6,8,10,12] if is_left else [3,5,7,9,11,13] + + last_joint_values = self.robot.joints_position[indices[0:4]] #exclude feet joints to compute ankle trajectory + next_step = interpolation[0] + trajectory = [] + + for p in interpolation[1:-1]: + if np.any(np.abs(p[1][0:4]-last_joint_values) > 7.03): + trajectory.append(next_step[1:3]) + last_joint_values = next_step[1][0:4] + next_step = p + else: + next_step = p + + trajectory.append(interpolation[-1][1:3]) + + return indices, trajectory + + + + def leg(self, ankle_pos3d, foot_ori3d, is_left:bool, dynamic_pose:bool): + ''' + Compute inverse kinematics for the leg, considering as input the relative 3D position of the ankle and 3D orientation* of the foot + *the yaw can be controlled directly, but the pitch and roll are biases (see below) + + Parameters + ---------- + ankle_pos3d : array_like, length 3 + (x,y,z) position of ankle in 3D, relative to the center of both hip joints + foot_ori3d : array_like, length 3 + rotation around x,y,z (rotation around x & y are biases, relative to a vertical pose, or dynamic pose, if enabled) + is_left : `bool` + set to True to select left leg, False to select right leg + dynamic_pose : `bool` + enable dynamic feet rotation to be parallel to the ground, based on IMU + + Returns + ------- + indices : `list` + indices of computed joints + values : `list` + values of computed joints + error_codes : `list` + list of error codes + Error codes: + (-1) Foot is too far (unreachable) + (x) Joint x is out of range + ''' + + error_codes = [] + leg_y_dev, upper_leg_height, upper_leg_depth, lower_leg_len, knee_extra_angle, _ = self.NAO_SPECS + sign = -1 if is_left else 1 + + # Then we translate to origin of leg by shifting the y coordinate + ankle_pos3d = np.asarray(ankle_pos3d) + (0,sign*leg_y_dev,0) + + # First we rotate the leg, then we rotate the coordinates to abstract from the rotation + ankle_pos3d = Matrix_3x3().rotate_z_deg(-foot_ori3d[2]).multiply(ankle_pos3d) + + # Use geometric solution to compute knee angle and foot pitch + dist = np.linalg.norm(ankle_pos3d) #dist hip <-> ankle + sq_dist = dist * dist + sq_upper_leg_h = upper_leg_height * upper_leg_height + sq_lower_leg_l = lower_leg_len * lower_leg_len + sq_upper_leg_l = upper_leg_depth * upper_leg_depth + sq_upper_leg_h + upper_leg_len = sqrt(sq_upper_leg_l) + knee = M.acos((sq_upper_leg_l + sq_lower_leg_l - sq_dist)/(2 * upper_leg_len * lower_leg_len)) + knee_extra_angle # Law of cosines + foot = M.acos((sq_lower_leg_l + sq_dist - sq_upper_leg_l)/(2 * lower_leg_len * dist)) # foot perpendicular to vec(origin->ankle_pos) + + # Check if target is reachable + if dist > upper_leg_len + lower_leg_len: + error_codes.append(-1) + + # Knee and foot + knee_angle = pi - knee + foot_pitch = foot - atan(ankle_pos3d[0] / np.linalg.norm(ankle_pos3d[1:3])) + foot_roll = atan(ankle_pos3d[1] / min(-0.05, ankle_pos3d[2])) * -sign # avoid instability of foot roll (not relevant above -0.05m) + + # Raw hip angles if all joints were straightforward + raw_hip_yaw = foot_ori3d[2] + raw_hip_pitch = foot_pitch - knee_angle + raw_hip_roll = -sign * foot_roll + + # Rotate 45deg due to yaw joint orientation, then rotate yaw, roll and pitch + m = Matrix_3x3().rotate_y_rad(raw_hip_pitch).rotate_x_rad(raw_hip_roll).rotate_z_deg(raw_hip_yaw).rotate_x_deg(-45*sign) + + # Get actual hip angles considering the yaw joint orientation + hip_roll = (pi/4) - (sign * asin(m.m[1,2])) #Add pi/4 due to 45deg rotation + hip_pitch = - atan2(m.m[0,2],m.m[2,2]) + hip_yaw = sign * atan2(m.m[1,0],m.m[1,1]) + + # Convert rad to deg + values = np.array([hip_yaw,hip_roll,hip_pitch,-knee_angle,foot_pitch,foot_roll]) * 57.2957795 #rad to deg + + # Set feet rotation bias (based on vertical pose, or dynamic_pose) + values[4] -= foot_ori3d[1] + values[5] -= foot_ori3d[0] * sign + + indices = [2,4,6,8,10,12] if is_left else [3,5,7,9,11,13] + + if dynamic_pose: + + # Rotation of torso in relation to foot + m : Matrix_3x3 = Matrix_3x3.from_rotation_deg((self.robot.imu_torso_roll, self.robot.imu_torso_pitch, 0)) + m.rotate_z_deg(foot_ori3d[2], True) + + roll = m.get_roll_deg() + pitch = m.get_pitch_deg() + + # Simple balance algorithm + correction = 1 #correction to motivate a vertical torso (in degrees) + roll = 0 if abs(roll) < correction else roll - np.copysign(correction,roll) + pitch = 0 if abs(pitch) < correction else pitch - np.copysign(correction,pitch) + + values[4] += pitch + values[5] += roll * sign + + + # Check and limit range of joints + for i in range(len(indices)): + if values[i] < self.robot.joints_info[indices[i]].min or values[i] > self.robot.joints_info[indices[i]].max: + error_codes.append(indices[i]) + values[i] = np.clip(values[i], self.robot.joints_info[indices[i]].min, self.robot.joints_info[indices[i]].max) + + + return indices, values, error_codes + diff --git a/math_ops/Math_Ops.py b/math_ops/Math_Ops.py new file mode 100644 index 0000000..ddbf4f3 --- /dev/null +++ b/math_ops/Math_Ops.py @@ -0,0 +1,361 @@ +from math import acos, asin, atan2, cos, pi, sin, sqrt +import numpy as np +import sys + +try: + GLOBAL_DIR = sys._MEIPASS # temporary folder with libs & data files +except: + GLOBAL_DIR = "." + + +class Math_Ops(): + ''' + This class provides general mathematical operations that are not directly available through numpy + ''' + + @staticmethod + def deg_sph2cart(spherical_vec): + ''' Converts SimSpark's spherical coordinates in degrees to cartesian coordinates ''' + r = spherical_vec[0] + h = spherical_vec[1] * pi / 180 + v = spherical_vec[2] * pi / 180 + return np.array([r * cos(v) * cos(h), r * cos(v) * sin(h), r * sin(v)]) + + @staticmethod + def deg_sin(deg_angle): + ''' Returns sin of degrees ''' + return sin(deg_angle * pi / 180) + + @staticmethod + def deg_cos(deg_angle): + ''' Returns cos of degrees ''' + return cos(deg_angle * pi / 180) + + @staticmethod + def to_3d(vec_2d, value=0) -> np.ndarray: + ''' Returns new 3d vector from 2d vector ''' + return np.append(vec_2d,value) + + @staticmethod + def to_2d_as_3d(vec_3d) -> np.ndarray: + ''' Returns new 3d vector where the 3rd dimension is zero ''' + vec_2d_as_3d = np.copy(vec_3d) + vec_2d_as_3d[2] = 0 + return vec_2d_as_3d + + @staticmethod + def normalize_vec(vec) -> np.ndarray: + ''' Divides vector by its length ''' + size = np.linalg.norm(vec) + if size == 0: return vec + return vec / size + + @staticmethod + def get_active_directory(dir:str) -> str: + global GLOBAL_DIR + return GLOBAL_DIR + dir + + @staticmethod + def acos(val): + ''' arccosine function that limits input ''' + return acos( np.clip(val,-1,1) ) + + @staticmethod + def asin(val): + ''' arcsine function that limits input ''' + return asin( np.clip(val,-1,1) ) + + @staticmethod + def normalize_deg(val): + ''' normalize val in range [-180,180[ ''' + return (val + 180.0) % 360 - 180 + + @staticmethod + def normalize_rad(val): + ''' normalize val in range [-pi,pi[ ''' + return (val + pi) % (2*pi) - pi + + @staticmethod + def deg_to_rad(val): + ''' convert degrees to radians ''' + return val * 0.01745329251994330 + + @staticmethod + def rad_to_deg(val): + ''' convert radians to degrees ''' + return val * 57.29577951308232 + + @staticmethod + def vector_angle(vector, is_rad=False): + ''' angle (degrees or radians) of 2D vector ''' + if is_rad: + return atan2(vector[1], vector[0]) + else: + return atan2(vector[1], vector[0]) * 180 / pi + + @staticmethod + def vectors_angle(vec1, vec2, is_rad=False): + ''' get angle between vectors (degrees or radians) ''' + ang_rad = acos(np.dot(Math_Ops.normalize_vec(vec1),Math_Ops.normalize_vec(vec2))) + return ang_rad if is_rad else ang_rad * 180 / pi + + @staticmethod + def vector_from_angle(angle, is_rad=False): + ''' unit vector with direction given by `angle` ''' + if is_rad: + return np.array([cos(angle), sin(angle)], float) + else: + return np.array([Math_Ops.deg_cos(angle), Math_Ops.deg_sin(angle)], float) + + @staticmethod + def target_abs_angle(pos2d, target, is_rad=False): + ''' angle (degrees or radians) of vector (target-pos2d) ''' + if is_rad: + return atan2(target[1]-pos2d[1], target[0]-pos2d[0]) + else: + return atan2(target[1]-pos2d[1], target[0]-pos2d[0]) * 180 / pi + + @staticmethod + def target_rel_angle(pos2d, ori, target, is_rad=False): + ''' relative angle (degrees or radians) of target if we're located at 'pos2d' with orientation 'ori' (degrees or radians) ''' + if is_rad: + return Math_Ops.normalize_rad( atan2(target[1]-pos2d[1], target[0]-pos2d[0]) - ori ) + else: + return Math_Ops.normalize_deg( atan2(target[1]-pos2d[1], target[0]-pos2d[0]) * 180 / pi - ori ) + + @staticmethod + def rotate_2d_vec(vec, angle, is_rad=False): + ''' rotate 2D vector anticlockwise around the origin by `angle` ''' + cos_ang = cos(angle) if is_rad else cos(angle * pi / 180) + sin_ang = sin(angle) if is_rad else sin(angle * pi / 180) + return np.array([cos_ang*vec[0]-sin_ang*vec[1], sin_ang*vec[0]+cos_ang*vec[1]]) + + @staticmethod + def distance_point_to_line(p:np.ndarray, a:np.ndarray, b:np.ndarray): + ''' + Distance between point p and 2d line 'ab' (and side where p is) + + Parameters + ---------- + a : ndarray + 2D point that defines line + b : ndarray + 2D point that defines line + p : ndarray + 2D point + + Returns + ------- + distance : float + distance between line and point + side : str + if we are at a, looking at b, p may be at our "left" or "right" + ''' + line_len = np.linalg.norm(b-a) + + if line_len == 0: # assumes vertical line + dist = sdist = np.linalg.norm(p-a) + else: + sdist = np.cross(b-a,p-a)/line_len + dist = abs(sdist) + + return dist, "left" if sdist>0 else "right" + + @staticmethod + def distance_point_to_segment(p:np.ndarray, a:np.ndarray, b:np.ndarray): + ''' Distance from point p to 2d line segment 'ab' ''' + + ap = p-a + ab = b-a + + ad = Math_Ops.vector_projection(ap,ab) + + # Is d in ab? We can find k in (ad = k * ab) without computing any norm + # we use the largest dimension of ab to avoid division by 0 + k = ad[0]/ab[0] if abs(ab[0])>abs(ab[1]) else ad[1]/ab[1] + + if k <= 0: return np.linalg.norm(ap) + elif k >= 1: return np.linalg.norm(p-b) + else: return np.linalg.norm(p-(ad + a)) # p-d + + @staticmethod + def distance_point_to_ray(p:np.ndarray, ray_start:np.ndarray, ray_direction:np.ndarray): + ''' Distance from point p to 2d ray ''' + + rp = p-ray_start + rd = Math_Ops.vector_projection(rp,ray_direction) + + # Is d in ray? We can find k in (rd = k * ray_direction) without computing any norm + # we use the largest dimension of ray_direction to avoid division by 0 + k = rd[0]/ray_direction[0] if abs(ray_direction[0])>abs(ray_direction[1]) else rd[1]/ray_direction[1] + + if k <= 0: return np.linalg.norm(rp) + else: return np.linalg.norm(p-(rd + ray_start)) # p-d + + @staticmethod + def closest_point_on_ray_to_point(p:np.ndarray, ray_start:np.ndarray, ray_direction:np.ndarray): + ''' Point on ray closest to point p ''' + + rp = p-ray_start + rd = Math_Ops.vector_projection(rp,ray_direction) + + # Is d in ray? We can find k in (rd = k * ray_direction) without computing any norm + # we use the largest dimension of ray_direction to avoid division by 0 + k = rd[0]/ray_direction[0] if abs(ray_direction[0])>abs(ray_direction[1]) else rd[1]/ray_direction[1] + + if k <= 0: return ray_start + else: return rd + ray_start + + @staticmethod + def does_circle_intersect_segment(p:np.ndarray, r, a:np.ndarray, b:np.ndarray): + ''' Returns true if circle (center p, radius r) intersect 2d line segment ''' + + ap = p-a + ab = b-a + + ad = Math_Ops.vector_projection(ap,ab) + + # Is d in ab? We can find k in (ad = k * ab) without computing any norm + # we use the largest dimension of ab to avoid division by 0 + k = ad[0]/ab[0] if abs(ab[0])>abs(ab[1]) else ad[1]/ab[1] + + if k <= 0: return np.dot(ap,ap) <= r*r + elif k >= 1: return np.dot(p-b,p-b) <= r*r + + dp = p-(ad + a) + return np.dot(dp,dp) <= r*r + + @staticmethod + def vector_projection(a:np.ndarray, b:np.ndarray): + ''' Vector projection of a onto b ''' + b_dot = np.dot(b,b) + return b * np.dot(a,b) / b_dot if b_dot != 0 else b + + @staticmethod + def do_noncollinear_segments_intersect(a,b,c,d): + ''' + Check if 2d line segment 'ab' intersects with noncollinear 2d line segment 'cd' + Explanation: https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/ + ''' + + ccw = lambda a,b,c: (c[1]-a[1]) * (b[0]-a[0]) > (b[1]-a[1]) * (c[0]-a[0]) + return ccw(a,c,d) != ccw(b,c,d) and ccw(a,b,c) != ccw(a,b,d) + + @staticmethod + def intersection_segment_opp_goal(a:np.ndarray, b:np.ndarray): + ''' Computes the intersection point of 2d segment 'ab' and the opponents' goal (front line) ''' + vec_x = b[0]-a[0] + + # Collinear intersections are not accepted + if vec_x == 0: return None + + k = (15.01-a[0])/vec_x + + # No collision + if k < 0 or k > 1: return None + + intersection_pt = a + (b-a) * k + + if -1.01 <= intersection_pt[1] <= 1.01: + return intersection_pt + else: + return None + + @staticmethod + def intersection_circle_opp_goal(p:np.ndarray, r): + ''' + Computes the intersection segment of circle (center p, radius r) and the opponents' goal (front line) + Only the y coordinates are returned since the x coordinates are always equal to 15 + ''' + + x_dev = abs(15-p[0]) + + if x_dev > r: + return None # no intersection with x=15 + + y_dev = sqrt(r*r - x_dev*x_dev) + + p1 = max(p[1] - y_dev, -1.01) + p2 = min(p[1] + y_dev, 1.01) + + if p1 == p2: + return p1 # return the y coordinate of a single intersection point + elif p2 < p1: + return None # no intersection + else: + return p1, p2 # return the y coordinates of the intersection segment + + + @staticmethod + def distance_point_to_opp_goal(p:np.ndarray): + ''' Distance between point 'p' and the opponents' goal (front line) ''' + + if p[1] < -1.01: + return np.linalg.norm( p-(15,-1.01) ) + elif p[1] > 1.01: + return np.linalg.norm( p-(15, 1.01) ) + else: + return abs(15-p[0]) + + + @staticmethod + def circle_line_segment_intersection(circle_center, circle_radius, pt1, pt2, full_line=True, tangent_tol=1e-9): + """ Find the points at which a circle intersects a line-segment. This can happen at 0, 1, or 2 points. + + :param circle_center: The (x, y) location of the circle center + :param circle_radius: The radius of the circle + :param pt1: The (x, y) location of the first point of the segment + :param pt2: The (x, y) location of the second point of the segment + :param full_line: True to find intersections along full line - not just in the segment. False will just return intersections within the segment. + :param tangent_tol: Numerical tolerance at which we decide the intersections are close enough to consider it a tangent + :return Sequence[Tuple[float, float]]: A list of length 0, 1, or 2, where each element is a point at which the circle intercepts a line segment. + + Note: We follow: http://mathworld.wolfram.com/Circle-LineIntersection.html + """ + + (p1x, p1y), (p2x, p2y), (cx, cy) = pt1, pt2, circle_center + (x1, y1), (x2, y2) = (p1x - cx, p1y - cy), (p2x - cx, p2y - cy) + dx, dy = (x2 - x1), (y2 - y1) + dr = (dx ** 2 + dy ** 2)**.5 + big_d = x1 * y2 - x2 * y1 + discriminant = circle_radius ** 2 * dr ** 2 - big_d ** 2 + + if discriminant < 0: # No intersection between circle and line + return [] + else: # There may be 0, 1, or 2 intersections with the segment + intersections = [ + (cx + (big_d * dy + sign * (-1 if dy < 0 else 1) * dx * discriminant**.5) / dr ** 2, + cy + (-big_d * dx + sign * abs(dy) * discriminant**.5) / dr ** 2) + for sign in ((1, -1) if dy < 0 else (-1, 1))] # This makes sure the order along the segment is correct + if not full_line: # If only considering the segment, filter out intersections that do not fall within the segment + fraction_along_segment = [ + (xi - p1x) / dx if abs(dx) > abs(dy) else (yi - p1y) / dy for xi, yi in intersections] + intersections = [pt for pt, frac in zip( + intersections, fraction_along_segment) if 0 <= frac <= 1] + # If line is tangent to circle, return just one point (as both intersections have same location) + if len(intersections) == 2 and abs(discriminant) <= tangent_tol: + return [intersections[0]] + else: + return intersections + + + + + # adapted from https://stackoverflow.com/questions/3252194/numpy-and-line-intersections + @staticmethod + def get_line_intersection(a1, a2, b1, b2): + """ + Returns the point of intersection of the lines passing through a2,a1 and b2,b1. + a1: [x, y] a point on the first line + a2: [x, y] another point on the first line + b1: [x, y] a point on the second line + b2: [x, y] another point on the second line + """ + s = np.vstack([a1,a2,b1,b2]) # s for stacked + h = np.hstack((s, np.ones((4, 1)))) # h for homogeneous + l1 = np.cross(h[0], h[1]) # get first line + l2 = np.cross(h[2], h[3]) # get second line + x, y, z = np.cross(l1, l2) # point of intersection + if z == 0: # lines are parallel + return np.array([float('inf'), float('inf')]) + return np.array([x/z, y/z],float) diff --git a/math_ops/Matrix_3x3.py b/math_ops/Matrix_3x3.py new file mode 100644 index 0000000..f34ba47 --- /dev/null +++ b/math_ops/Matrix_3x3.py @@ -0,0 +1,350 @@ +from math import asin, atan2, pi, sqrt +import numpy as np + +class Matrix_3x3(): + + def __init__(self, matrix = None) -> None: + ''' + Constructor examples: + a = Matrix_3x3( ) # create identity matrix + b = Matrix_3x3( [[1,1,1],[2,2,2],[3,3,3]] ) # manually initialize matrix + c = Matrix_3x3( [1,1,1,2,2,2,3,3,3] ) # manually initialize matrix + d = Matrix_3x3( b ) # copy constructor + ''' + if matrix is None: + self.m = np.identity(3) + elif type(matrix) == Matrix_3x3: + self.m = np.copy(matrix.m) + else: + self.m = np.asarray(matrix) + self.m.shape = (3,3) #reshape if needed, throw error if impossible + + + self.rotation_shortcuts={(1,0,0):self.rotate_x_rad, (-1, 0, 0):self._rotate_x_neg_rad, + (0,1,0):self.rotate_y_rad, ( 0,-1, 0):self._rotate_y_neg_rad, + (0,0,1):self.rotate_z_rad, ( 0, 0,-1):self._rotate_z_neg_rad} + + @classmethod + def from_rotation_deg(cls, euler_vec): + ''' + Create rotation matrix from Euler angles, in degrees. + Rotation order: RotZ*RotY*RotX + + Parameters + ---------- + euler_vec : array_like, length 3 + vector with Euler angles (x,y,z) aka (roll, pitch, yaw) + + Example + ---------- + Matrix_3x3.from_rotation_deg((roll,pitch,yaw)) # Creates: RotZ(yaw)*RotY(pitch)*RotX(roll) + ''' + mat = cls().rotate_z_deg(euler_vec[2], True).rotate_y_deg(euler_vec[1], True).rotate_x_deg(euler_vec[0], True) + return mat + + def get_roll_deg(self): + ''' Get angle around the x-axis in degrees, Rotation order: RotZ*RotY*RotX=Rot ''' + if self.m[2,1] == 0 and self.m[2,2] == 0: + return 180 + return atan2(self.m[2,1], self.m[2,2]) * 180 / pi + + def get_pitch_deg(self): + ''' Get angle around the y-axis in degrees, Rotation order: RotZ*RotY*RotX=Rot ''' + return atan2(-self.m[2,0], sqrt(self.m[2,1]*self.m[2,1] + self.m[2,2]*self.m[2,2])) * 180 / pi + + def get_yaw_deg(self): + ''' Get angle around the z-axis in degrees, Rotation order: RotZ*RotY*RotX=Rot ''' + if self.m[1,0] == 0 and self.m[0,0] == 0: + return atan2(self.m[0,1], self.m[1,1]) * 180 / pi + return atan2(self.m[1,0], self.m[0,0]) * 180 / pi + + def get_inclination_deg(self): + ''' Get inclination of z-axis in relation to reference z-axis ''' + return 90 - (asin(self.m[2,2]) * 180 / pi) + + + def rotate_deg(self, rotation_vec, rotation_deg, in_place=False): + ''' + Rotates the current rotation matrix + + Parameters + ---------- + rotation_vec : array_like, length 3 + rotation vector + rotation_rad : float + rotation in degrees + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_3x3 + self is returned if in_place is True + ''' + return self.rotate_rad(rotation_vec, rotation_deg * (pi/180) , in_place) + + + def rotate_rad(self, rotation_vec, rotation_rad, in_place=False): + ''' + Rotates the current rotation matrix + + Parameters + ---------- + rotation_vec : array_like, length 3 + rotation vector + rotation_rad : float + rotation in radians + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_3x3 + self is returned if in_place is True + ''' + + if rotation_rad == 0: return + + shortcut = self.rotation_shortcuts.get(tuple(a for a in rotation_vec)) + if shortcut: + return shortcut(rotation_rad, in_place) + + c = np.math.cos(rotation_rad) + c1 = 1 - c + s = np.math.sin(rotation_rad) + x = rotation_vec[0] + y = rotation_vec[1] + z = rotation_vec[2] + xxc1 = x * x * c1 + yyc1 = y * y * c1 + zzc1 = z * z * c1 + xyc1 = x * y * c1 + xzc1 = x * z * c1 + yzc1 = y * z * c1 + xs = x * s + ys = y * s + zs = z * s + + mat = np.array([ + [xxc1 + c, xyc1 - zs, xzc1 + ys], + [xyc1 + zs, yyc1 + c, yzc1 - xs], + [xzc1 - ys, yzc1 + xs, zzc1 + c]]) + + return self.multiply(mat, in_place) + + + def _rotate_x_neg_rad(self, rotation_rad, in_place=False): + self.rotate_x_rad(-rotation_rad, in_place) + + def _rotate_y_neg_rad(self, rotation_rad, in_place=False): + self.rotate_y_rad(-rotation_rad, in_place) + + def _rotate_z_neg_rad(self, rotation_rad, in_place=False): + self.rotate_z_rad(-rotation_rad, in_place) + + def rotate_x_rad(self, rotation_rad, in_place=False): + ''' + Rotates the current rotation matrix around the x-axis + + Parameters + ---------- + rotation_rad : float + rotation in radians + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_3x3 + self is returned if in_place is True + ''' + if rotation_rad == 0: + return self if in_place else Matrix_3x3(self) + + c = np.math.cos(rotation_rad) + s = np.math.sin(rotation_rad) + + mat = np.array([ + [1, 0, 0], + [0, c,-s], + [0, s, c]]) + + return self.multiply(mat, in_place) + + def rotate_y_rad(self, rotation_rad, in_place=False): + ''' + Rotates the current rotation matrix around the y-axis + + Parameters + ---------- + rotation_rad : float + rotation in radians + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_3x3 + self is returned if in_place is True + ''' + if rotation_rad == 0: + return self if in_place else Matrix_3x3(self) + + c = np.math.cos(rotation_rad) + s = np.math.sin(rotation_rad) + + mat = np.array([ + [ c, 0, s], + [ 0, 1, 0], + [-s, 0, c]]) + + return self.multiply(mat, in_place) + + def rotate_z_rad(self, rotation_rad, in_place=False): + ''' + Rotates the current rotation matrix around the z-axis + + Parameters + ---------- + rotation_rad : float + rotation in radians + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_3x3 + self is returned if in_place is True + ''' + if rotation_rad == 0: + return self if in_place else Matrix_3x3(self) + + c = np.math.cos(rotation_rad) + s = np.math.sin(rotation_rad) + + mat = np.array([ + [ c,-s, 0], + [ s, c, 0], + [ 0, 0, 1]]) + + return self.multiply(mat, in_place) + + def rotate_x_deg(self, rotation_deg, in_place=False): + ''' + Rotates the current rotation matrix around the x-axis + + Parameters + ---------- + rotation_rad : float + rotation in degrees + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_3x3 + self is returned if in_place is True + ''' + return self.rotate_x_rad(rotation_deg * (pi/180), in_place) + + def rotate_y_deg(self, rotation_deg, in_place=False): + ''' + Rotates the current rotation matrix around the y-axis + + Parameters + ---------- + rotation_rad : float + rotation in degrees + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_3x3 + self is returned if in_place is True + ''' + return self.rotate_y_rad(rotation_deg * (pi/180), in_place) + + def rotate_z_deg(self, rotation_deg, in_place=False): + ''' + Rotates the current rotation matrix around the z-axis + + Parameters + ---------- + rotation_rad : float + rotation in degrees + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_3x3 + self is returned if in_place is True + ''' + return self.rotate_z_rad(rotation_deg * (pi/180), in_place) + + def invert(self, in_place=False): + ''' + Inverts the current rotation matrix + + Parameters + ---------- + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_3x3 + self is returned if in_place is True + ''' + + if in_place: + self.m = np.linalg.inv(self.m) + return self + else: + return Matrix_3x3(np.linalg.inv(self.m)) + + def multiply(self,mat, in_place=False, reverse_order=False): + ''' + Multiplies the current rotation matrix by mat + + Parameters + ---------- + mat : Matrix_3x3 or array_like + multiplier matrix or 3D vector + in_place: bool, optional + - True: the internal matrix is changed in-place + - False: a new matrix is returned and the current one is not changed (default) + reverse_order: bool, optional + - False: self * mat + - True: mat * self + + Returns + ------- + result : Matrix_3x3 | array_like + Matrix_3x3 is returned if mat is a matrix (self is returned if in_place is True); + a 3D vector is returned if mat is a vector + ''' + # get array from matrix object or convert to numpy array (if needed) + mat = mat.m if type(mat) == Matrix_3x3 else np.asarray(mat) + + a,b = (mat, self.m) if reverse_order else (self.m, mat) + + if mat.ndim == 1: + return np.matmul(a, b) # multiplication by 3D vector + elif in_place: + np.matmul(a, b, self.m) # multiplication by matrix, in place + return self + else: # multiplication by matrix, return new Matrix_3x3 + return Matrix_3x3(np.matmul(a, b)) + + diff --git a/math_ops/Matrix_4x4.py b/math_ops/Matrix_4x4.py new file mode 100644 index 0000000..ec11b49 --- /dev/null +++ b/math_ops/Matrix_4x4.py @@ -0,0 +1,440 @@ +from math import asin, atan2, pi, sqrt +from math_ops.Math_Ops import Math_Ops as M +from math_ops.Matrix_3x3 import Matrix_3x3 +import numpy as np + +class Matrix_4x4(): + + def __init__(self, matrix = None) -> None: + ''' + Constructor examples: + a = Matrix_4x4( ) # create identity matrix + b = Matrix_4x4( [[1,1,1,1],[2,2,2,2],[3,3,3,3],[4,4,4,4]] ) # manually initialize matrix + c = Matrix_4x4( [1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4] ) # manually initialize matrix + d = Matrix_4x4( b ) # copy constructor + ''' + if matrix is None: + self.m = np.identity(4) + elif type(matrix) == Matrix_4x4: + self.m = np.copy(matrix.m) + elif type(matrix) == Matrix_3x3: + self.m = np.identity(4) + self.m[0:3,0:3] = matrix.m + else: + self.m = np.asarray(matrix) + self.m.shape = (4,4) #reshape if needed, throw error if impossible + + + @classmethod + def from_translation(cls, translation_vec): + ''' + Create transformation matrix from translation_vec translation + e.g. Matrix_4x4.from_translation((a,b,c)) + output: [[1,0,0,a],[0,1,0,b],[0,0,1,c],[0,0,0,1]] + ''' + mat = np.identity(4) + mat[0:3,3] = translation_vec + return cls(mat) + + @classmethod + def from_3x3_and_translation(cls, mat3x3:Matrix_3x3, translation_vec): + ''' + Create transformation matrix from rotation matrix (3x3) and translation + e.g. Matrix_4x4.from_3x3_and_translation(r,(a,b,c)) + output: [[r00,r01,r02,a],[r10,r11,r12,b],[r20,r21,r22,c],[0,0,0,1]] + ''' + mat = np.identity(4) + mat[0:3,0:3] = mat3x3.m + mat[0:3,3] = translation_vec + return cls(mat) + + def translate(self, translation_vec, in_place=False): + ''' + Translates the current transformation matrix + + Parameters + ---------- + translation_vec : array_like, length 3 + translation vector + in_place: bool, optional + * True: the internal matrix is changed in-place + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_4x4 + self is returned if in_place is True + ''' + vec = np.array([*translation_vec,1])# conversion to 4D vector + np.matmul(self.m, vec, out=vec) # compute only 4th column + + if in_place: + self.m[:,3] = vec + return self + else: + ret = Matrix_4x4(self.m) + ret.m[:,3] = vec + return ret + + + def get_translation(self): + ''' Get translation vector (x,y,z) ''' + return self.m[0:3,3] # return view + + def get_x(self): + return self.m[0,3] + + def get_y(self): + return self.m[1,3] + + def get_z(self): + return self.m[2,3] + + def get_rotation_4x4(self): + ''' Get Matrix_4x4 without translation ''' + mat = Matrix_4x4(self) + mat.m[0:3,3] = 0 + return mat + + def get_rotation(self): + ''' Get rotation Matrix_3x3 ''' + return Matrix_3x3(self.m[0:3,0:3]) + + def get_distance(self): + ''' Get translation vector length ''' + return np.linalg.norm(self.m[0:3,3]) + + def get_roll_deg(self): + ''' Get angle around the x-axis in degrees, Rotation order: RotZ*RotY*RotX=Rot ''' + if self.m[2,1] == 0 and self.m[2,2] == 0: + return 180 + return atan2(self.m[2,1], self.m[2,2]) * 180 / pi + + def get_pitch_deg(self): + ''' Get angle around the y-axis in degrees, Rotation order: RotZ*RotY*RotX=Rot ''' + return atan2(-self.m[2,0], sqrt(self.m[2,1]*self.m[2,1] + self.m[2,2]*self.m[2,2])) * 180 / pi + + def get_yaw_deg(self): + ''' Get angle around the z-axis in degrees, Rotation order: RotZ*RotY*RotX=Rot ''' + if self.m[1,0] == 0 and self.m[0,0] == 0: + return atan2(self.m[0,1], self.m[1,1]) * 180 / pi + return atan2(self.m[1,0], self.m[0,0]) * 180 / pi + + def get_inclination_deg(self): + ''' Get inclination of z-axis in relation to reference z-axis ''' + return 90 - (asin(np.clip(self.m[2,2],-1,1)) * 180 / pi) + + def rotate_deg(self, rotation_vec, rotation_deg, in_place=False): + ''' + Rotates the current transformation matrix + + Parameters + ---------- + rotation_vec : array_like, length 3 + rotation vector + rotation_rad : float + rotation in degrees + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_4x4 + self is returned if in_place is True + ''' + return self.rotate_rad(rotation_vec, rotation_deg * (pi/180) , in_place) + + + def rotate_rad(self, rotation_vec, rotation_rad, in_place=False): + ''' + Rotates the current transformation matrix + + Parameters + ---------- + rotation_vec : array_like, length 3 + rotation vector + rotation_rad : float + rotation in radians + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_4x4 + self is returned if in_place is True + ''' + + if rotation_rad == 0: + return self if in_place else Matrix_4x4(self) + + # shortcuts for rotation around 1 axis + if rotation_vec[0]==0: + if rotation_vec[1]==0: + if rotation_vec[2]==1: + return self.rotate_z_rad(rotation_rad, in_place) + elif rotation_vec[2]==-1: + return self.rotate_z_rad(-rotation_rad, in_place) + elif rotation_vec[2]==0: + if rotation_vec[1]==1: + return self.rotate_y_rad(rotation_rad, in_place) + elif rotation_vec[1]==-1: + return self.rotate_y_rad(-rotation_rad, in_place) + elif rotation_vec[1]==0 and rotation_vec[2]==0: + if rotation_vec[0]==1: + return self.rotate_x_rad(rotation_rad, in_place) + elif rotation_vec[0]==-1: + return self.rotate_x_rad(-rotation_rad, in_place) + + c = np.math.cos(rotation_rad) + c1 = 1 - c + s = np.math.sin(rotation_rad) + x = rotation_vec[0] + y = rotation_vec[1] + z = rotation_vec[2] + xxc1 = x * x * c1 + yyc1 = y * y * c1 + zzc1 = z * z * c1 + xyc1 = x * y * c1 + xzc1 = x * z * c1 + yzc1 = y * z * c1 + xs = x * s + ys = y * s + zs = z * s + + mat = np.array([ + [xxc1 + c, xyc1 - zs, xzc1 + ys, 0], + [xyc1 + zs, yyc1 + c, yzc1 - xs, 0], + [xzc1 - ys, yzc1 + xs, zzc1 + c, 0], + [0, 0, 0, 1]]) + + return self.multiply(mat, in_place) + + + def rotate_x_rad(self, rotation_rad, in_place=False): + ''' + Rotates the current transformation matrix around the x-axis + + Parameters + ---------- + rotation_rad : float + rotation in radians + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_4x4 + self is returned if in_place is True + ''' + if rotation_rad == 0: + return self if in_place else Matrix_4x4(self) + + c = np.math.cos(rotation_rad) + s = np.math.sin(rotation_rad) + + mat = np.array([ + [1, 0, 0, 0], + [0, c,-s, 0], + [0, s, c, 0], + [0, 0, 0, 1]]) + + return self.multiply(mat, in_place) + + def rotate_y_rad(self, rotation_rad, in_place=False): + ''' + Rotates the current transformation matrix around the y-axis + + Parameters + ---------- + rotation_rad : float + rotation in radians + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_4x4 + self is returned if in_place is True + ''' + if rotation_rad == 0: + return self if in_place else Matrix_4x4(self) + + c = np.math.cos(rotation_rad) + s = np.math.sin(rotation_rad) + + mat = np.array([ + [ c, 0, s, 0], + [ 0, 1, 0, 0], + [-s, 0, c, 0], + [ 0, 0, 0, 1]]) + + return self.multiply(mat, in_place) + + def rotate_z_rad(self, rotation_rad, in_place=False): + ''' + Rotates the current transformation matrix around the z-axis + + Parameters + ---------- + rotation_rad : float + rotation in radians + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_4x4 + self is returned if in_place is True + ''' + if rotation_rad == 0: + return self if in_place else Matrix_4x4(self) + + c = np.math.cos(rotation_rad) + s = np.math.sin(rotation_rad) + + mat = np.array([ + [ c,-s, 0, 0], + [ s, c, 0, 0], + [ 0, 0, 1, 0], + [ 0, 0, 0, 1]]) + + return self.multiply(mat, in_place) + + def rotate_x_deg(self, rotation_deg, in_place=False): + ''' + Rotates the current transformation matrix around the x-axis + + Parameters + ---------- + rotation_rad : float + rotation in degrees + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_4x4 + self is returned if in_place is True + ''' + return self.rotate_x_rad(rotation_deg * (pi/180), in_place) + + def rotate_y_deg(self, rotation_deg, in_place=False): + ''' + Rotates the current transformation matrix around the y-axis + + Parameters + ---------- + rotation_rad : float + rotation in degrees + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_4x4 + self is returned if in_place is True + ''' + return self.rotate_y_rad(rotation_deg * (pi/180), in_place) + + def rotate_z_deg(self, rotation_deg, in_place=False): + ''' + Rotates the current transformation matrix around the z-axis + + Parameters + ---------- + rotation_rad : float + rotation in degrees + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_4x4 + self is returned if in_place is True + ''' + return self.rotate_z_rad(rotation_deg * (pi/180), in_place) + + def invert(self, in_place=False): + ''' + Inverts the current transformation matrix + + Parameters + ---------- + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed + + Returns + ------- + result : Matrix_4x4 + self is returned if in_place is True + ''' + + if in_place: + self.m = np.linalg.inv(self.m) + return self + else: + return Matrix_4x4(np.linalg.inv(self.m)) + + def multiply(self,mat, in_place=False): + ''' + Multiplies the current transformation matrix by mat + + Parameters + ---------- + mat : Matrix_4x4 or array_like + multiplier matrix or 3D vector + in_place: bool, optional + * True: the internal matrix is changed in-place (default) + * False: a new matrix is returned and the current one is not changed (if mat is a 4x4 matrix) + + Returns + ------- + result : Matrix_4x4 | array_like + Matrix_4x4 is returned if mat is a matrix (self is returned if in_place is True); + a 3D vector is returned if mat is a vector + ''' + if type(mat) == Matrix_4x4: + mat = mat.m + else: + mat = np.asarray(mat) # conversion to array, if needed + if mat.ndim == 1: # multiplication by 3D vector + vec = np.append(mat,1) # conversion to 4D vector + return np.matmul(self.m, vec)[0:3] # conversion to 3D vector + + if in_place: + np.matmul(self.m, mat, self.m) + return self + else: + return Matrix_4x4(np.matmul(self.m, mat)) + + def __call__(self,mat, is_spherical=False): + ''' + Multiplies the current transformation matrix by mat and returns a new matrix or vector + + Parameters + ---------- + mat : Matrix_4x4 or array_like + multiplier matrix or 3D vector + is_spherical : bool + only relevant if mat is a 3D vector, True if it uses spherical coordinates + + Returns + ------- + result : Matrix_4x4 | array_like + Matrix_4x4 is returned if mat is a matrix; + a 3D vector is returned if mat is a vector + ''' + + if is_spherical and mat.ndim == 1: mat = M.deg_sph2cart(mat) + return self.multiply(mat,False) + + \ No newline at end of file diff --git a/math_ops/Neural_Network.py b/math_ops/Neural_Network.py new file mode 100644 index 0000000..fec7016 --- /dev/null +++ b/math_ops/Neural_Network.py @@ -0,0 +1,28 @@ +import numpy as np + + +def run_mlp(obs, weights, activation_function="tanh"): + ''' + Run multilayer perceptron using numpy + + Parameters + ---------- + obs : ndarray + float32 array with neural network inputs + weights : list + list of MLP layers of type (bias, kernel) + activation_function : str + activation function for hidden layers + set to "none" to disable + ''' + + obs = obs.astype(np.float32, copy=False) + out = obs + + for w in weights[:-1]: # for each hidden layer + out = np.matmul(w[1],out) + w[0] + if activation_function == "tanh": + np.tanh(out, out=out) + elif activation_function != "none": + raise NotImplementedError + return np.matmul(weights[-1][1],out) + weights[-1][0] # final layer \ No newline at end of file diff --git a/scripts/commons/Script.py b/scripts/commons/Script.py new file mode 100644 index 0000000..d14ebed --- /dev/null +++ b/scripts/commons/Script.py @@ -0,0 +1,300 @@ +from os import path, listdir, getcwd, cpu_count +from os.path import join, realpath, dirname, isfile, isdir, getmtime +from scripts.commons.UI import UI +import __main__ +import argparse,json,sys +import pickle +import subprocess + + +class Script(): + ROOT_DIR = path.dirname(path.dirname(realpath( join(getcwd(), dirname(__file__))) )) # project root directory + + def __init__(self, cpp_builder_unum=0) -> None: + + ''' + Arguments specification + ----------------------- + - To add new arguments, edit the information below + - After changing information below, the config.json file must be manually deleted + - In other modules, these arguments can be accessed by their 1-letter ID + ''' + # list of arguments: 1-letter ID, Description, Hardcoded default + self.options = {'i': ('Server Hostname/IP', 'localhost'), + 'p': ('Agent Port', '3100'), + 'm': ('Monitor Port', '3200'), + 't': ('Team Name', 'FCPortugal'), + 'u': ('Uniform Number', '1'), + 'r': ('Robot Type', '1'), + 'P': ('Penalty Shootout', '0'), + 'F': ('magmaFatProxy', '0'), + 'D': ('Debug Mode', '1')} + + # list of arguments: 1-letter ID, data type, choices + self.op_types = {'i': (str, None), + 'p': (int, None), + 'm': (int, None), + 't': (str, None), + 'u': (int, range(1,12)), + 'r': (int, [0,1,2,3,4]), + 'P': (int, [0,1]), + 'F': (int, [0,1]), + 'D': (int, [0,1])} + + ''' + End of arguments specification + ''' + + self.read_or_create_config() + + #advance help text position + formatter = lambda prog: argparse.HelpFormatter(prog,max_help_position=52) + parser = argparse.ArgumentParser(formatter_class=formatter) + + o = self.options + t = self.op_types + + for id in self.options: # shorter metavar for aesthetic reasons + parser.add_argument(f"-{id}", help=f"{o[id][0]:30}[{o[id][1]:20}]", type=t[id][0], nargs='?', default=o[id][1], metavar='X', choices=t[id][1]) + + self.args = parser.parse_args() + + if getattr(sys, 'frozen', False): # disable debug mode when running from binary + self.args.D = 0 + + self.players = [] # list of created players + + Script.build_cpp_modules(exit_on_build = (cpp_builder_unum != 0 and cpp_builder_unum != self.args.u)) + + if self.args.D: + try: + print(f"\nNOTE: for help run \"python {__main__.__file__} -h\"") + except: + pass + + columns = [[],[],[]] + for key, value in vars(self.args).items(): + columns[0].append(o[key][0]) + columns[1].append(o[key][1]) + columns[2].append(value) + + UI.print_table(columns, ["Argument","Default at /config.json","Active"], alignment=["<","^","^"]) + + + def read_or_create_config(self) -> None: + + if not path.isfile('config.json'): # Save hardcoded default values if file does not exist + with open("config.json", "w") as f: + json.dump(self.options, f, indent=4) + else: # Load user-defined values (that can be overwritten in the terminal) + with open("config.json", "r") as f: + self.options = json.loads(f.read()) + + + @staticmethod + def build_cpp_modules(special_environment_prefix=[], exit_on_build=False): + ''' + Build C++ modules in folder /cpp using Pybind11 + + Parameters + ---------- + special_environment_prefix : `list` + command prefix to run a given command in the desired environment + useful to compile C++ modules for different python interpreter versions (other than default version) + Conda Env. example: ['conda', 'run', '-n', 'myEnv'] + If [] the default python interpreter is used as compilation target + exit_on_build : bool + exit if there is something to build (so that only 1 player per team builds c++ modules) + ''' + cpp_path = Script.ROOT_DIR + "/cpp/" + exclusions = ["__pycache__"] + + cpp_modules = [d for d in listdir(cpp_path) if isdir(join(cpp_path, d)) and d not in exclusions] + + if not cpp_modules: return #no modules to build + + python_cmd = f"python{sys.version_info.major}.{sys.version_info.minor}" # "python3" can select the wrong version, this prevents that + + def init(): + print("--------------------------\nC++ modules:",cpp_modules) + + try: + process = subprocess.Popen(special_environment_prefix+[python_cmd, "-m", "pybind11", "--includes"], stdout=subprocess.PIPE) + (includes, err) = process.communicate() + process.wait() + except: + print(f"Error while executing child program: '{python_cmd} -m pybind11 --includes'") + exit() + + includes = includes.decode().rstrip() # strip trailing newlines (and other whitespace chars) + print("Using Pybind11 includes: '",includes,"'",sep="") + return includes + + nproc = str(cpu_count()) + zero_modules = True + + for module in cpp_modules: + module_path = join(cpp_path, module) + + # skip module if there is no Makefile (typical distribution case) + if not isfile(join(module_path, "Makefile")): + continue + + # skip module in certain conditions + if isfile(join(module_path, module+".so")) and isfile(join(module_path, module+".c_info")): + with open(join(module_path, module+".c_info"), 'rb') as f: + info = pickle.load(f) + if info == python_cmd: + code_mod_time = max(getmtime(join(module_path, f)) for f in listdir(module_path) if f.endswith(".cpp") or f.endswith(".h")) + bin_mod_time = getmtime(join(module_path, module+".so")) + if bin_mod_time + 30 > code_mod_time: # favor not building with a margin of 30s (scenario: we unzip the fcpy project, including the binaries, the modification times are all similar) + continue + + # init: print stuff & get Pybind11 includes + if zero_modules: + if exit_on_build: + print("There are C++ modules to build. This player is not allowed to build. Aborting.") + exit() + zero_modules = False + includes = init() + + # build module + print(f'{f"Building: {module}... ":40}',end='',flush=True) + process = subprocess.Popen(['make', '-j'+nproc, 'PYBIND_INCLUDES='+includes], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=module_path) + (output, err) = process.communicate() + exit_code = process.wait() + if exit_code == 0: + print("success!") + with open(join(module_path, module+".c_info"),"wb") as f: # save python version + pickle.dump(python_cmd, f, protocol=4) # protocol 4 is backward compatible with Python 3.4 + else: + print("Aborting! Building errors:") + print(output.decode(), err.decode()) + exit() + + if not zero_modules: + print("All modules were built successfully!\n--------------------------") + + + def batch_create(self, agent_cls, args_per_player): + ''' Creates batch of agents ''' + + for a in args_per_player: + self.players.append( agent_cls(*a) ) + + def batch_execute_agent(self, index : slice = slice(None)): + ''' + Executes agent normally (including commit & send) + + Parameters + ---------- + index : slice + subset of agents + (e.g. index=slice(1,2) will select the second agent) + (e.g. index=slice(1,3) will select the second and third agents) + by default, all agents are selected + ''' + for p in self.players[index]: + p.think_and_send() + + def batch_execute_behavior(self, behavior, index : slice = slice(None)): + ''' + Executes behavior + + Parameters + ---------- + behavior : str + name of behavior to execute + index : slice + subset of agents + (e.g. index=slice(1,2) will select the second agent) + (e.g. index=slice(1,3) will select the second and third agents) + by default, all agents are selected + ''' + for p in self.players[index]: + p.behavior.execute(behavior) + + def batch_commit_and_send(self, index : slice = slice(None)): + ''' + Commits & sends data to server + + Parameters + ---------- + index : slice + subset of agents + (e.g. index=slice(1,2) will select the second agent) + (e.g. index=slice(1,3) will select the second and third agents) + by default, all agents are selected + ''' + for p in self.players[index]: + p.scom.commit_and_send( p.world.robot.get_command() ) + + def batch_receive(self, index : slice = slice(None), update=True): + ''' + Waits for server messages + + Parameters + ---------- + index : slice + subset of agents + (e.g. index=slice(1,2) will select the second agent) + (e.g. index=slice(1,3) will select the second and third agents) + by default, all agents are selected + update : bool + update world state based on information received from server + if False, the agent becomes unaware of itself and its surroundings + which is useful for reducing cpu resources for dummy agents in demonstrations + ''' + for p in self.players[index]: + p.scom.receive(update) + + def batch_commit_beam(self, pos2d_and_rotation, index : slice = slice(None)): + ''' + Beam all player to 2D position with a given rotation + + Parameters + ---------- + pos2d_and_rotation : `list` + iterable of 2D positions and rotations e.g. [(0,0,45),(-5,0,90)] + index : slice + subset of agents + (e.g. index=slice(1,2) will select the second agent) + (e.g. index=slice(1,3) will select the second and third agents) + by default, all agents are selected + ''' + for p, pos_rot in zip(self.players[index], pos2d_and_rotation): + p.scom.commit_beam(pos_rot[0:2],pos_rot[2]) + + def batch_unofficial_beam(self, pos3d_and_rotation, index : slice = slice(None)): + ''' + Beam all player to 3D position with a given rotation + + Parameters + ---------- + pos3d_and_rotation : `list` + iterable of 3D positions and rotations e.g. [(0,0,0.5,45),(-5,0,0.5,90)] + index : slice + subset of agents + (e.g. index=slice(1,2) will select the second agent) + (e.g. index=slice(1,3) will select the second and third agents) + by default, all agents are selected + ''' + for p, pos_rot in zip(self.players[index], pos3d_and_rotation): + p.scom.unofficial_beam(pos_rot[0:3],pos_rot[3]) + + def batch_terminate(self, index : slice = slice(None)): + ''' + Close all sockets connected to the agent port + For scripts where the agent lives until the application ends, this is not needed + + Parameters + ---------- + index : slice + subset of agents + (e.g. index=slice(1,2) will select the second agent) + (e.g. index=slice(1,3) will select the second and third agents) + by default, all agents are selected + ''' + for p in self.players[index]: + p.terminate() + del self.players[index] # delete selection \ No newline at end of file diff --git a/scripts/commons/Server.py b/scripts/commons/Server.py new file mode 100644 index 0000000..caecb28 --- /dev/null +++ b/scripts/commons/Server.py @@ -0,0 +1,60 @@ +import subprocess + +class Server(): + def __init__(self, first_server_p, first_monitor_p, n_servers) -> None: + try: + import psutil + self.check_running_servers(psutil, first_server_p, first_monitor_p, n_servers) + except ModuleNotFoundError: + print("Info: Cannot check if the server is already running, because the psutil module was not found") + + self.first_server_p = first_server_p + self.n_servers = n_servers + self.rcss_processes = [] + + # makes it easier to kill test servers without affecting train servers + cmd = "simspark" if n_servers == 1 else "rcssserver3d" + for i in range(n_servers): + self.rcss_processes.append( + subprocess.Popen((f"{cmd} --agent-port {first_server_p+i} --server-port {first_monitor_p+i}").split(), + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, start_new_session=True) + ) + + def check_running_servers(self, psutil, first_server_p, first_monitor_p, n_servers): + ''' Check if any server is running on chosen ports ''' + found = False + p_list = [p for p in psutil.process_iter() if p.cmdline() and p.name() in ["rcssserver3d","simspark"]] + range1 = (first_server_p, first_server_p + n_servers) + range2 = (first_monitor_p,first_monitor_p + n_servers) + bad_processes = [] + + for p in p_list: + # currently ignoring remaining default port when only one of the ports is specified (uncommon scenario) + ports = [int(arg) for arg in p.cmdline()[1:] if arg.isdigit()] + if len(ports) == 0: + ports = [3100,3200] # default server ports (changing this is unlikely) + + conflicts = [str(port) for port in ports if ( + (range1[0] <= port < range1[1]) or (range2[0] <= port < range2[1]) )] + + if len(conflicts)>0: + if not found: + print("\nThere are already servers running on the same port(s)!") + found = True + bad_processes.append(p) + print(f"Port(s) {','.join(conflicts)} already in use by \"{' '.join(p.cmdline())}\" (PID:{p.pid})") + + if found: + print() + while True: + inp = input("Enter 'kill' to kill these processes or ctrl+c to abort. ") + if inp == "kill": + for p in bad_processes: + p.kill() + return + + + def kill(self): + for p in self.rcss_processes: + p.kill() + print(f"Killed {self.n_servers} rcssserver3d processes starting at {self.first_server_p}") diff --git a/scripts/commons/Train_Base.py b/scripts/commons/Train_Base.py new file mode 100644 index 0000000..960b4c8 --- /dev/null +++ b/scripts/commons/Train_Base.py @@ -0,0 +1,494 @@ +from datetime import datetime, timedelta +from itertools import count +from os import listdir +from os.path import isdir, join, isfile +from scripts.commons.UI import UI +from shutil import copy +from stable_baselines3 import PPO +from stable_baselines3.common.base_class import BaseAlgorithm +from stable_baselines3.common.callbacks import EvalCallback, CheckpointCallback, CallbackList, BaseCallback +from typing import Callable +from world.World import World +from xml.dom import minidom +import numpy as np +import os, time, math, csv, select, sys +import pickle +import xml.etree.ElementTree as ET + + +class Train_Base(): + def __init__(self, script) -> None: + ''' + When training with multiple environments (multiprocessing): + The server port is incremented as follows: + self.server_p, self.server_p+1, self.server_p+2, ... + We add +1000 to the initial monitor port, so than we can have more than 100 environments: + self.monitor_p+1000, self.monitor_p+1001, self.monitor_p+1002, ... + When testing we use self.server_p and self.monitor_p + ''' + + args = script.args + self.script = script + self.ip = args.i + self.server_p = args.p # (initial) server port + self.monitor_p = args.m # monitor port when testing + self.monitor_p_1000 = args.m + 1000 # initial monitor port when training + self.robot_type = args.r + self.team = args.t + self.uniform = args.u + self.cf_last_time = 0 + self.cf_delay = 0 + self.cf_target_period = World.STEPTIME # target simulation speed while testing (default: real-time) + + @staticmethod + def prompt_user_for_model(): + + gyms_logs_path = "./scripts/gyms/logs/" + folders = [f for f in listdir(gyms_logs_path) if isdir(join(gyms_logs_path, f))] + folders.sort(key=lambda f: os.path.getmtime(join(gyms_logs_path, f)), reverse=True) # sort by modification date + + while True: + try: + folder_name = UI.print_list(folders,prompt="Choose folder (ctrl+c to return): ")[1] + except KeyboardInterrupt: + print() + return None # ctrl+c + + folder_dir = os.path.join(gyms_logs_path, folder_name) + models = [m[:-4] for m in listdir(folder_dir) if isfile(join(folder_dir, m)) and m.endswith(".zip")] + + if not models: + print("The chosen folder does not contain any .zip file!") + continue + + models.sort(key=lambda m: os.path.getmtime(join(folder_dir, m+".zip")), reverse=True) # sort by modification date + + try: + model_name = UI.print_list(models,prompt="Choose model (ctrl+c to return): ")[1] + break + except KeyboardInterrupt: + print() + + return {"folder_dir":folder_dir, "folder_name":folder_name, "model_file":os.path.join(folder_dir, model_name+".zip")} + + + def control_fps(self, read_input = False): + ''' Add delay to control simulation speed ''' + + if read_input: + speed = input() + if speed == '': + self.cf_target_period = 0 + print(f"Changed simulation speed to MAX") + else: + if speed == '0': + inp = input("Paused. Set new speed or '' to use previous speed:") + if inp != '': + speed = inp + + try: + speed = int(speed) + assert speed >= 0 + self.cf_target_period = World.STEPTIME * 100 / speed + print(f"Changed simulation speed to {speed}%") + except: + print("""Train_Base.py: + Error: To control the simulation speed, enter a non-negative integer. + To disable this control module, use test_model(..., enable_FPS_control=False) in your gym environment.""") + + now = time.time() + period = now - self.cf_last_time + self.cf_last_time = now + self.cf_delay += (self.cf_target_period - period)*0.9 + if self.cf_delay > 0: + time.sleep(self.cf_delay) + else: + self.cf_delay = 0 + + + def test_model(self, model:BaseAlgorithm, env, log_path:str=None, model_path:str=None, max_episodes=0, enable_FPS_control=True, verbose=1): + ''' + Test model and log results + + Parameters + ---------- + model : BaseAlgorithm + Trained model + env : Env + Gym-like environment + log_path : str + Folder where statistics file is saved, default is `None` (no file is saved) + model_path : str + Folder where it reads evaluations.npz to plot it and create evaluations.csv, default is `None` (no plot, no csv) + max_episodes : int + Run tests for this number of episodes + Default is 0 (run until user aborts) + verbose : int + 0 - no output (except if enable_FPS_control=True) + 1 - print episode statistics + ''' + + if model_path is not None: + assert os.path.isdir(model_path), f"{model_path} is not a valid path" + self.display_evaluations(model_path) + + if log_path is not None: + assert os.path.isdir(log_path), f"{log_path} is not a valid path" + + # If file already exists, don't overwrite + if os.path.isfile(log_path + "/test.csv"): + for i in range(1000): + p = f"{log_path}/test_{i:03}.csv" + if not os.path.isfile(p): + log_path = p + break + else: + log_path += "/test.csv" + + with open(log_path, 'w') as f: + f.write("reward,ep. length,rew. cumulative avg., ep. len. cumulative avg.\n") + print("Train statistics are saved to:", log_path) + + if enable_FPS_control: # control simulation speed (using non blocking user input) + print("\nThe simulation speed can be changed by sending a non-negative integer\n" + "(e.g. '50' sets speed to 50%, '0' pauses the simulation, '' sets speed to MAX)\n") + + ep_reward = 0 + ep_length = 0 + rewards_sum = 0 + reward_min = math.inf + reward_max = -math.inf + ep_lengths_sum = 0 + ep_no = 0 + + obs = env.reset() + while True: + action, _states = model.predict(obs, deterministic=True) + obs, reward, done, info = env.step(action) + ep_reward += reward + ep_length += 1 + + if enable_FPS_control: # control simulation speed (using non blocking user input) + self.control_fps(select.select([sys.stdin], [], [], 0)[0]) + + if done: + obs = env.reset() + rewards_sum += ep_reward + ep_lengths_sum += ep_length + reward_max = max(ep_reward, reward_max) + reward_min = min(ep_reward, reward_min) + ep_no += 1 + avg_ep_lengths = ep_lengths_sum/ep_no + avg_rewards = rewards_sum/ep_no + + if verbose > 0: + print( f"\rEpisode: {ep_no:<3} Ep.Length: {ep_length:<4.0f} Reward: {ep_reward:<6.2f} \n", + end=f"--AVERAGE-- Ep.Length: {avg_ep_lengths:<4.0f} Reward: {avg_rewards:<6.2f} (Min: {reward_min:<6.2f} Max: {reward_max:<6.2f})", flush=True) + + if log_path is not None: + with open(log_path, 'a') as f: + writer = csv.writer(f) + writer.writerow([ep_reward, ep_length, avg_rewards, avg_ep_lengths]) + + if ep_no == max_episodes: + return + + ep_reward = 0 + ep_length = 0 + + def learn_model(self, model:BaseAlgorithm, total_steps:int, path:str, eval_env=None, eval_freq=None, eval_eps=5, save_freq=None, backup_env_file=None, export_name=None): + ''' + Learn Model for a specific number of time steps + + Parameters + ---------- + model : BaseAlgorithm + Model to train + total_steps : int + The total number of samples (env steps) to train on + path : str + Path where the trained model is saved + If the path already exists, an incrementing number suffix is added + eval_env : Env + Environment to periodically test the model + Default is None (no periodical evaluation) + eval_freq : int + Evaluate the agent every X steps + Default is None (no periodical evaluation) + eval_eps : int + Evaluate the agent for X episodes (both eval_env and eval_freq must be defined) + Default is 5 + save_freq : int + Saves model at every X steps + Default is None (no periodical checkpoint) + backup_gym_file : str + Generates backup of environment file in model's folder + Default is None (no backup) + export_name : str + If export_name and save_freq are defined, a model is exported every X steps + Default is None (no export) + + Returns + ------- + model_path : str + Directory where model was actually saved (considering incremental suffix) + + Notes + ----- + If `eval_env` and `eval_freq` were specified: + - The policy will be evaluated in `eval_env` every `eval_freq` steps + - Evaluation results will be saved in `path` and shown at the end of training + - Every time the results improve, the model is saved + ''' + + start = time.time() + start_date = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + + # If path already exists, add suffix to avoid overwriting + if os.path.isdir(path): + for i in count(): + p = path.rstrip("/")+f'_{i:03}/' + if not os.path.isdir(p): + path = p + break + os.makedirs(path) + + # Backup environment file + if backup_env_file is not None: + backup_file = os.path.join(path, os.path.basename(backup_env_file)) + copy(backup_env_file, backup_file) + + evaluate = bool(eval_env is not None and eval_freq is not None) + + # Create evaluation callback + eval_callback = None if not evaluate else EvalCallback(eval_env, n_eval_episodes=eval_eps, eval_freq=eval_freq, log_path=path, + best_model_save_path=path, deterministic=True, render=False) + + # Create custom callback to display evaluations + custom_callback = None if not evaluate else Cyclic_Callback(eval_freq, lambda:self.display_evaluations(path,True)) + + # Create checkpoint callback + checkpoint_callback = None if save_freq is None else CheckpointCallback(save_freq=save_freq, save_path=path, name_prefix="model", verbose=1) + + # Create custom callback to export checkpoint models + export_callback = None if save_freq is None or export_name is None else Export_Callback(save_freq, path, export_name) + + callbacks = CallbackList([c for c in [eval_callback, custom_callback, checkpoint_callback, export_callback] if c is not None]) + + model.learn( total_timesteps=total_steps, callback=callbacks ) + model.save( os.path.join(path, "last_model") ) + + # Display evaluations if they exist + if evaluate: + self.display_evaluations(path) + + # Display timestamps + Model path + end_date = datetime.now().strftime('%d/%m/%Y %H:%M:%S') + duration = timedelta(seconds=int(time.time()-start)) + print(f"Train start: {start_date}") + print(f"Train end: {end_date}") + print(f"Train duration: {duration}") + print(f"Model path: {path}") + + # Append timestamps to backup environment file + if backup_env_file is not None: + with open(backup_file, 'a') as f: + f.write(f"\n# Train start: {start_date}\n") + f.write( f"# Train end: {end_date}\n") + f.write( f"# Train duration: {duration}") + + return path + + def display_evaluations(self, path, save_csv=False): + + eval_npz = os.path.join(path, "evaluations.npz") + + if not os.path.isfile(eval_npz): + return + + console_width = 80 + console_height = 18 + symb_x = "\u2022" + symb_o = "\u007c" + symb_xo = "\u237f" + + with np.load(eval_npz) as data: + time_steps = data["timesteps"] + results_raw = np.mean(data["results"],axis=1) + ep_lengths_raw = np.mean(data["ep_lengths"],axis=1) + sample_no = len(results_raw) + + xvals = np.linspace(0, sample_no-1, 80) + results = np.interp(xvals, range(sample_no), results_raw) + ep_lengths = np.interp(xvals, range(sample_no), ep_lengths_raw) + + results_limits = np.min(results), np.max(results) + ep_lengths_limits = np.min(ep_lengths), np.max(ep_lengths) + + results_discrete = np.digitize(results, np.linspace(results_limits[0]-1e-5, results_limits[1]+1e-5, console_height+1))-1 + ep_lengths_discrete = np.digitize(ep_lengths, np.linspace(0, ep_lengths_limits[1]+1e-5, console_height+1))-1 + + matrix = np.zeros((console_height, console_width, 2), int) + matrix[results_discrete[0] ][0][0] = 1 # draw 1st column + matrix[ep_lengths_discrete[0]][0][1] = 1 # draw 1st column + rng = [[results_discrete[0], results_discrete[0]], [ep_lengths_discrete[0], ep_lengths_discrete[0]]] + + # Create continuous line for both plots + for k in range(2): + for i in range(1,console_width): + x = [results_discrete, ep_lengths_discrete][k][i] + if x > rng[k][1]: + rng[k] = [rng[k][1]+1, x] + elif x < rng[k][0]: + rng[k] = [x, rng[k][0]-1] + else: + rng[k] = [x,x] + for j in range(rng[k][0],rng[k][1]+1): + matrix[j][i][k] = 1 + + print(f'{"-"*console_width}') + for l in reversed(range(console_height)): + for c in range(console_width): + if np.all(matrix[l][c] == 0): print(end=" ") + elif np.all(matrix[l][c] == 1): print(end=symb_xo) + elif matrix[l][c][0] == 1: print(end=symb_x) + else: print(end=symb_o) + print() + print(f'{"-"*console_width}') + print(f"({symb_x})-reward min:{results_limits[0]:11.2f} max:{results_limits[1]:11.2f}") + print(f"({symb_o})-ep. length min:{ep_lengths_limits[0]:11.0f} max:{ep_lengths_limits[1]:11.0f} {time_steps[-1]/1000:15.0f}k steps") + print(f'{"-"*console_width}') + + # save CSV + if save_csv: + eval_csv = os.path.join(path, "evaluations.csv") + with open(eval_csv, 'a+') as f: + writer = csv.writer(f) + if sample_no == 1: + writer.writerow(["time_steps", "reward ep.", "length"]) + writer.writerow([time_steps[-1],results_raw[-1],ep_lengths_raw[-1]]) + + + def generate_slot_behavior(self, path, slots, auto_head:bool, XML_name): + ''' + Function that generates the XML file for the optimized slot behavior, overwriting previous files + ''' + + file = os.path.join( path, XML_name ) + + # create the file structure + auto_head = '1' if auto_head else '0' + EL_behavior = ET.Element('behavior',{'description':'Add description to XML file', "auto_head":auto_head}) + + for i,s in enumerate(slots): + EL_slot = ET.SubElement(EL_behavior, 'slot', {'name':str(i), 'delta':str(s[0]/1000)}) + for j in s[1]: # go through all joint indices + ET.SubElement(EL_slot, 'move', {'id':str(j), 'angle':str(s[2][j])}) + + # create XML file + xml_rough = ET.tostring( EL_behavior, 'utf-8' ) + xml_pretty = minidom.parseString(xml_rough).toprettyxml(indent=" ") + with open(file, "w") as x: + x.write(xml_pretty) + + print(file, "was created!") + + @staticmethod + def linear_schedule(initial_value: float) -> Callable[[float], float]: + ''' + Linear learning rate schedule + + Parameters + ---------- + initial_value : float + Initial learning rate + + Returns + ------- + schedule : Callable[[float], float] + schedule that computes current learning rate depending on remaining progress + ''' + def func(progress_remaining: float) -> float: + ''' + Compute learning rate according to current progress + + Parameters + ---------- + progress_remaining : float + Progress will decrease from 1 (beginning) to 0 + + Returns + ------- + learning_rate : float + Learning rate according to current progress + ''' + return progress_remaining * initial_value + + return func + + @staticmethod + def export_model(input_file, output_file, add_sufix=True): + ''' + Export model weights to binary file + + Parameters + ---------- + input_file : str + Input file, compatible with algorithm + output_file : str + Output file, including directory + add_sufix : bool + If true, a suffix is appended to the file name: output_file + "_{index}.pkl" + ''' + + # If file already exists, don't overwrite + if add_sufix: + for i in count(): + f = f"{output_file}_{i:03}.pkl" + if not os.path.isfile(f): + output_file = f + break + + model = PPO.load(input_file) + weights = model.policy.state_dict() # dictionary containing network layers + + w = lambda name : weights[name].detach().cpu().numpy() # extract weights from policy + + var_list = [] + for i in count(0,2): # add hidden layers (step=2 because that's how SB3 works) + if f"mlp_extractor.policy_net.{i}.bias" not in weights: + break + var_list.append([w(f"mlp_extractor.policy_net.{i}.bias"), w(f"mlp_extractor.policy_net.{i}.weight"), "tanh"]) + + var_list.append( [w("action_net.bias"), w("action_net.weight"), "none"] ) # add final layer + + with open(output_file,"wb") as f: + pickle.dump(var_list, f, protocol=4) # protocol 4 is backward compatible with Python 3.4 + + + +class Cyclic_Callback(BaseCallback): + ''' Stable baselines custom callback ''' + def __init__(self, freq, function): + super(Cyclic_Callback, self).__init__(1) + self.freq = freq + self.function = function + + def _on_step(self) -> bool: + if self.n_calls % self.freq == 0: + self.function() + return True # If the callback returns False, training is aborted early + +class Export_Callback(BaseCallback): + ''' Stable baselines custom callback ''' + def __init__(self, freq, load_path, export_name): + super(Export_Callback, self).__init__(1) + self.freq = freq + self.load_path = load_path + self.export_name = export_name + + def _on_step(self) -> bool: + if self.n_calls % self.freq == 0: + path = os.path.join(self.load_path, f"model_{self.num_timesteps}_steps.zip") + Train_Base.export_model(path, f"./scripts/gyms/export/{self.export_name}") + return True # If the callback returns False, training is aborted early \ No newline at end of file diff --git a/scripts/commons/UI.py b/scripts/commons/UI.py new file mode 100644 index 0000000..e1869e4 --- /dev/null +++ b/scripts/commons/UI.py @@ -0,0 +1,302 @@ +from itertools import zip_longest +from math import inf +import math +import numpy as np +import shutil + +class UI(): + console_width = 80 + console_height = 24 + + @staticmethod + def read_particle(prompt, str_options, dtype=str, interval=[-inf,inf]): + ''' + Read particle from user from a given dtype or from a str_options list + + Parameters + ---------- + prompt : `str` + prompt to show user before reading input + str_options : `list` + list of str options (in addition to dtype if dtype is not str) + dtype : `class` + if dtype is str, then user must choose a value from str_options, otherwise it can also send a dtype value + interval : `list` + [>=min,= interval[0] and inp < interval[1]: + return inp, False + except: + pass + + print("Error: illegal input! Options:", str_options, f" or {dtype}" if dtype != str else "") + + @staticmethod + def read_int(prompt, min, max): + ''' + Read int from user in a given interval + :param prompt: prompt to show user before reading input + :param min: minimum input (inclusive) + :param max: maximum input (exclusive) + :return: choice + ''' + while True: + inp = input(prompt) + try: + inp = int(inp) + assert inp >= min and inp < max + return inp + except: + print(f"Error: illegal input! Choose number between {min} and {max-1}") + + @staticmethod + def print_table(data, titles=None, alignment=None, cols_width=None, cols_per_title=None, margins=None, numbering=None, prompt=None): + ''' + Print table + + Parameters + ---------- + data : `list` + list of columns, where each column is a list of items + titles : `list` + list of titles for each column, default is `None` (no titles) + alignment : `list` + list of alignments per column (excluding titles), default is `None` (left alignment for all cols) + cols_width : `list` + list of widths per column, default is `None` (fit to content) + Positive values indicate a fixed column width + Zero indicates that the column will fit its content + cols_per_title : `list` + maximum number of subcolumns per title, default is `None` (1 subcolumn per title) + margins : `list` + number of added leading and trailing spaces per column, default is `None` (margin=2 for all columns) + numbering : `list` + list of booleans per columns, indicating whether to assign numbers to each option + prompt : `str` + the prompt string, if given, is printed after the table before reading input + + Returns + ------- + index : `int` + returns global index of selected item (relative to table) + col_index : `int` + returns local index of selected item (relative to column) + column : `int` + returns number of column of selected item (starts at 0) + * if `numbering` or `prompt` are `None`, `None` is returned + + + Example + ------- + titles = ["Name","Age"] + data = [[John,Graciete], [30,50]] + alignment = ["<","^"] # 1st column is left-aligned, 2nd is centered + cols_width = [10,5] # 1st column's width=10, 2nd column's width=5 + margins = [3,3] + numbering = [True,False] # prints: [0-John,1-Graciete][30,50] + prompt = "Choose a person:" + ''' + + #--------------------------------------------- parameters + cols_no = len(data) + + if alignment is None: + alignment = ["<"]*cols_no + + if cols_width is None: + cols_width = [0]*cols_no + + if numbering is None: + numbering = [False]*cols_no + any_numbering = False + else: + any_numbering = True + + if margins is None: + margins = [2]*cols_no + + # Fit column to content + margin, if required + subcol = [] # subcolumn length and widths + for i in range(cols_no): + subcol.append([[],[]]) + if cols_width[i] == 0: + numbering_width = 4 if numbering[i] else 0 + if cols_per_title is None or cols_per_title[i] < 2: + cols_width[i] = max([len(str(item))+numbering_width for item in data[i]]) + margins[i]*2 + else: + subcol[i][0] = math.ceil(len(data[i])/cols_per_title[i]) # subcolumn maximum length + cols_per_title[i] = math.ceil(len(data[i])/subcol[i][0]) # reduce number of columns as needed + cols_width[i] = margins[i]*(1+cols_per_title[i]) - (1 if numbering[i] else 0) # remove one if numbering, same as when printing + for j in range(cols_per_title[i]): + subcol_data_width = max([len(str(item))+numbering_width for item in data[i][j*subcol[i][0]:j*subcol[i][0]+subcol[i][0]]]) + cols_width[i] += subcol_data_width # add subcolumn data width to column width + subcol[i][1].append(subcol_data_width) # save subcolumn data width + + if titles is not None: # expand to acomodate titles if needed + cols_width[i] = max(cols_width[i], len(titles[i]) + margins[i]*2 ) + + if any_numbering: + no_of_items=0 + cumulative_item_per_col=[0] # useful for getting the local index + for i in range(cols_no): + assert type(data[i]) == list, "In function 'print_table', 'data' must be a list of lists!" + + if numbering[i]: + data[i] = [f"{n+no_of_items:3}-{d}" for n,d in enumerate(data[i])] + no_of_items+=len(data[i]) + cumulative_item_per_col.append(no_of_items) + + table_width = sum(cols_width)+cols_no-1 + + #--------------------------------------------- col titles + print(f'{"="*table_width}') + if titles is not None: + for i in range(cols_no): + print(f'{titles[i]:^{cols_width[i]}}', end='|' if i < cols_no - 1 else '') + print() + for i in range(cols_no): + print(f'{"-"*cols_width[i]}', end='+' if i < cols_no - 1 else '') + print() + + #--------------------------------------------- merge subcolumns + if cols_per_title is not None: + for i,col in enumerate(data): + if cols_per_title[i] < 2: + continue + for k in range(subcol[i][0]): # create merged items + col[k] = (" "*margins[i]).join( f'{col[item]:{alignment[i]}{subcol[i][1][subcol_idx]}}' + for subcol_idx, item in enumerate(range(k,len(col),subcol[i][0])) ) + del col[subcol[i][0]:] # delete repeated items + + #--------------------------------------------- col items + for line in zip_longest(*data): + for i,item in enumerate(line): + l_margin = margins[i]-1 if numbering[i] else margins[i] # adjust margins when there are numbered options + item = "" if item is None else f'{" "*l_margin}{item}{" "*margins[i]}' # add margins + print(f'{item:{alignment[i]}{cols_width[i]}}', end='') + if i < cols_no - 1: + print(end='|') + print(end="\n") + print(f'{"="*table_width}') + + #--------------------------------------------- prompt + if prompt is None: + return None + + if not any_numbering: + print(prompt) + return None + + index = UI.read_int(prompt, 0, no_of_items) + + for i,n in enumerate(cumulative_item_per_col): + if index < n: + return index, index-cumulative_item_per_col[i-1], i-1 + + raise ValueError('Failed to catch illegal input') + + + @staticmethod + def print_list(data, numbering=True, prompt=None, divider=" | ", alignment="<", min_per_col=6): + ''' + Print list - prints list, using as many columns as possible + + Parameters + ---------- + data : `list` + list of items + numbering : `bool` + assigns number to each option + prompt : `str` + the prompt string, if given, is printed after the table before reading input + divider : `str` + string that divides columns + alignment : `str` + f-string style alignment ( '<', '>', '^' ) + min_per_col : int + avoid splitting columns with fewer items + + Returns + ------- + item : `int`, item + returns tuple with global index of selected item and the item object, + or `None` (if `numbering` or `prompt` are `None`) + + ''' + + WIDTH = shutil.get_terminal_size()[0] + + data_size = len(data) + items = [] + items_len = [] + + #--------------------------------------------- Add numbers, margins and divider + for i in range(data_size): + number = f"{i}-" if numbering else "" + items.append( f"{divider}{number}{data[i]}" ) + items_len.append( len(items[-1]) ) + + max_cols = np.clip((WIDTH+len(divider)) // min(items_len),1,math.ceil(data_size/max(min_per_col,1))) # width + len(divider) because it is not needed in last col + + #--------------------------------------------- Check maximum number of columns, considering content width (min:1) + for i in range(max_cols,0,-1): + cols_width = [] + cols_items = [] + table_width = 0 + a,b = divmod(data_size,i) + for col in range(i): + start = a*col + min(b,col) + end = start+a+(1 if col (px,py,pz,fx,fy,fz)* + self.obs[18:24] = r.frp.get('rf', (0,0,0,0,0,0)) # right foot: relative point of origin (p) and force vector (f) -> (px,py,pz,fx,fy,fz)* + self.obs[15:18] /= 100 # naive normalization of force vector + self.obs[21:24] /= 100 # naive normalization of force vector + self.obs[24:44] = r.joints_position[2:22] /100 # position of all joints except head & toes (for robot type 4) + self.obs[44:64] = r.joints_speed[2:22] /6.1395 # speed of all joints except head & toes (for robot type 4) + # *if foot is not touching the ground, then (px=0,py=0,pz=0,fx=0,fy=0,fz=0) + + if init: # the walking parameters refer to the last parameters in effect (after a reset, they are pointless) + self.obs[64] = self.step_default_dur /10 # step duration in time steps + self.obs[65] = self.step_default_z_span *20 # vertical movement span + self.obs[66] = self.step_default_z_max # relative extension of support leg + self.obs[67] = 1 # step progress + self.obs[68] = 1 # 1 if left leg is active + self.obs[69] = 0 # 1 if right leg is active + else: + self.obs[64] = self.step_obj.step_generator.ts_per_step /10 # step duration in time steps + self.obs[65] = self.step_obj.step_generator.swing_height *20 # vertical movement span + self.obs[66] = self.step_obj.step_generator.max_leg_extension / self.step_obj.leg_length # relative extension of support leg + self.obs[67] = self.step_obj.step_generator.external_progress # step progress + self.obs[68] = float(self.step_obj.step_generator.state_is_left_active) # 1 if left leg is active + self.obs[69] = float(not self.step_obj.step_generator.state_is_left_active) # 1 if right leg is active + + ''' + Expected observations for walking parameters/state (example): + Time step R 0 1 2 0 1 2 3 4 + Progress 1 0 .5 1 0 .25 .5 .75 1 + Left leg active T F F F T T T T T + Parameters A A A B B B B B C + Example note: (A) has a step duration of 3ts, (B) has a step duration of 5ts + ''' + + return self.obs + + def sync(self): + ''' Run a single simulation step ''' + r = self.player.world.robot + self.player.scom.commit_and_send( r.get_command() ) + self.player.scom.receive() + + + def reset(self): + ''' + Reset and stabilize the robot + Note: for some behaviors it would be better to reduce stabilization or add noise + ''' + + self.step_counter = 0 + r = self.player.world.robot + + for _ in range(25): + self.player.scom.unofficial_beam((-14,0,0.50),0) # beam player continuously (floating above ground) + self.player.behavior.execute("Zero_Bent_Knees") + self.sync() + + # beam player to ground + self.player.scom.unofficial_beam((-14,0,r.beam_height),0) + r.joints_target_speed[0] = 0.01 # move head to trigger physics update (rcssserver3d bug when no joint is moving) + self.sync() + + # stabilize on ground + for _ in range(7): + self.player.behavior.execute("Zero_Bent_Knees") + self.sync() + + # memory variables + self.lastx = r.cheat_abs_pos[0] + self.act = np.zeros(self.no_of_actions,np.float32) + + return self.observe(True) + + def render(self, mode='human', close=False): + return + + def close(self): + Draw.clear_all() + self.player.terminate() + + def step(self, action): + + r = self.player.world.robot + + # exponential moving average + self.act = 0.4 * self.act + 0.6 * action + + # execute Step behavior to extract the target positions of each leg (we will override these targets) + if self.step_counter == 0: + ''' + The first time step will change the parameters of the next footstep + It uses default parameters so that the agent can anticipate the next generated pose + Reason: the agent decides the parameters during the previous footstep + ''' + self.player.behavior.execute("Step", self.step_default_dur, self.step_default_z_span, self.step_default_z_max) + else: + + step_zsp = np.clip(self.step_default_z_span + self.act[20]/300, 0, 0.07) + step_zmx = np.clip(self.step_default_z_max + self.act[21]/30, 0.6, 0.9) + + self.player.behavior.execute("Step", self.step_default_dur, step_zsp, step_zmx) + + + # add action as residuals to Step behavior (the index of these actions is not the typical index because both head joints are excluded) + new_action = self.act[:20] * 2 # scale up actions to motivate exploration + new_action[[0,2,4,6,8,10]] += self.step_obj.values_l + new_action[[1,3,5,7,9,11]] += self.step_obj.values_r + new_action[12] -= 90 # arms down + new_action[13] -= 90 # arms down + new_action[16] += 90 # untwist arms + new_action[17] += 90 # untwist arms + new_action[18] += 90 # elbows at 90 deg + new_action[19] += 90 # elbows at 90 deg + + r.set_joints_target_position_direct( # commit actions: + slice(2,22), # act on all joints except head & toes (for robot type 4) + new_action, # target joint positions + harmonize=False # there is no point in harmonizing actions if the targets change at every step + ) + + self.sync() # run simulation step + self.step_counter += 1 + + reward = r.cheat_abs_pos[0] - self.lastx + self.lastx = r.cheat_abs_pos[0] + + # terminal state: the robot is falling or timeout + terminal = r.cheat_abs_pos[2] < 0.3 or self.step_counter > 300 + + return self.observe(), reward, terminal, {} + + + + + +class Train(Train_Base): + def __init__(self, script) -> None: + super().__init__(script) + + + def train(self, args): + + #--------------------------------------- Learning parameters + n_envs = min(16, os.cpu_count()) + n_steps_per_env = 1024 # RolloutBuffer is of size (n_steps_per_env * n_envs) + minibatch_size = 64 # should be a factor of (n_steps_per_env * n_envs) + total_steps = 30000000 + learning_rate = 3e-4 + folder_name = f'Basic_Run_R{self.robot_type}' + model_path = f'./scripts/gyms/logs/{folder_name}/' + + print("Model path:", model_path) + + #--------------------------------------- Run algorithm + def init_env(i_env): + def thunk(): + return Basic_Run( self.ip , self.server_p + i_env, self.monitor_p_1000 + i_env, self.robot_type, False ) + return thunk + + servers = Server( self.server_p, self.monitor_p_1000, n_envs+1 ) #include 1 extra server for testing + + env = SubprocVecEnv( [init_env(i) for i in range(n_envs)] ) + eval_env = SubprocVecEnv( [init_env(n_envs)] ) + + try: + if "model_file" in args: # retrain + model = PPO.load( args["model_file"], env=env, device="cpu", n_envs=n_envs, n_steps=n_steps_per_env, batch_size=minibatch_size, learning_rate=learning_rate ) + else: # train new model + model = PPO( "MlpPolicy", env=env, verbose=1, n_steps=n_steps_per_env, batch_size=minibatch_size, learning_rate=learning_rate, device="cpu" ) + + model_path = self.learn_model( model, total_steps, model_path, eval_env=eval_env, eval_freq=n_steps_per_env*20, save_freq=n_steps_per_env*200, backup_env_file=__file__ ) + except KeyboardInterrupt: + sleep(1) # wait for child processes + print("\nctrl+c pressed, aborting...\n") + servers.kill() + return + + env.close() + eval_env.close() + servers.kill() + + + def test(self, args): + + # Uses different server and monitor ports + server = Server( self.server_p-1, self.monitor_p, 1 ) + env = Basic_Run( self.ip, self.server_p-1, self.monitor_p, self.robot_type, True ) + model = PPO.load( args["model_file"], env=env ) + + try: + self.export_model( args["model_file"], args["model_file"]+".pkl", False ) # Export to pkl to create custom behavior + self.test_model( model, env, log_path=args["folder_dir"], model_path=args["folder_dir"] ) + except KeyboardInterrupt: + print() + + env.close() + server.kill() + + +''' +The learning process takes several hours. +A video with the results can be seen at: +https://imgur.com/a/dC2V6Et + +Stats: +- Avg. reward: 7.7 +- Avg. ep. length: 5.5s (episode is limited to 6s) +- Max. reward: 9.3 (speed: 1.55m/s) + +State space: +- Composed of all joint positions + torso height +- Stage of the underlying Step behavior + +Reward: +- Displacement in the x-axis (it can be negative) +- Note that cheat and visual data is only updated every 3 steps +''' diff --git a/scripts/gyms/Fall.py b/scripts/gyms/Fall.py new file mode 100644 index 0000000..9f48ac8 --- /dev/null +++ b/scripts/gyms/Fall.py @@ -0,0 +1,214 @@ +from agent.Base_Agent import Base_Agent as Agent +from world.commons.Draw import Draw +from stable_baselines3 import PPO +from stable_baselines3.common.vec_env import SubprocVecEnv +from scripts.commons.Server import Server +from scripts.commons.Train_Base import Train_Base +from time import sleep +import os, gym +import numpy as np + +''' +Objective: +Learn how to fall (simplest example) +---------- +- class Fall: implements an OpenAI custom gym +- class Train: implements algorithms to train a new model or test an existing model +''' + +class Fall(gym.Env): + def __init__(self, ip, server_p, monitor_p, r_type, enable_draw) -> None: + + self.robot_type = r_type + + # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name, Enable Log, Enable Draw + self.player = Agent(ip, server_p, monitor_p, 1, self.robot_type, "Gym", True, enable_draw) + self.step_counter = 0 # to limit episode size + + # State space + self.no_of_joints = self.player.world.robot.no_of_joints + self.obs = np.zeros(self.no_of_joints + 1, np.float32) # joints + torso height + self.observation_space = gym.spaces.Box(low=np.full(len(self.obs),-np.inf,np.float32), high=np.full(len(self.obs),np.inf,np.float32), dtype=np.float32) + + # Action space + MAX = np.finfo(np.float32).max + no_of_actions = self.no_of_joints + self.action_space = gym.spaces.Box(low=np.full(no_of_actions,-MAX,np.float32), high=np.full(no_of_actions,MAX,np.float32), dtype=np.float32) + + # Check if cheats are enabled + assert np.any(self.player.world.robot.cheat_abs_pos), "Cheats are not enabled! Run_Utils.py -> Server -> Cheats" + + + def observe(self): + + r = self.player.world.robot + + for i in range(self.no_of_joints): + self.obs[i] = r.joints_position[i] / 100 # naive scale normalization + + self.obs[self.no_of_joints] = r.cheat_abs_pos[2] # head.z (alternative: r.loc_head_z) + + return self.obs + + def sync(self): + ''' Run a single simulation step ''' + r = self.player.world.robot + self.player.scom.commit_and_send( r.get_command() ) + self.player.scom.receive() + + + def reset(self): + ''' + Reset and stabilize the robot + Note: for some behaviors it would be better to reduce stabilization or add noise + ''' + + self.step_counter = 0 + r = self.player.world.robot + + for _ in range(25): + self.player.scom.unofficial_beam((-3,0,0.50),0) # beam player continuously (floating above ground) + self.player.behavior.execute("Zero") + self.sync() + + # beam player to ground + self.player.scom.unofficial_beam((-3,0,r.beam_height),0) + r.joints_target_speed[0] = 0.01 # move head to trigger physics update (rcssserver3d bug when no joint is moving) + self.sync() + + # stabilize on ground + for _ in range(7): + self.player.behavior.execute("Zero") + self.sync() + + return self.observe() + + def render(self, mode='human', close=False): + return + + def close(self): + Draw.clear_all() + self.player.terminate() + + def step(self, action): + + r = self.player.world.robot + r.set_joints_target_position_direct( # commit actions: + slice(self.no_of_joints), # act on all available joints + action*10, # scale actions up to motivate early exploration + harmonize=False # there is no point in harmonizing actions if the targets change at every step + ) + + self.sync() # run simulation step + self.step_counter += 1 + self.observe() + + if self.obs[-1] < 0.15: # terminal state: the robot has fallen successfully + return self.obs, 1, True, {} # Reward: 1 (this reward will motivate a fast reaction if the return is discounted) + elif self.step_counter > 150: # terminal state: 3s passed and robot has not fallen (may be stuck) + return self.obs, 0, True, {} + else: + return self.obs, 0, False, {} # Reward: 0 + + + + + +class Train(Train_Base): + def __init__(self, script) -> None: + super().__init__(script) + + + def train(self, args): + + #--------------------------------------- Learning parameters + n_envs = min(4, os.cpu_count()) + n_steps_per_env = 128 # RolloutBuffer is of size (n_steps_per_env * n_envs) (*RV: >=2048) + minibatch_size = 64 # should be a factor of (n_steps_per_env * n_envs) + total_steps = 50000 # (*RV: >=10M) + learning_rate = 30e-4 # (*RV: 3e-4) + # *RV -> Recommended value for more complex environments + folder_name = f'Fall_R{self.robot_type}' + model_path = f'./scripts/gyms/logs/{folder_name}/' + + print("Model path:", model_path) + + #--------------------------------------- Run algorithm + def init_env(i_env): + def thunk(): + return Fall( self.ip , self.server_p + i_env, self.monitor_p_1000 + i_env, self.robot_type, False ) + return thunk + + servers = Server( self.server_p, self.monitor_p_1000, n_envs+1 ) #include 1 extra server for testing + + env = SubprocVecEnv( [init_env(i) for i in range(n_envs)] ) + eval_env = SubprocVecEnv( [init_env(n_envs)] ) + + try: + if "model_file" in args: # retrain + model = PPO.load( args["model_file"], env=env, n_envs=n_envs, n_steps=n_steps_per_env, batch_size=minibatch_size, learning_rate=learning_rate ) + else: # train new model + model = PPO( "MlpPolicy", env=env, verbose=1, n_steps=n_steps_per_env, batch_size=minibatch_size, learning_rate=learning_rate ) + + model_path = self.learn_model( model, total_steps, model_path, eval_env=eval_env, eval_freq=n_steps_per_env*10, save_freq=n_steps_per_env*20, backup_env_file=__file__ ) + except KeyboardInterrupt: + sleep(1) # wait for child processes + print("\nctrl+c pressed, aborting...\n") + servers.kill() + return + + env.close() + eval_env.close() + servers.kill() + + + def test(self, args): + + # Uses different server and monitor ports + server = Server( self.server_p-1, self.monitor_p, 1 ) + env = Fall( self.ip, self.server_p-1, self.monitor_p, self.robot_type, True ) + model = PPO.load( args["model_file"], env=env ) + + try: + self.export_model( args["model_file"], args["model_file"]+".pkl", False ) # Export to pkl to create custom behavior + self.test_model( model, env, log_path=args["folder_dir"], model_path=args["folder_dir"] ) + except KeyboardInterrupt: + print() + + env.close() + server.kill() + + +''' +The learning process takes about 5 minutes. +A video with the results can be seen at: +https://imgur.com/a/KvpXS41 + +State space: +- Composed of all joint positions + torso height +- The number of joint positions is different for robot type 4, so the models are not interchangeable +- For this example, this problem can be avoided by using only the first 22 joints and actuators + +Reward: +- The reward for falling is 1, which means that after a while every episode will have a r=1. +- What is the incetive for the robot to fall faster? Discounted return. + In every state, the algorithm will seek short-term rewards. +- During training, the best model is saved according to the average return, which is almost always 1. + Therefore, the last model will typically be superior for this example. + +Expected evolution of episode length: + 3s|o + |o + | o + | o + | oo + | ooooo + 0.4s| oooooooooooooooo + |------------------------------> time + + +This example scales poorly with the number of CPUs because: +- It uses a small rollout buffer (n_steps_per_env * n_envs) +- The simulation workload is light +- For these reasons, the IPC overhead is significant +''' diff --git a/scripts/utils/Beam.py b/scripts/utils/Beam.py new file mode 100644 index 0000000..5d71647 --- /dev/null +++ b/scripts/utils/Beam.py @@ -0,0 +1,60 @@ + +from agent.Base_Agent import Base_Agent as Agent +from scripts.commons.Script import Script +from time import sleep + + +class Beam(): + def __init__(self, script:Script) -> None: + self.script = script + + def ask_for_input(self,prompt, default): + try: + inp=input(prompt) + return float(inp) + except ValueError: + if inp != '': + print("Illegal input:", inp, "\n") + return default + + def beam_and_update(self,x,y,rot): + r = self.player.world.robot + d = self.player.world.draw + + d.annotation((x,y,0.7), f"x:{x} y:{y} r:{rot}", d.Color.yellow, "pos_label") + + self.player.scom.unofficial_beam((x,y,r.beam_height),rot) + for _ in range(10): # run multiple times to beam and then simulate eventual collisions (e.g. goal posts) + sleep(0.03) + self.player.behavior.execute("Zero") + self.player.scom.commit_and_send( r.get_command() ) + self.player.scom.receive() + + def execute(self): + + a = self.script.args + self.player = Agent(a.i, a.p, a.m, a.u, a.r, a.t) # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name + d = self.player.world.draw + + self.player.scom.unofficial_set_play_mode("PlayOn") + + # Draw grid + for x in range(-15,16): + for y in range(-10,11): + d.point((x,y), 6, d.Color.red, "grid", False) + d.flush("grid") + + for _ in range(10): # Initialize + self.player.scom.send() + self.player.scom.receive() + + print("\nBeam player to coordinates + orientation:") + + x=y=a=0 + while True: # Beam self.player to given position + x = self.ask_for_input(f"\nInput x coordinate ('' to send {x:5} again, ctrl+c to return): ",x) + self.beam_and_update(x,y,a) + y = self.ask_for_input( f"Input y coordinate ('' to send {y:5} again, ctrl+c to return): ",y) + self.beam_and_update(x,y,a) + a = self.ask_for_input( f"Orientation -180 to 180 ('' to send {a:5} again, ctrl+c to return): ",a) + self.beam_and_update(x,y,a) \ No newline at end of file diff --git a/scripts/utils/Behaviors.py b/scripts/utils/Behaviors.py new file mode 100644 index 0000000..653c923 --- /dev/null +++ b/scripts/utils/Behaviors.py @@ -0,0 +1,55 @@ +from agent.Base_Agent import Base_Agent as Agent +from scripts.commons.Script import Script +from scripts.commons.UI import UI + +class Behaviors(): + + def __init__(self,script:Script) -> None: + self.script = script + self.player : Agent = None + + def ask_for_behavior(self): + names, descriptions = self.player.behavior.get_all_behaviors() + + UI.print_table( [names,descriptions], ["Behavior Name","Description"], numbering=[True,False]) + choice, is_str_opt = UI.read_particle('Choose behavior ("" to skip 2 time steps, "b" to beam, ctrl+c to return): ',["","b"],int,[0,len(names)]) + if is_str_opt: return choice #skip 2 time steps or quit + return names[choice] + + def sync(self): + self.player.scom.commit_and_send( self.player.world.robot.get_command() ) + self.player.scom.receive() + + def beam(self): + self.player.scom.unofficial_beam((-2.5,0,self.player.world.robot.beam_height),0) + for _ in range(5): + self.sync() + + def execute(self): + + a = self.script.args + self.player = Agent(a.i, a.p, a.m, a.u, a.r, a.t) # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name + behavior = self.player.behavior + + self.beam() + self.player.scom.unofficial_set_play_mode("PlayOn") + + # Special behaviors + special_behaviors = {"Step":(), "Basic_Kick":(0,), "Walk":((0.5,0),False,0,False,None), "Dribble":(None,None)} + + while True: + behavior_name = self.ask_for_behavior() + if behavior_name == 0: # skip 2 time steps (user request) + self.sync() + self.sync() + elif behavior_name == 1: # beam + self.beam() + else: + if behavior_name in special_behaviors: # not using execute_to_completion to abort behavior after a timeout + duration = UI.read_int("For how many time steps [1,1000]? ", 1, 1001) + for _ in range(duration): + if behavior.execute(behavior_name, *special_behaviors[behavior_name]): + break # break if behavior ends + self.sync() + else: + behavior.execute_to_completion(behavior_name) \ No newline at end of file diff --git a/scripts/utils/Drawings.py b/scripts/utils/Drawings.py new file mode 100644 index 0000000..2ffddd4 --- /dev/null +++ b/scripts/utils/Drawings.py @@ -0,0 +1,35 @@ +from time import sleep +from world.commons.Draw import Draw + + +class Drawings(): + + def __init__(self,script) -> None: + self.script = script + + def execute(self): + + # Creating a draw object is done automatically for each agent + # This is a shortcut to draw shapes without creating an agent + # Usually, we can access the object through player.world.draw + a = self.script.args + draw = Draw(True, 0, a.i, 32769) + + print("\nPress ctrl+c to return.") + + while True: + for i in range(100): + sleep(0.02) + + draw.circle( (0,0),i/10,2, Draw.Color.green_light,"green") + draw.circle( (0,0),i/9,2, Draw.Color.red,"red") + draw.sphere( (0,0,5-i/20),0.2, Draw.Color.red,"ball" ) + draw.annotation((0,0,1), "Hello!", Draw.Color.cyan, "text") + draw.arrow( (0,0,5), (0,0,5-i/25), 0.5, 4, Draw.Color.get(127,50,255), "my_arrow") + + #draw pyramid + draw.polygon(((2,0,0),(3,0,0),(3,1,0),(2,1,0)), Draw.Color.blue, 255, "solid", False) + draw.line( (2,0,0), (2.5,0.5,1), 2, Draw.Color.cyan, "solid", False) + draw.line( (3,0,0), (2.5,0.5,1), 2, Draw.Color.cyan, "solid", False) + draw.line( (2,1,0), (2.5,0.5,1), 2, Draw.Color.cyan, "solid", False) + draw.line( (3,1,0), (2.5,0.5,1), 2, Draw.Color.cyan, "solid", True) \ No newline at end of file diff --git a/scripts/utils/Dribble.py b/scripts/utils/Dribble.py new file mode 100644 index 0000000..fb776e8 --- /dev/null +++ b/scripts/utils/Dribble.py @@ -0,0 +1,54 @@ +from agent.Agent import Agent +from agent.Base_Agent import Base_Agent +from scripts.commons.Script import Script +import numpy as np + +''' +Objective: +---------- +Dribble and score +''' + +class Dribble(): + def __init__(self, script:Script) -> None: + self.script = script + + def execute(self): + + a = self.script.args + + # Args: Server IP, Agent Port, Monitor Port, Uniform No., [Robot Type] (for Base_Agent), Team name, Enable Log, Enable Draw + self.script.batch_create(Base_Agent, ((a.i,a.p,a.m,a.u,a.r,a.t,True,True),)) # one dribbler + self.script.batch_create(Agent, ((a.i,a.p,a.m,u,"Opponent",False,False) for u in range(1,2))) # 1 opponent (normal agent) + + p : Base_Agent = self.script.players[0] + p.path_manager.draw_options(enable_obstacles=True, enable_path=True) + + behavior = p.behavior + w = p.world + r = w.robot + d = w.draw + + p.scom.unofficial_beam((-3,0,r.beam_height),0) + p.scom.unofficial_set_play_mode("PlayOn") + print("\nPress ctrl+c to return.") + + while True: + + if w.play_mode == w.M_THEIR_KICKOFF: + p.scom.unofficial_set_play_mode("PlayOn") + + # execute dribbler + if behavior.is_ready("Get_Up") or w.play_mode_group in [w.MG_ACTIVE_BEAM, w.MG_PASSIVE_BEAM]: + p.scom.unofficial_beam((*(w.ball_abs_pos[:2]-(1,0)),r.beam_height),0) + behavior.execute("Zero_Bent_Knees") + else: + behavior.execute("Dribble",None,None) + d.annotation(r.loc_head_position+(0,0,0.2),f"{np.linalg.norm(r.get_head_abs_vel(40)[:2]):.2f}",d.Color.white,"vel_annotation") + p.scom.commit_and_send( r.get_command() ) + + # execute opponents as normal agents + self.script.batch_execute_agent(slice(1,None)) + + # all players wait for server to send feedback + self.script.batch_receive() \ No newline at end of file diff --git a/scripts/utils/Fwd_Kinematics.py b/scripts/utils/Fwd_Kinematics.py new file mode 100644 index 0000000..af6f6a8 --- /dev/null +++ b/scripts/utils/Fwd_Kinematics.py @@ -0,0 +1,115 @@ +from agent.Base_Agent import Base_Agent as Agent +from scripts.commons.Script import Script +from world.commons.Draw import Draw +import numpy as np + +class Fwd_Kinematics(): + + def __init__(self,script:Script) -> None: + self.script = script + self.cycle_duration = 200 #steps + + def draw_cycle(self): + + #Draw position of body parts + for _ in range(self.cycle_duration): + self.script.batch_execute_behavior("Squat") + self.script.batch_commit_and_send() + self.script.batch_receive() + + p : Agent + for p in self.script.players: + if p.world.vision_is_up_to_date and not p.world.robot.loc_is_up_to_date: + p.world.draw.annotation(p.world.robot.cheat_abs_pos, "Not enough visual data! Using IMU", Draw.Color.red,"localization") + + for key, val in p.world.robot.body_parts.items(): + rp = val.transform.get_translation() + pos = p.world.robot.loc_head_to_field_transform(rp,False) + label_rp = np.array([rp[0]-0.0001,rp[1]*0.5,0]) + label_rp /= np.linalg.norm(label_rp) / 0.4 #labels at 0.4m from body part + label = p.world.robot.loc_head_to_field_transform(rp+label_rp,False) + p.world.draw.line(pos,label,2,Draw.Color.green_light,key,False) + p.world.draw.annotation(label,key,Draw.Color.red,key) + + rp = p.world.robot.body_parts['lfoot'].transform((0.08,0,0)) + ap = p.world.robot.loc_head_to_field_transform(rp,False) + p.world.draw.line(ap,ap+(0,0,0.1),1,Draw.Color.red,"soup",False) + rp = p.world.robot.body_parts['lfoot'].transform((-0.08,0,0)) + ap = p.world.robot.loc_head_to_field_transform(rp,False) + p.world.draw.line(ap,ap+(0,0,0.1),1,Draw.Color.red,"soup",False) + rp = p.world.robot.body_parts['lfoot'].transform((0,0.04,0)) + ap = p.world.robot.loc_head_to_field_transform(rp,False) + p.world.draw.line(ap,ap+(0,0,0.1),1,Draw.Color.red,"soup",False) + rp = p.world.robot.body_parts['lfoot'].transform((0,-0.04,0)) + ap = p.world.robot.loc_head_to_field_transform(rp,False) + p.world.draw.line(ap,ap+(0,0,0.1),1,Draw.Color.red,"soup",True) + + Draw.clear_all() + + #Draw position of joints + for _ in range(self.cycle_duration): + self.script.batch_execute_behavior("Squat") + self.script.batch_commit_and_send() + self.script.batch_receive() + + for p in self.script.players: + if p.world.vision_is_up_to_date and not p.world.robot.loc_is_up_to_date: + p.world.draw.annotation(p.world.robot.cheat_abs_pos, "Not enough visual data! Using IMU", Draw.Color.red,"localization") + + zstep = 0.05 + label_z = [0,0,0,0,zstep,zstep,2*zstep,2*zstep,0,0,0,0,zstep,zstep,0,0,zstep,zstep,2*zstep,2*zstep,3*zstep,3*zstep,0,0] + for j, transf in enumerate(p.world.robot.joints_transform): + rp = transf.get_translation() + pos = p.world.robot.loc_head_to_field_transform(rp,False) + j_name = str(j) + label_rp = np.array([rp[0]-0.0001,rp[1]*0.5,0]) + label_rp /= np.linalg.norm(label_rp) / 0.4 #labels at 0.4m from body part + label_rp += (0,0,label_z[j]) + label = p.world.robot.loc_head_to_field_transform(rp+label_rp,False) + p.world.draw.line( pos,label,2,Draw.Color.green_light,j_name,False) + p.world.draw.annotation( label,j_name,Draw.Color.cyan,j_name) + + + Draw.clear_all() + + #Draw orientation of body parts + for _ in range(self.cycle_duration): + self.script.batch_execute_behavior("Squat") + self.script.batch_commit_and_send() + self.script.batch_receive() + + p : Agent + for p in self.script.players: + if p.world.vision_is_up_to_date and not p.world.robot.loc_is_up_to_date: + p.world.draw.annotation(p.world.robot.cheat_abs_pos, "Not enough visual data! Using IMU", Draw.Color.red,"localization") + + for key in p.world.robot.body_parts: + #Select only some body parts + if key not in ['head', 'torso', 'llowerarm', 'rlowerarm', 'lthigh', 'rthigh', 'lshank', 'rshank', 'lfoot', 'rfoot']: continue + bpart_abs_pos = p.world.robot.get_body_part_to_field_transform(key).translate((0.1,0,0)) #10cm in front of body part + x_axis = bpart_abs_pos((0.05,0,0),False) + y_axis = bpart_abs_pos((0,0.05,0),False) + z_axis = bpart_abs_pos((0,0,0.05),False) + axes_0 = bpart_abs_pos.get_translation() + p.world.draw.line( axes_0,x_axis,2,Draw.Color.green_light,key,False) + p.world.draw.line( axes_0,y_axis,2,Draw.Color.blue,key,False) + p.world.draw.line( axes_0,z_axis,2,Draw.Color.red,key) + + Draw.clear_all() + + + + def execute(self): + + a = self.script.args + + # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name, Enable Log, Enable Draw + self.script.batch_create(Agent, ((a.i,a.p,a.m,u,u-1,a.t,True,True) for u in range(1,6)) ) + + #Beam players + self.script.batch_unofficial_beam( [(-2,i*4-10,0.5,i*45) for i in range(5)] ) + + print("\nPress ctrl+c to return.") + + while True: + self.draw_cycle() \ No newline at end of file diff --git a/scripts/utils/Get_Up.py b/scripts/utils/Get_Up.py new file mode 100644 index 0000000..f95464c --- /dev/null +++ b/scripts/utils/Get_Up.py @@ -0,0 +1,45 @@ +from agent.Base_Agent import Base_Agent as Agent +from itertools import count +from scripts.commons.Script import Script +import numpy as np + +''' +Objective: +---------- +Fall and get up +''' + +class Get_Up(): + def __init__(self, script:Script) -> None: + self.script = script + self.player : Agent = None + + def sync(self): + r = self.player.world.robot + self.player.scom.commit_and_send( r.get_command() ) + self.player.scom.receive() + + def execute(self): + + a = self.script.args + player = self.player = Agent(a.i, a.p, a.m, a.u, a.r, a.t) # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name + behavior = player.behavior + r = player.world.robot + + player.scom.commit_beam((-3,0),0) + print("\nPress ctrl+c to return.") + + for i in count(): + rnd = np.random.uniform(-6,6,r.no_of_joints) + + # Fall + while r.loc_head_z > 0.3 and r.imu_torso_inclination < 50: + if i < 4: + behavior.execute(["Fall_Front","Fall_Back","Fall_Left","Fall_Right"][i % 4]) # First, fall deterministically + else: + r.joints_target_speed[:] = rnd # Second, fall randomly + self.sync() + + # Get up + behavior.execute_to_completion("Get_Up") + behavior.execute_to_completion("Zero_Bent_Knees") diff --git a/scripts/utils/IMU.py b/scripts/utils/IMU.py new file mode 100644 index 0000000..8fedfd1 --- /dev/null +++ b/scripts/utils/IMU.py @@ -0,0 +1,179 @@ +from agent.Base_Agent import Base_Agent as Agent +from math_ops.Matrix_3x3 import Matrix_3x3 +from math_ops.Matrix_4x4 import Matrix_4x4 +from scripts.commons.Script import Script +from world.commons.Draw import Draw +from world.Robot import Robot +import numpy as np + +''' +Objective: +---------- +Demonstrate the accuracy of the IMU +Robot.imu_(...) variables are based on the visual localizer algorithm and the IMU when no visual data is available. +If visual data is not available for longer than 0.2 seconds, the robot's position is frozen and the velocity decays to zero. +The rotation computed by the IMU is so accurate that it is never frozen, no matter how long the robot goes without visual data. +It is almost always safe to use IMU data for rotation. +Known issues: the accelerometer is not reliable in the presence of "instant" acceleration peaks, due to its low sample rate (50Hz) + this limitation impacts the translation estimation during crashes (e.g. falling, crashing against other players) +''' + +class IMU(): + + def __init__(self,script:Script) -> None: + self.script = script + self.player : Agent = None + self.cycle = 0 + + self.imu_torso_to_field_rotation = [Matrix_3x3() for _ in range(3)] + self.imu_torso_to_field_transform = [Matrix_4x4() for _ in range(3)] + self.imu_head_to_field_transform = [Matrix_4x4() for _ in range(3)] + self.imu_torso_position = np.zeros((3,3)) + self.imu_torso_velocity = np.zeros((3,3)) + self.imu_torso_acceleration = np.zeros((3,3)) + self.imu_torso_next_position = np.zeros((3,3)) + self.imu_torso_next_velocity = np.zeros((3,3)) + self.imu_CoM_position = np.zeros((3,3)) + self.colors = [Draw.Color.green_light, Draw.Color.yellow, Draw.Color.red] + + def act(self): + r = self.player.world.robot + joint_indices = [r.J_LLEG_PITCH, + r.J_LKNEE, + r.J_LFOOT_PITCH, + r.J_LARM_ROLL, + r.J_RLEG_PITCH, + r.J_RKNEE, + r.J_RFOOT_PITCH, + r.J_RARM_ROLL] + + amplitude = [1,0.93,1,1,1][r.type] + + self.cycle += 1 + if self.cycle < 50: + r.set_joints_target_position_direct(joint_indices, np.array([32+10,-64,32, 45, 40+10,-80,40, 0])*amplitude) + elif self.cycle < 100: + r.set_joints_target_position_direct(joint_indices, np.array([ -10, 0, 0, 0, -10, 0, 0, 0])*amplitude) + elif self.cycle < 150: + r.set_joints_target_position_direct(joint_indices, np.array([40+10,-80,40, 0, 32+10,-64,32, 45])*amplitude) + elif self.cycle < 200: + r.set_joints_target_position_direct(joint_indices, np.array([ -10, 0, 0, 0, -10, 0, 0, 0])*amplitude) + else: + self.cycle = 0 + + self.player.scom.commit_and_send( r.get_command() ) + self.player.scom.receive() + + def act2(self): + r = self.player.world.robot + self.player.behavior.execute("Walk", (0.2,0), False, 5, False, None ) # Args: target, is_target_abs, ori, is_ori_abs, distance + self.player.scom.commit_and_send( r.get_command() ) + self.player.scom.receive() + + def draw_player_reference_frame(self,i): + pos = self.imu_torso_position[i] + xvec = self.imu_torso_to_field_rotation[i].multiply((1,0,0)) + pos + yvec = self.imu_torso_to_field_rotation[i].multiply((0,1,0)) + pos + zvec = self.imu_torso_to_field_rotation[i].multiply((0,0,1)) + pos + self.player.world.draw.arrow(pos, xvec, 0.2, 2, self.colors[i], "IMU"+str(i), False) + self.player.world.draw.arrow(pos, yvec, 0.2, 2, self.colors[i], "IMU"+str(i), False) + self.player.world.draw.arrow(pos, zvec, 0.2, 2, self.colors[i], "IMU"+str(i), False) + self.player.world.draw.annotation(xvec, "x", Draw.Color.white, "IMU"+str(i), False) + self.player.world.draw.annotation(yvec, "y", Draw.Color.white, "IMU"+str(i), False) + self.player.world.draw.annotation(zvec, "z", Draw.Color.white, "IMU"+str(i), False) + self.player.world.draw.sphere(self.imu_CoM_position[i],0.04,self.colors[i],"IMU"+str(i), True) + + + def compute_local_IMU(self): + r = self.player.world.robot + g = r.gyro / 50 # convert degrees per second to degrees per step + self.imu_torso_to_field_rotation[2].multiply( Matrix_3x3.from_rotation_deg(g), in_place=True, reverse_order=True) + self.imu_torso_position[2][:] = self.imu_torso_next_position[2] + if self.imu_torso_position[2][2] < 0: self.imu_torso_position[2][2] = 0 #limit z coordinate to positive values + self.imu_torso_velocity[2][:] = self.imu_torso_next_velocity[2] + + # convert proper acceleration to coordinate acceleration and fix rounding bias + self.imu_torso_acceleration[2] = self.imu_torso_to_field_rotation[2].multiply(r.acc) + Robot.GRAVITY + self.imu_torso_to_field_transform[2] = Matrix_4x4.from_3x3_and_translation(self.imu_torso_to_field_rotation[2],self.imu_torso_position[2]) + self.imu_head_to_field_transform[2] = self.imu_torso_to_field_transform[2].multiply(r.body_parts["torso"].transform.invert()) + self.imu_CoM_position[2][:] = self.imu_head_to_field_transform[2](r.rel_cart_CoM_position) + + # Next Position = x0 + v0*t + 0.5*a*t^2, Next velocity = v0 + a*t + self.imu_torso_next_position[2] = self.imu_torso_position[2] + self.imu_torso_velocity[2] * 0.02 + self.imu_torso_acceleration[2] * 0.0002 + self.imu_torso_next_velocity[2] = self.imu_torso_velocity[2] + self.imu_torso_acceleration[2] * 0.02 + self.imu_torso_next_velocity[2] *= Robot.IMU_DECAY #stability tradeoff + + def compute_local_IMU_rotation_only(self): + r = self.player.world.robot + g = r.gyro / 50 # convert degrees per second to degrees per step + self.imu_torso_to_field_rotation[1].multiply( Matrix_3x3.from_rotation_deg(g), in_place=True, reverse_order=True) + self.imu_torso_position[1][:] = r.loc_torso_position + self.imu_torso_to_field_transform[1] = Matrix_4x4.from_3x3_and_translation(self.imu_torso_to_field_rotation[1],self.imu_torso_position[1]) + self.imu_head_to_field_transform[1] = self.imu_torso_to_field_transform[1].multiply(r.body_parts["torso"].transform.invert()) + self.imu_CoM_position[1][:] = self.imu_head_to_field_transform[1](r.rel_cart_CoM_position) + + + def update_local_IMU(self, i): + r = self.player.world.robot + self.imu_torso_to_field_rotation[i].m[:] = r.imu_torso_to_field_rotation.m + self.imu_torso_to_field_transform[i].m[:] = r.imu_weak_torso_to_field_transform.m + self.imu_head_to_field_transform[i].m[:] = r.imu_weak_head_to_field_transform.m + self.imu_torso_position[i][:] = r.imu_weak_torso_position + self.imu_torso_velocity[i][:] = r.imu_weak_torso_velocity + self.imu_torso_acceleration[i][:] = r.imu_weak_torso_acceleration + self.imu_torso_next_position[i] = self.imu_torso_position[i] + self.imu_torso_velocity[i] * 0.02 + self.imu_torso_acceleration[i] * 0.0002 + self.imu_torso_next_velocity[i] = self.imu_torso_velocity[i] + self.imu_torso_acceleration[i] * 0.02 + self.imu_CoM_position[i][:] = r.imu_weak_CoM_position + + def execute(self): + + a = self.script.args + self.player = Agent(a.i, a.p, a.m, a.u, a.r, a.t) # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name + + self.player.scom.unofficial_beam((-3,0,self.player.world.robot.beam_height),15) + + for _ in range(10): #beam to place + self.player.scom.commit_and_send() + self.player.scom.receive() + + self.player.world.draw.annotation((-3,1,1.1), "IMU + Localizer", self.colors[0], "note_IMU_1", True) + + for _ in range(150): + self.act() + self.update_local_IMU(0) + self.draw_player_reference_frame(0) + + self.player.world.draw.annotation((-3,1,0.9), "IMU for rotation", self.colors[1], "note_IMU_2", True) + self.update_local_IMU(1) + + for _ in range(200): + self.act() + self.update_local_IMU(0) + self.draw_player_reference_frame(0) + self.compute_local_IMU_rotation_only() + self.draw_player_reference_frame(1) + + self.player.world.draw.annotation((-3,1,0.7), "IMU for rotation & position", self.colors[2], "note_IMU_3", True) + self.update_local_IMU(2) + + for _ in range(200): + self.act() + self.update_local_IMU(0) + self.draw_player_reference_frame(0) + self.compute_local_IMU_rotation_only() + self.draw_player_reference_frame(1) + self.compute_local_IMU() + self.draw_player_reference_frame(2) + + print("\nPress ctrl+c to return.") + + # Still "IMU for rotation & position" but now start walking + self.update_local_IMU(2) + while True: + self.act2() + self.update_local_IMU(0) + self.draw_player_reference_frame(0) + self.compute_local_IMU_rotation_only() + self.draw_player_reference_frame(1) + self.compute_local_IMU() + self.draw_player_reference_frame(2) \ No newline at end of file diff --git a/scripts/utils/Inv_Kinematics.py b/scripts/utils/Inv_Kinematics.py new file mode 100644 index 0000000..235104a --- /dev/null +++ b/scripts/utils/Inv_Kinematics.py @@ -0,0 +1,146 @@ +from agent.Base_Agent import Base_Agent as Agent +from itertools import count +from math_ops.Inverse_Kinematics import Inverse_Kinematics +from scripts.commons.Script import Script +from world.commons.Draw import Draw +import numpy as np + + +class Inv_Kinematics(): + def __init__(self, script:Script) -> None: + self.args = script.args + self.last_action = (0,0,0) + self.gravity = True + + # Initial pose is a neutral pose where all angles are 0 + leg_y_dev, upper_leg_height, upper_leg_depth, lower_leg_len, _, _ = Inverse_Kinematics.NAO_SPECS_PER_ROBOT[self.args.r] + leg_height = upper_leg_height + lower_leg_len + self.feet_pose = [ [[upper_leg_depth,leg_y_dev,-leg_height],[0,0,0]], [[upper_leg_depth,-leg_y_dev,-leg_height], [0,0,0]] ] + + + + def _user_control(self): + + while True: + + inp = input("Command:") + if inp == "": return 2 + elif inp == ".": return 1 + elif inp == "h": self.print_help(); continue + elif inp == "g": + self.gravity = not self.gravity + print("Using gravity:",self.gravity) + if self.gravity: + return 6 # extra steps for beam to take effect + else: + return 1 + + #Check if user input is a value + try: + val = float(inp) + self.feet_pose[self.last_action[0]][self.last_action[1]][self.last_action[2]] = val + continue + except: + pass + + if inp[0] not in ['l','r'] or inp[1] not in ['x','y','z','X','Y','Z']: + print("Illegal command!") + continue + + side = 0 if inp[0]=='l' else 1 + pos_rot = 0 if inp[1].islower() else 1 + axis = {'x':0,'y':1,'z':2}[inp[1].lower()] + self.last_action = (side,pos_rot,axis) + + try: + val = float(inp[2:]) + self.feet_pose[side][pos_rot][axis] = val + except: + print("Illegal value conversion!") + + + + def _draw_labels(self, player:Agent): + r = player.world.robot + robot_pos = r.loc_head_position + for i, body_part in enumerate(['lankle','rankle']): + pos = r.get_body_part_abs_position(body_part) + label_rel_pos = np.array([-0.2,(0.5-i),0]) + label_rel_pos /= np.linalg.norm(label_rel_pos) / 1.0 #labels at 1.0m from body part + player.world.draw.line( pos,pos+label_rel_pos,2,Draw.Color.green_light,body_part,False) + p = self.feet_pose[i] + pose_text = f"x:{p[0][0]:.4f} y:{p[0][1]:.4f} z:{p[0][2]:.4f}", f"rol:{p[1][0]:.2f} (bias) pit:{p[1][1]:.2f} (bias) yaw:{p[1][2]:.2f} " + player.world.draw.annotation( pos+label_rel_pos+[0,0,0.2], pose_text[0], Draw.Color.cyan,body_part,False) + player.world.draw.annotation( pos+label_rel_pos+[0,0,0.1], pose_text[1], Draw.Color.cyan,body_part,False) + + # Draw forward kinematics (ankles positions + feet rotation) + p = player.inv_kinematics.get_body_part_pos_relative_to_hip(body_part) # ankle relative to center of both hip joints + foot_rel_torso = r.head_to_body_part_transform("torso", r.body_parts[['lfoot','rfoot'][i]].transform ) + w = foot_rel_torso.get_roll_deg(), foot_rel_torso.get_pitch_deg(), foot_rel_torso.get_yaw_deg() + pose_text = f"x:{p[0]:.4f} y:{p[1]:.4f} z:{p[2]:.4f}", f"rol:{w[0]:.4f} pit:{w[1]:.4f} yaw:{w[2]:.4f}" + + player.world.draw.annotation( pos+label_rel_pos+[0,0,-0.2], pose_text[0], Draw.Color.red,body_part,False) + player.world.draw.annotation( pos+label_rel_pos+[0,0,-0.3], pose_text[1], Draw.Color.red,body_part,False) + player.world.draw.annotation( pos+label_rel_pos+[0,0,-0.4], "(forward kinematics data)", Draw.Color.red,body_part,True) + + note = f"Torso roll: {r.imu_torso_roll:.2f} Torso pitch: {r.imu_torso_pitch:.2f}" + player.world.draw.annotation( robot_pos+[0,0,0.10],note,Draw.Color.red,"Torso") + + + def print_help(self): + print(""" +---------------- Inverse kinematics demonstration ---------------- +INPUT: ankle positions + feet rotations (relative coordinates) +OUTPUT: angular positions of both legs' joints +------------------------------------------------------------------ +Command: {action/option} + action: [side:{l/r} axis*:{x/y/z/X/Y/Z}] value + *for position use x/y/z, for rotation use X/Y/Z + option: {"",.,g,h} +Examples: + "lz-0.12" - move left ankle to -0.1m in the z-axis + "rX30.5" - rotate right foot to 30.5 deg in the x-axis (roll) + "20" - repeat last action but change value to 20 + "" - advance 2 simulation step + "." - advance 1 simulation step + "g" - toggle gravity + "h" - help, display this message + "ctrl+c" - quit demonstration +------------------------------------------------------------------""") + + def execute(self): + + self.state = 0 + a = self.args + + self.print_help() + player = Agent(a.i, a.p, a.m, a.u, a.r, a.t) # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name + + player.scom.unofficial_beam((-3,0,0.42),0) + + next_control_step = 20 + + for i in count(): + + if self.gravity: + player.scom.unofficial_beam((-3,0,0.42),0) + + self._draw_labels(player) + + if i == next_control_step: + next_control_step += self._user_control() + + for i in range(2): #left and right legs + + indices, values, error_codes = player.inv_kinematics.leg(self.feet_pose[i][0], self.feet_pose[i][1], bool(i==0), False) + + if -1 in error_codes: + print("Position is out of reach!") + error_codes.remove(-1) + for j in error_codes: + print(f"Joint {j} is out of range!") + + player.world.robot.set_joints_target_position_direct(indices,values) + + player.scom.commit_and_send( player.world.robot.get_command() ) + player.scom.receive() \ No newline at end of file diff --git a/scripts/utils/Joints.py b/scripts/utils/Joints.py new file mode 100644 index 0000000..8ca013c --- /dev/null +++ b/scripts/utils/Joints.py @@ -0,0 +1,151 @@ +from agent.Base_Agent import Base_Agent as Agent +from scripts.commons.Script import Script +from world.commons.Draw import Draw +import numpy as np + +class Joints(): + + def __init__(self,script:Script) -> None: + self.script = script + self.agent_pos = (-3,0,0.45) + self.enable_pos = True + self.enable_gravity = False + self.enable_labels = True + self.enable_harmonize = True + self.active_joint = 0 + self.joints_value = None #position or speed + + + def _draw_joints(self,player:Agent): + zstep = 0.05 + label_z = [3*zstep,5*zstep,0,0,zstep,zstep,2*zstep,2*zstep,0,0,0,0,zstep,zstep,0,0,zstep,zstep,4*zstep,4*zstep,5*zstep,5*zstep,0,0] + for j, transf in enumerate(player.world.robot.joints_transform): + rp = transf.get_translation() + pos = player.world.robot.loc_head_to_field_transform(rp,False) + j_id = f"{j}" + j_name = f"{j}" + color = Draw.Color.cyan + if player.world.robot.joints_position[j] != 0: + j_name += f" ({int(player.world.robot.joints_position[j])})" + color = Draw.Color.red + label_rp = np.array([rp[0]-0.0001,rp[1]*0.5,0]) + label_rp /= np.linalg.norm(label_rp) / 0.5 #labels at 0.5m from body part + label_rp += (0,0,label_z[j]) + label = player.world.robot.loc_head_to_field_transform(rp+label_rp,False) + player.world.draw.line( pos,label,2,Draw.Color.green_light,j_id,False) + player.world.draw.annotation( label,j_name,color,j_id) + + + def print_help(self): + print(f""" +---------------------- Joints demonstration ---------------------- +Command: {{action/actions/option}} + action : [joint:{{int}}] value + actions: value0,value1,...,valueN + e.g. if N=10, you control all joints from j0 to j10 + option: {{h,s,g,l,w,r,"",.}} +Examples: + "6 90" - move joint 6 to 90deg or move joint 6 at 90deg/step + "4" - move last joint to 4deg or apply speed of 4deg/step + "1,9,-35"- move joints 0,1,2 to 1deg, 9deg, -35deg (or speed) + "h" - help, display this message + "s" - toggle position/speed control ({"Posi" if self.enable_pos else "Spee"}) + "g" - toggle gravity ({self.enable_gravity}) + "l" - toggle labels ({self.enable_labels}) + "w" - toggle harmonize* ({self.enable_harmonize}) + "r" - reset (position mode + reset joints) + "" - advance 2 simulation step + "." - advance 1 simulation step + "ctrl+c" - quit demonstration + + *all joints end moving at the same time when harmonize is True +------------------------------------------------------------------""") + + def _user_control_step(self,player:Agent): + + while True: + + inp = input("Command: ") + if inp == "s": + self.enable_pos = not self.enable_pos + print("Using", "position" if self.enable_pos else "velocity", "control.") + if self.enable_pos: + self.joints_value[:] = player.world.robot.joints_position + else: + self.joints_value.fill(0) + continue + elif inp == "g": + self.enable_gravity = not self.enable_gravity + print("Using gravity:",self.enable_gravity) + continue + elif inp == "l": + self.enable_labels = not self.enable_labels + print("Using labels:",self.enable_labels) + continue + elif inp == "w": + self.enable_harmonize = not self.enable_harmonize + print("Using harmonize:",self.enable_harmonize) + continue + elif inp == "r": + self.enable_pos = True + self.joints_value.fill(0) + print("Using position control. All joints are set to zero.") + continue + elif inp == "h": + self.print_help(); continue + + elif inp == "": return 1 + elif inp == ".": return 0 + + try: + if " " in inp: + self.active_joint, value = map(float, inp.split()) + self.joints_value[int(self.active_joint)] = value + elif "," in inp: + values = inp.split(",") + self.joints_value[0:len(values)] = values + else: + self.joints_value[self.active_joint] = float(inp) + except: + print("Illegal command!") + continue + + + + def execute(self): + + a = self.script.args + player = Agent(a.i, a.p, a.m, a.u, a.r, a.t) # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name + + self.joints_no = player.world.robot.no_of_joints + self.joints_value = np.zeros(self.joints_no) # initialize + + player.scom.commit_beam(self.agent_pos[0:2],0) + + self.print_help() + + # initialize (+beam) + for _ in range(8): + player.scom.commit_and_send() + player.scom.receive() + self._draw_joints(player) + + skip_next = 0 # variable to advance more than 1 step + + while True: + if skip_next == 0: + skip_next = self._user_control_step(player) + else: + skip_next -= 1 + + if self.enable_labels: + self._draw_joints(player) + + if self.enable_pos: + player.world.robot.set_joints_target_position_direct(slice(self.joints_no), self.joints_value, harmonize=self.enable_harmonize) + else: + player.world.robot.joints_target_speed[:]=self.joints_value * 0.87266463 #deg/step to rad/s + + if not self.enable_gravity: player.scom.unofficial_beam(self.agent_pos,0) + player.scom.commit_and_send( player.world.robot.get_command() ) + player.scom.receive() \ No newline at end of file diff --git a/scripts/utils/Kick.py b/scripts/utils/Kick.py new file mode 100644 index 0000000..2cfe3db --- /dev/null +++ b/scripts/utils/Kick.py @@ -0,0 +1,53 @@ +from agent.Base_Agent import Base_Agent as Agent +from math_ops.Math_Ops import Math_Ops as M +from scripts.commons.Script import Script +import numpy as np + +''' +Objective: +---------- +Demonstrate kick +''' + +class Kick(): + def __init__(self, script:Script) -> None: + self.script = script + + def execute(self): + + a = self.script.args + player = Agent(a.i, a.p, a.m, a.u, a.r, a.t) # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name + player.path_manager.draw_options(enable_obstacles=True, enable_path=True) # enable drawings of obstacles and path to ball + behavior = player.behavior + w = player.world + r = w.robot + + print("\nThe robot will kick towards the center of the field") + print("Try to manually relocate the ball") + print("Press ctrl+c to return\n") + + player.scom.unofficial_set_play_mode("PlayOn") + player.scom.unofficial_beam((-3,0,r.beam_height),0) + vec = (1,0) + + while True: + player.scom.unofficial_set_game_time(0) + b = w.ball_abs_pos[:2] + + if 0 < np.linalg.norm(w.get_ball_abs_vel(6)) < 0.02: # speed of zero is likely to indicate prolongued inability to see the ball + if np.linalg.norm(w.ball_rel_head_cart_pos[:2]) > 0.5: # update kick if ball is further than 0.5 m + if max(abs(b)) < 0.5: + vec = np.array([6,0]) + else: + vec = M.normalize_vec((0,0)-b) * 6 + + w.draw.point(b+vec, 8, w.draw.Color.pink, "target") + + behavior.execute("Basic_Kick", M.vector_angle(vec)) + + player.scom.commit_and_send( r.get_command() ) + player.scom.receive() + + if behavior.is_ready("Get_Up"): + player.scom.unofficial_beam((*r.loc_head_position[0:2],r.beam_height),0) + behavior.execute_to_completion("Zero_Bent_Knees") \ No newline at end of file diff --git a/scripts/utils/Localization.py b/scripts/utils/Localization.py new file mode 100644 index 0000000..bb91937 --- /dev/null +++ b/scripts/utils/Localization.py @@ -0,0 +1,103 @@ +from agent.Agent import Agent as Agent +from cpp.localization import localization +from math_ops.Math_Ops import Math_Ops as M +from scripts.commons.Script import Script +from world.commons.Draw import Draw +from world.commons.Other_Robot import Other_Robot + + +class Localization(): + + def __init__(self,script:Script) -> None: + self.script = script + + + def execute(self): + + a = self.script.args + d = self.draw = Draw(True, 0, a.i, 32769) # using independent draw object so that the internal agent drawings can be disabled + + # Args: Server IP, Agent Port, Monitor Port, Uniform No., Team name, Enable Log, Enable Draw + self.script.batch_create(Agent, ((a.i,a.p,a.m,1,a.t,False,False),)) # one teammate (dummy goalkeeper without communication) + self.script.batch_create(Agent, ((a.i,a.p,a.m,5,"Opponent",False,False),)) # one opponent + self.script.batch_create(Agent, ((a.i,a.p,a.m,9,a.t,False,False),)) # one main agent (the one who draws its world) + + # Beam dummy goalkeeper + self.script.batch_unofficial_beam( ((-14,0,0.5,0),), slice(0,1)) + + p : Agent = self.script.players[-1] # p identifies the main agent + p.scom.unofficial_set_play_mode("PlayOn") + + # Execute + while True: + self.script.batch_commit_and_send(slice(0,1)) # dummy agent does not think + self.script.batch_execute_agent(slice(1,None)) # execute normal agents + self.script.batch_receive(slice(0,1), False) # receive & don't update dummy's world state (to save cpu resources) + self.script.batch_receive(slice(1,None)) # receive & update world state + + if p.world.vision_is_up_to_date: + if p.world.robot.loc_is_up_to_date: # localization will draw the world of the last agent to be executed + localization.print_python_data() # print data received by the localization module + localization.draw_visible_elements(not p.world.team_side_is_left) # draw visible elements + localization.print_report() # print report with stats + print("\nPress ctrl+c to return.") + d.circle( p.world.ball_abs_pos, 0.1,6,Draw.Color.purple_magenta,"world", False) + else: + d.annotation( p.world.robot.cheat_abs_pos, "Not enough visual data!", Draw.Color.red,"world", False) + + for o in p.world.teammates: + if o.state_last_update != 0 and not o.is_self: # skip if other robot was not yet seen + self._draw_other_robot(p, o, Draw.Color.white) + + for o in p.world.opponents: + if o.state_last_update != 0: # skip if other robot was not yet seen + self._draw_other_robot(p, o, Draw.Color.red) + + d.flush("world") + + + + def _draw_other_robot(self, p:Agent, o:Other_Robot, team_color): + #p - player that sees + #o - other robot (player that is seen) + + d = self.draw + white = Draw.Color.white + green = Draw.Color.green_light + gray = Draw.Color.gray_20 + + time_diff = p.world.time_local_ms - o.state_last_update + if time_diff > 0: + white = Draw.Color.gray_40 + green = Draw.Color.get(107, 139, 107) + gray = Draw.Color.gray_50 + + #orientation + if len(o.state_abs_pos)==3: + line_tip = o.state_abs_pos + (0.5*M.deg_cos(o.state_orientation),0.5*M.deg_sin(o.state_orientation),0) + d.line( o.state_abs_pos, line_tip, 3, white, "world", False) + else: + temp_pos = M.to_3d(o.state_abs_pos, 0.3) + line_tip = temp_pos + (0.5*M.deg_cos(o.state_orientation),0.5*M.deg_sin(o.state_orientation),0) + d.line( temp_pos, line_tip, 3, Draw.Color.yellow, "world", False) + + #body parts + for pos in o.state_body_parts_abs_pos.values(): + d.sphere( pos, 0.07, green,"world", False) + + #player ground area + d.circle( o.state_ground_area[0], o.state_ground_area[1], 6, team_color,"world", False) + + #distance + midpoint = (o.state_abs_pos[0:2] + p.world.robot.loc_head_position[0:2])/2 + d.line( o.state_abs_pos[0:2], p.world.robot.loc_head_position[0:2], 1, gray, "world", False) + d.annotation( midpoint, f'{o.state_horizontal_dist:.2f}m', white, "world", False) + + #velocity + arrow_tip = o.state_abs_pos[0:2] + o.state_filtered_velocity[0:2] + d.arrow( o.state_abs_pos[0:2], arrow_tip, 0.2, 4, green, "world", False) + + #state + state_color = white if not o.state_fallen else Draw.Color.yellow + d.annotation( (o.state_abs_pos[0],o.state_abs_pos[1],1), + f"({o.unum}) {'Fallen' if o.state_fallen else 'Normal'}", state_color, "world", False) \ No newline at end of file diff --git a/scripts/utils/Pathfinding.py b/scripts/utils/Pathfinding.py new file mode 100644 index 0000000..c371b19 --- /dev/null +++ b/scripts/utils/Pathfinding.py @@ -0,0 +1,203 @@ +from agent.Base_Agent import Base_Agent as Agent +from cpp.a_star import a_star +from scripts.commons.Script import Script +import numpy as np +import time + +''' +:::::::::::::::::::::::::::::::::::::::::: +::::::::a_star.compute(param_vec)::::::::: +:::::::::::::::::::::::::::::::::::::::::: + +param_vec (numpy array, float32) +param_vec[0] - start x +param_vec[1] - start y +param_vec[2] - allow path to go out of bounds? (useful when player does not have the ball) +param_vec[3] - go to opposite goal? (path goes to the most efficient part of the goal) +param_vec[4] - target x (only used if param_vec[3]==0) +param_vec[5] - target y (only used if param_vec[3]==0) +param_vec[6] - timeout in us (maximum execution time) +-------------- [optional] ---------------- +param_vec[ 7-11] - obstacle 1: x, y, hard radius (max:5m), soft radius (max:5m), repulsive force for soft radius (min:0) +param_vec[12-16] - obstacle 2: x, y, hard radius (max:5m), soft radius (max:5m), repulsive force for soft radius (min:0) +... - obstacle n: x, y, hard radius (max:5m), soft radius (max:5m), repulsive force for soft radius (min:0) +---------------- return ------------------ +path_ret : numpy array (float32) + path_ret[:-2] + contains path from start to target (up to a maximum of 1024 positions) + each position is composed of x,y coordinates (so, up to 2048 coordinates) + the return vector is flat (1 dimension) (e.g. [x1,y1,x2,y2,x3,y3,...]) + reasons why path may not end in target: + - path is longer than 1024 positions (which is at least 102 meters!) + - reaching target is impossible or timeout (in which case, the path ends in the closest position to target found) + path_ret[-2] + number indicating the path status + 0 - success + 1 - timeout before the target was reached (may be impossible) + 2 - impossible to reach target (all options were tested) + 3 - no obstacles between start and target (path_ret[:-2] contains only 2 points: the start and target) + path_ret[-1] + A* path cost +:::::::::::::::::::::::::::::::::::::::::: +::::::::::::::::::Notes::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::: + +Map of field: + - The algorithm has a 32m by 22m map with a precision of 10cm (same dimension as field +1 meter border) + - The map contains information about field lines, goal posts and goal net + - The path may go outside the field (out of bounds) if the user allows it, + but it may never go through goal posts or the goal net (these are considered static inaccessible obstacles) + - The user must only specify dynamic obstacles through the arguments + +Repulsive force: + - The repulsive force is implemented as an extra cost for the A* algorithm + - The cost for walking 10cm is 1, and the cost for walking diagonally is sqrt(2) + - The extra cost of stepping on a position with a repulsive force f=1 is 1 + - For any given position on the field, the repulsive force of >=2 objects is combined with the max function, max(f1,f2), NOT f1+f2! + - If path starts on inaccessible position, it can go to a neighbor inaccessible position but there is a cost of 100 (to avoid inaccessible paths) + Example: + Map 1 Map 2 Map 3 + ..x.. ..o.. ..o.. + ..1.. ..o.. .o1.. + ..o.. ..o.. ..o.. + Consider 'Map 1' where 'x' is the target, 'o' is the player, and '1' is a repulsive force of 1 + In 'Map 2', the player chooses to go forward, the total cost of this path is: 1+(extra=1)+1 = 3 + In 'Map 3', the player avoids the repulsive force, the total cost of this path is: sqrt(2)+sqrt(2) = 2.83 (optimal solution) + Map 1 Map 2 Map 3 Map 4 + ...x... ..oo... ...o... ...o... + ..123.. .o123.. ..o23.. ..1o3.. + ...o... ..oo... ...o... ...o... + Consider 'Map 1' with 3 positions with 3 distinct repulsive forces, going from 1 to 3. + In 'Map 2', the player avoids all repulsive forces, total cost: 1+sqrt(2)+sqrt(2)+1 = 4.83 + In 'Map 3', the player goes through the smallest repulsive force, total cost: sqrt(2)+(extra=1)+sqrt(2) = 3.83 (optimal solution) + In 'Map 4', the player chooses to go forward, total cost: 1+(extra=2)+1 = 4.00 + +Obstacles: + hard radius: inaccessible obstacle radius (infinite repulsive force) + soft radius: accessible obstacle radius with user-defined repulsive force (fades with distance) (disabled if <= hard radius) + Example: + obstacle(0,0,1,3,5) -> obstacle at pos(0,0) with hard radius of 1m, soft radius of 3m with repulsive force 5 + - the path cannot be at <=1m from this obstacle, unless the path were to start inside that radius + - the soft radius force is maximum at the center (5) and fades with distance until (0) at 3m from the obstacle + - so to sum up, at a distance of [0,1]m the force is infinite, [1,3]m the force goes from 3.333 to 0 + obstacle(-2.1,3,0,0,0) -> obstacle at pos(-2.1,3) with hard radius of 0m, soft radius of 0m with repulsive force 0 + - the path cannot go through (-2.1,3) + obstacle(-2.16,3,0,0,8) -> obstacle at pos(-2.2,3) with hard radius of 0m, soft radius of 0m with repulsive force 8 + - the path cannot go through (-2.2,3), the map has a precision of 10cm, so the obstacle is placed at the nearest valid position + - the repulsive force is ignored because (soft radius <= hard radius) +''' + + + +class Pathfinding(): + def __init__(self, script:Script) -> None: + self.script = script + a_star.compute(np.zeros(6, np.float32)) # Initialize (not needed, but the first run takes a bit more time) + + def draw_grid(self): + d = self.player.world.draw + MAX_RAW_COST = 0.6 # dribble cushion + + for x in np.arange(-16,16.01,0.1): + for y in np.arange(-11,11.01,0.1): + s_in, cost_in = a_star.compute(np.array([x, y, 0, 0, x, y, 5000], np.float32))[-2:] # do not allow out of bounds + s_out, cost_out = a_star.compute(np.array([x, y, 1, 0, x, y, 5000], np.float32))[-2:] # allow out of bounds + #print(path_cost_in, path_cost_out) + if s_out != 3: + d.point((x,y), 5, d.Color.red, "grid", False) + elif s_in != 3: + d.point((x,y), 4, d.Color.blue_pale, "grid", False) + elif 0 < cost_in < MAX_RAW_COST + 1e-6: + d.point((x,y), 4, d.Color.get(255,(1-cost_in/MAX_RAW_COST)*255,0), "grid", False) + elif cost_in > MAX_RAW_COST: + d.point((x,y), 4, d.Color.black, "grid", False) + #else: + # d.point((x,y), 4, d.Color.white, "grid", False) + d.flush("grid") + + def sync(self): + r = self.player.world.robot + self.player.behavior.head.execute() + self.player.scom.commit_and_send( r.get_command() ) + self.player.scom.receive() + + def draw_path_and_obstacles(self, obst, path_ret_pb, path_ret_bp): + w = self.player.world + + # draw obstacles + for i in range(0,len(obst[0]),5): + w.draw.circle(obst[0][i:i+2], obst[0][i+2], 2, w.draw.Color.red, "obstacles", False) + w.draw.circle(obst[0][i:i+2], obst[0][i+3], 2, w.draw.Color.orange, "obstacles", False) + + # draw path + path_pb = path_ret_pb[:-2] # create view without status + path_status_pb = path_ret_pb[-2] # extract status + path_cost_pb = path_ret_pb[-1] # extract A* cost + path_bp = path_ret_bp[:-2] # create view without status + path_status_bp = path_ret_bp[-2] # extract status + path_cost_bp = path_ret_bp[-1] # extract A* cost + + c_pb = {0: w.draw.Color.green_lime, 1: w.draw.Color.yellow, 2: w.draw.Color.red, 3: w.draw.Color.blue_light}[path_status_pb] + c_bp = {0: w.draw.Color.green_pale, 1: w.draw.Color.yellow_light, 2: w.draw.Color.red_salmon, 3: w.draw.Color.blue_pale}[path_status_bp] + + for i in range(2,len(path_pb)-2,2): + w.draw.line(path_pb[i-2:i],path_pb[i:i+2], 5, c_pb, "path_player_ball", False) + + if len(path_pb)>=4: + w.draw.arrow(path_pb[-4:-2],path_pb[-2:],0.4, 5, c_pb, "path_player_ball", False) + + for i in range(2,len(path_bp)-2,2): + w.draw.line(path_bp[i-2:i],path_bp[i:i+2], 5, c_bp, "path_ball_player", False) + + if len(path_bp)>=4: + w.draw.arrow(path_bp[-4:-2],path_bp[-2:],0.4, 5, c_bp, "path_ball_player", False) + + w.draw.flush("obstacles") + w.draw.flush("path_player_ball") + w.draw.flush("path_ball_player") + + def move_obstacles(self, obst): + + for i in range(len(obst[0])//5): + obst[0][i*5] +=obst[1][i,0] + obst[0][i*5+1]+=obst[1][i,1] + if not -16ball) + param_vec_bp = np.array([*ball, 0, go_to_goal, *rpos, timeout, *obst[0]], np.float32) # don't allow (ball->player) + t1 = time.time() + path_ret_pb = a_star.compute(param_vec_pb) + t2 = time.time() + path_ret_bp = a_star.compute(param_vec_bp) + t3 = time.time() + + print(end=f"\rplayer->ball {int((t2-t1)*1000000):5}us (len:{len(path_ret_pb[:-2])//2:4}) ball->player {int((t3-t2)*1000000):5}us (len:{len(path_ret_bp[:-2])//2:4}) ") + + self.draw_path_and_obstacles( obst, path_ret_pb, path_ret_bp ) + self.sync() diff --git a/scripts/utils/Radio_Localization.py b/scripts/utils/Radio_Localization.py new file mode 100644 index 0000000..ccdca6f --- /dev/null +++ b/scripts/utils/Radio_Localization.py @@ -0,0 +1,97 @@ +from agent.Agent import Agent +from itertools import count +from scripts.commons.Script import Script +from typing import List +from world.commons.Draw import Draw + +class Radio_Localization(): + def __init__(self,script:Script) -> None: + self.script = script + + def draw_objects(self, p:Agent, pos, is_down, was_seen, last_update, is_self=False): + w = p.world + me = w.robot.loc_head_position + + # get draw object from same player to always overwrite previous drawing + # could also use team channel but with this approach we could draw for both teams + d:Draw = self.script.players[0].world.draw + + # VISUALSTEP_MS is the time it takes to get a visual update + is_current = last_update > w.time_local_ms - w.VISUALSTEP_MS + + # 0.12s is the time it takes to do a full broadcast with all positions if every group is completely visible + # here we use >= instead of > because the radio message comes with a delay of 20ms + is_recent = last_update >= w.time_local_ms - 120 + + if is_current and was_seen: + c = d.Color.green_light # I've seen this object in the current or previous time step + elif is_recent and was_seen: + c = d.Color.green # I've seen this object in the last 0.12s + elif is_current: + c = d.Color.yellow # I've heard about this object in the current or previous time step (and it was not seen in the same period) + elif is_recent: + c = d.Color.yellow_light # I've heard about this object in the last 0.12s (the last available info was not obtained from vision) + else: + c = d.Color.red # I haven't seen or heard about this object in the last 0.12s + + if is_self: + if w.robot.radio_fallen_state: + d.annotation(me, "Fallen (radio)", d.Color.yellow, "objects", False) # I heard I've fallen (but I missed the last 2 visual steps) + elif w.robot.loc_head_z < 0.3: + d.annotation(me, "Fallen (internal)", d.Color.white, "objects", False) # I have detected I've fallen + d.sphere(me, 0.06, c, "objects", False) + else: + if is_down: + d.annotation((me[:2]+pos[:2])/2,"Fallen",d.Color.white,"objects",False) + d.arrow(me, pos, 0.1, 3, c, "objects", False) + + + def draw(self,p:Agent): + w = p.world + others = w.teammates + w.opponents + + #----------------------------------------------------------- draw other players + + for o in others: + if o.is_self or o.state_last_update==0: # do not draw self or never before seen players + continue + + pos = o.state_abs_pos + is_down = o.state_fallen + # 3D head position means head is visible, 2D means some body parts are visible but not the head, or the head position comes from radio + is_3D = pos is not None and len(pos)==3 + + self.draw_objects(p, pos, is_down, is_3D, o.state_last_update) + + #----------------------------------------------------------- draw self + + is_pos_from_vision = w.robot.loc_head_position_last_update == w.robot.loc_last_update + self.draw_objects(p, None, None, is_pos_from_vision, w.robot.loc_head_position_last_update, True) + + #----------------------------------------------------------- draw ball and flush drawings + + self.draw_objects(p, w.ball_abs_pos, False, w.is_ball_abs_pos_from_vision, w.ball_abs_pos_last_update) + self.script.players[0].world.draw.flush("objects") + + + def execute(self): + a = self.script.args + + # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name, Enable Log, Enable Draw + self.script.batch_create(Agent, ((a.i,a.p,a.m,u,a.t, False,u==1) for u in range(1,12))) + self.script.batch_create(Agent, ((a.i,a.p,a.m,u,"Opponent",False,False) for u in range(1,12))) + players : List[Agent] = self.script.players + + # Beam opponents + beam_pos = [(-(i//2)-3,(i%2*2-1)*(i//2+1),0) for i in range(11)] + self.script.batch_commit_beam( beam_pos, slice(11,None) ) + print("\nPress ctrl+c to return.") + + # Execute + for j in count(): + self.script.batch_execute_agent( slice(11) ) # run our agents (think and send) + self.script.batch_commit_and_send( slice(11,None) ) # run their agents (don't think, just send) + + self.draw(players[j//15%11]) # draw knowledge, iterate through our team, 15 time steps per player + self.script.batch_receive(slice(11)) # receive & update our team's world state + self.script.batch_receive(slice(11,None), False) # receive & don't update opponent's world state (to save cpu resources) diff --git a/scripts/utils/Server.py b/scripts/utils/Server.py new file mode 100644 index 0000000..47dc714 --- /dev/null +++ b/scripts/utils/Server.py @@ -0,0 +1,148 @@ +import os + +class Server(): + + def __init__(self,script) -> None: + + if os.path.isdir("/usr/local/share/rcssserver3d/"): + self.source = "/usr/local/share/rcssserver3d/" + elif os.path.isdir("/usr/share/rcssserver3d/"): + self.source = "/usr/share/rcssserver3d/" + else: + raise FileNotFoundError("The server configuration files were not found!") + + # To add options: insert into options & explations with same index, read respective value from file or from other values, add edit entry + + self.options = ["Official Config", "Penalty Shootout", "Soccer Rules", "Sync Mode", "Real Time", "Cheats", "Full Vision", "Add Noise", "25Hz Monitor"] + self.descriptions = ["Configuration used in official matches", "Server's Penalty Shootout mode", "Play modes, automatic referee, etc.", + "Synchronous communication between agents and server", "Real Time (or maximum server speed)", + "Agent position & orientation, ball position", "See 360 deg instead of 120 deg (vertically & horizontally)", + "Noise added to the position of visible objects", "25Hz Monitor (or 50Hz but RoboViz will show 2x the actual speed)"] + + spark_f = os.path.expanduser("~/.simspark/spark.rb") + naoneckhead_f = self.source+"rsg/agent/nao/naoneckhead.rsg" + + self.files = {"Penalty Shootout" : self.source + "naosoccersim.rb", + "Soccer Rules" : self.source + "naosoccersim.rb", + "Sync Mode" : spark_f, + "Real Time" : self.source+"rcssserver3d.rb", + "Cheats" : naoneckhead_f, + "Full Vision" : naoneckhead_f, + "Add Noise" : naoneckhead_f, + "25Hz Monitor" : spark_f} + + + def label(self, setting_name, t_on, t_off): + with open(self.files[setting_name], "r") as sources: + content = sources.read() + + if t_on in content: + self.values[setting_name] = "On" + elif t_off in content: + self.values[setting_name] = "Off" + else: + self.values[setting_name] = "Error" + + + def read_config(self): + v = self.values = dict() + + print("Reading server configuration files...") + + self.label("Penalty Shootout", "addSoccerVar('PenaltyShootout', true)", "addSoccerVar('PenaltyShootout', false)") + self.label("Soccer Rules", " gameControlServer.initControlAspect('SoccerRuleAspect')", "#gameControlServer.initControlAspect('SoccerRuleAspect')") + self.label("Real Time", "enableRealTimeMode = true", "enableRealTimeMode = false") + self.label("Cheats", "setSenseMyPos true", "setSenseMyPos false") + self.label("Full Vision", "setViewCones 360 360", "setViewCones 120 120") + self.label("Add Noise", "addNoise true", "addNoise false") + self.label("Sync Mode", "agentSyncMode = true", "agentSyncMode = false") + self.label("25Hz Monitor", "monitorStep = 0.04", "monitorStep = 0.02") + + is_official_config = (v["Penalty Shootout"] == "Off" and v["Soccer Rules"] == "On" and v["Real Time"] == "On" and v["Cheats"] == "Off" and v["Full Vision"] == "Off" and + v["Add Noise"] == "On" and v["Sync Mode"] == "Off" and v["25Hz Monitor"] == "On") + print(v["Penalty Shootout"], is_official_config) + v["Official Config"] = "On" if is_official_config else "Off" + + def change_config(self, setting_name, t_on, t_off, current_value=None, file=None): + if current_value is None: + current_value = self.values[setting_name] + + if file is None: + file = self.files[setting_name] + + with open(file, "r") as sources: + t = sources.read() + if current_value == "On": + t = t.replace(t_on, t_off, 1) + print(f"Replacing '{t_on}' with '{t_off}' in '{file}'") + elif current_value == "Off": + t = t.replace(t_off, t_on, 1) + print(f"Replacing '{t_off}' with '{t_on}' in '{file}'") + else: + print(setting_name, "was not changed because the value is unknown!") + with open(file, "w") as sources: + sources.write(t) + + def execute(self): + + while True: + self.read_config() + + # Convert convenient values dict to list + values_list = [self.values[o] for o in self.options] + + print() + UI.print_table( [self.options, values_list, self.descriptions], ["Setting", "Value", "Description"], numbering=[True, False, False]) + choice = UI.read_int('Choose setting (ctrl+c to return): ',0,len(self.options)) + opt = self.options[choice] + + prefix = ['sudo', 'python3', 'scripts/utils/Server.py', opt] + + if opt in self.files: + suffix = [self.values[opt], self.files[opt]] + + if opt == "Penalty Shootout": + subprocess.call([*prefix, "addSoccerVar('PenaltyShootout', true)", "addSoccerVar('PenaltyShootout', false)", *suffix]) + elif opt == "Soccer Rules": + subprocess.call([*prefix, "gameControlServer.initControlAspect('SoccerRuleAspect')", "#gameControlServer.initControlAspect('SoccerRuleAspect')", *suffix]) + elif opt == "Sync Mode": + self.change_config(opt, "agentSyncMode = true", "agentSyncMode = false") # doesn't need sudo privileges + elif opt == "Real Time": + subprocess.call([*prefix, "enableRealTimeMode = true", "enableRealTimeMode = false", *suffix]) + elif opt == "Cheats": + subprocess.call([*prefix, "setSenseMyPos true", "setSenseMyPos false", *suffix, + opt, "setSenseMyOrien true", "setSenseMyOrien false", *suffix, + opt, "setSenseBallPos true", "setSenseBallPos false", *suffix]) + elif opt == "Full Vision": + subprocess.call([*prefix, "setViewCones 360 360", "setViewCones 120 120", *suffix]) + elif opt == "Add Noise": + subprocess.call([*prefix, "addNoise true", "addNoise false", *suffix]) + elif opt == "25Hz Monitor": + self.change_config(opt, "monitorStep = 0.04", "monitorStep = 0.02") # doesn't need sudo privileges + elif opt == "Official Config": + if self.values[opt] == "On": + print("The official configuration is already On!") + else: # here, the first option is the official value + subprocess.call([*prefix[:3], + "Penalty Shootout", "addSoccerVar('PenaltyShootout', false)", "addSoccerVar('PenaltyShootout', true)", "Off", self.files["Penalty Shootout"], + "Soccer Rules", "gameControlServer.initControlAspect('SoccerRuleAspect')", "#gameControlServer.initControlAspect('SoccerRuleAspect')", "Off", self.files["Soccer Rules"], + "Sync Mode", "agentSyncMode = false", "agentSyncMode = true", "Off", self.files["Sync Mode"], + "Real Time", "enableRealTimeMode = true", "enableRealTimeMode = false", "Off", self.files["Real Time"], + "Cheats", "setSenseMyPos false", "setSenseMyPos true", "Off", self.files["Cheats"], + "Cheats", "setSenseMyOrien false", "setSenseMyOrien true", "Off", self.files["Cheats"], + "Cheats", "setSenseBallPos false", "setSenseBallPos true", "Off", self.files["Cheats"], + "Full Vision", "setViewCones 120 120", "setViewCones 360 360", "Off", self.files["Full Vision"], + "Add Noise", "addNoise true", "addNoise false", "Off", self.files["Add Noise"], + "25Hz Monitor", "monitorStep = 0.04", "monitorStep = 0.02", "Off", self.files["25Hz Monitor"]]) + + +# process with sudo privileges to change the configuration files +if __name__ == "__main__": + import sys + s = Server(None) + + for i in range(1,len(sys.argv),5): + s.change_config( *sys.argv[i:i+5] ) +else: + import subprocess + from scripts.commons.UI import UI diff --git a/scripts/utils/Team_Communication.py b/scripts/utils/Team_Communication.py new file mode 100644 index 0000000..064a42c --- /dev/null +++ b/scripts/utils/Team_Communication.py @@ -0,0 +1,73 @@ +from agent.Base_Agent import Base_Agent as Agent +from itertools import count +from scripts.commons.Script import Script + +''' +How does communication work? + The say command allows a player to broadcast a message to everyone on the field + Message range: 50m (the field is 36m diagonally, so ignore this limitation) + The hear perceptor indicates 3 things: + - the message + - the origin team + - the origin absolute angle (set to "self" if the message was sent by oneself) + + Messages are heard in the next step. + Messages are only sent every 2 steps (0.04s). + Messages sent in muted steps are only heard by oneself. + In one time step, a player can only hear one other player besides itself. + If two other players say something, only the first message is heard. + This ability exists independetly for messages from both teams. + In theory, a player can hear its own message + the 1st teammate to speak + the 1st opponent to speak + In practice, the opponent doesn't matter because our team's parser ignores messages from other teams + + Message characteristics: + Maximum 20 characters, ascii between 0x20, 0x7E except ' ', '(', ')' + Accepted: letters+numbers+symbols: !"#$%&'*+,-./:;<=>?@[\]^_`{|}~ + However, due to a server bug, sending ' or " ends the message sooner + +''' + +class Team_Communication(): + + def __init__(self,script:Script) -> None: + self.script = script + + def player1_hear(self, msg:bytes, direction, timestamp:float) -> None: + print(f"Player 1 heard: {msg.decode():20} from:{direction:7} timestamp:{timestamp}") + + def player2_hear(self, msg:bytes, direction, timestamp:float) -> None: + print(f"Player 2 heard: {msg.decode():20} from:{direction:7} timestamp:{timestamp}") + + def player3_hear(self, msg:bytes, direction, timestamp:float) -> None: + print(f"Player 3 heard: {msg.decode():20} from:{direction:7} timestamp:{timestamp}") + + + def execute(self): + + a = self.script.args + + hear_callbacks = (self.player1_hear, self.player2_hear, self.player3_hear) + + # Args: Server IP, Agent Port, Monitor Port, Uniform No., Robot Type, Team Name, Enable Log, Enable Draw, Play Mode Correction, Wait for Server, Hear Callback + self.script.batch_create(Agent, ((a.i,a.p,a.m,i+1,0,a.t,True,True,False,True,clbk) for i,clbk in enumerate(hear_callbacks))) + p1:Agent = self.script.players[0] + p2:Agent = self.script.players[1] + p3:Agent = self.script.players[2] + + # Beam players + self.script.batch_commit_beam( [(-2,i,45) for i in range(3)] ) + + for i in count(): + msg1 = b"I_am_p1!_no:"+str(i).encode() + msg2 = b"I_am_p2!_no:"+str(i).encode() + msg3 = b"I_am_p3!_no:"+str(i).encode() + p1.scom.commit_announcement(msg1) # commit message + p2.scom.commit_announcement(msg2) # commit message + p3.scom.commit_announcement(msg3) # commit message + self.script.batch_commit_and_send() # send message + print(f"Player 1 sent: {msg1.decode()} HEX: {' '.join([f'{m:02X}' for m in msg1])}") + print(f"Player 2 sent: {msg2.decode()} HEX: {' '.join([f'{m:02X}' for m in msg2])}") + print(f"Player 3 sent: {msg3.decode()} HEX: {' '.join([f'{m:02X}' for m in msg3])}") + self.script.batch_receive() + input("Press enter to continue or ctrl+c to return.") + diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..48ba4b3 --- /dev/null +++ b/start.sh @@ -0,0 +1,9 @@ +#!/bin/bash +export OMP_NUM_THREADS=1 + +host=${1:-localhost} +port=${2:-3100} + +for i in {1..11}; do + python3 ./Run_Player.py -i $host -p $port -u $i -t FCPortugal -P 0 -D 0 & +done \ No newline at end of file diff --git a/start_debug.sh b/start_debug.sh new file mode 100755 index 0000000..e98c7bc --- /dev/null +++ b/start_debug.sh @@ -0,0 +1,9 @@ +#!/bin/bash +export OMP_NUM_THREADS=1 + +host=${1:-localhost} +port=${2:-3100} + +for i in {1..11}; do + python3 ./Run_Player.py -i $host -p $port -u $i -t FCP-debug -P 0 -D 1 & +done \ No newline at end of file diff --git a/start_fat_proxy.sh b/start_fat_proxy.sh new file mode 100755 index 0000000..cd86dd3 --- /dev/null +++ b/start_fat_proxy.sh @@ -0,0 +1,9 @@ +#!/bin/bash +export OMP_NUM_THREADS=1 + +host=${1:-localhost} +port=${2:-3100} + +for i in {1..11}; do + python3 ./Run_Player.py -i $host -p $port -u $i -t FCPortugal -F 1 -D 0 & +done \ No newline at end of file diff --git a/start_fat_proxy_debug.sh b/start_fat_proxy_debug.sh new file mode 100755 index 0000000..c156595 --- /dev/null +++ b/start_fat_proxy_debug.sh @@ -0,0 +1,9 @@ +#!/bin/bash +export OMP_NUM_THREADS=1 + +host=${1:-localhost} +port=${2:-3100} + +for i in {1..11}; do + python3 ./Run_Player.py -i $host -p $port -u $i -t FCP-debug -F 1 -D 1 & +done \ No newline at end of file diff --git a/start_penalty.sh b/start_penalty.sh new file mode 100755 index 0000000..f430951 --- /dev/null +++ b/start_penalty.sh @@ -0,0 +1,8 @@ +#!/bin/bash +export OMP_NUM_THREADS=1 + +host=${1:-localhost} +port=${2:-3100} + +python3 ./Run_Player.py -i $host -p $port -u 1 -t FCPortugal -P 1 -D 0 & +python3 ./Run_Player.py -i $host -p $port -u 11 -t FCPortugal -P 1 -D 0 & \ No newline at end of file diff --git a/start_penalty_debug.sh b/start_penalty_debug.sh new file mode 100755 index 0000000..002353e --- /dev/null +++ b/start_penalty_debug.sh @@ -0,0 +1,8 @@ +#!/bin/bash +export OMP_NUM_THREADS=1 + +host=${1:-localhost} +port=${2:-3100} + +python3 ./Run_Player.py -i $host -p $port -u 1 -t FCP-debug -P 1 -D 1 & +python3 ./Run_Player.py -i $host -p $port -u 11 -t FCP-debug -P 1 -D 1 & \ No newline at end of file diff --git a/world/Robot.py b/world/Robot.py new file mode 100644 index 0000000..bdeb901 --- /dev/null +++ b/world/Robot.py @@ -0,0 +1,535 @@ +from collections import deque +from math import atan, pi, sqrt, tan +from math_ops.Math_Ops import Math_Ops as M +from math_ops.Matrix_3x3 import Matrix_3x3 +from math_ops.Matrix_4x4 import Matrix_4x4 +from world.commons.Body_Part import Body_Part +from world.commons.Joint_Info import Joint_Info +import numpy as np +import xml.etree.ElementTree as xmlp + +class Robot(): + STEPTIME = 0.02 # Fixed step time + VISUALSTEP = 0.04 # Fixed visual step time + SQ_STEPTIME = STEPTIME * STEPTIME + GRAVITY = np.array([0,0,-9.81]) + IMU_DECAY = 0.996 #IMU's velocity decay + + #------------------ constants to force symmetry in joints/effectors + + MAP_PERCEPTOR_TO_INDEX = {"hj1":0, "hj2":1, "llj1":2, "rlj1":3, + "llj2":4, "rlj2":5, "llj3":6, "rlj3":7, + "llj4":8, "rlj4":9, "llj5":10,"rlj5":11, + "llj6":12,"rlj6":13,"laj1":14,"raj1":15, + "laj2":16,"raj2":17,"laj3":18,"raj3":19, + "laj4":20,"raj4":21,"llj7":22,"rlj7":23 } + + # Fix symmetry issues 1a/4 (identification) + FIX_PERCEPTOR_SET = {'rlj2','rlj6','raj2','laj3','laj4'} + FIX_INDICES_LIST = [5,13,17,18,20] + + # Recommended height for unofficial beam (near ground) + BEAM_HEIGHTS = [0.4, 0.43, 0.4, 0.46, 0.4] + + + def __init__(self, unum:int, robot_type:int) -> None: + robot_xml = "nao"+str(robot_type)+".xml" # Typical NAO file name + self.type = robot_type + self.beam_height = Robot.BEAM_HEIGHTS[robot_type] + self.no_of_joints = 24 if robot_type == 4 else 22 + + #Fix symmetry issues 1b/4 (identification) + self.FIX_EFFECTOR_MASK = np.ones(self.no_of_joints) + self.FIX_EFFECTOR_MASK[Robot.FIX_INDICES_LIST] = -1 + + self.body_parts = dict() # keys='body part names' (given by the robot's XML), values='Body_Part objects' + self.unum = unum # Robot's uniform number + self.gyro = np.zeros(3) # Angular velocity along the three axes of freedom of the robot's torso (deg/s) + self.acc = np.zeros(3) # Proper acceleration along the three axes of freedom of the robot's torso (m/s2) + self.frp = dict() # foot "lf"/"rf", toe "lf1"/"rf1" resistance perceptor (relative [p]oint of origin + [f]orce vector) e.g. {"lf":(px,py,pz,fx,fy,fz)} + self.feet_toes_last_touch = {"lf":0,"rf":0,"lf1":0,"rf1":0} # foot "lf"/"rf", toe "lf1"/"rf1" World.time_local_ms when foot/toe last touched any surface + self.feet_toes_are_touching = {"lf":False,"rf":False,"lf1":False,"rf1":False} # foot "lf"/"rf", toe "lf1"/"rf1" True if touching in last received server message + self.fwd_kinematics_list = None # List of body parts, ordered according to dependencies + self.rel_cart_CoM_position = np.zeros(3) # Center of Mass position, relative to head, in cartesian coordinates (m) + + # Joint variables are optimized for performance / array operations + self.joints_position = np.zeros(self.no_of_joints) # Joints' angular position (deg) + self.joints_speed = np.zeros(self.no_of_joints) # Joints' angular speed (rad/s) + self.joints_target_speed = np.zeros(self.no_of_joints) # Joints' target speed (rad/s) (max: 6.1395 rad/s, see rcssserver3d/data/rsg/agent/nao/hingejoint.rsg) + self.joints_target_last_speed = np.zeros(self.no_of_joints) # Joints' last target speed (rad/s) (max: 6.1395 rad/s, see rcssserver3d/data/rsg/agent/nao/hingejoint.rsg) + self.joints_info = [None] * self.no_of_joints # Joints' constant information (see class Joint_Info) + self.joints_transform = [Matrix_4x4() for _ in range(self.no_of_joints)] # Joints' transformation matrix + + # Localization variables relative to head + self.loc_head_to_field_transform = Matrix_4x4() # Transformation matrix from head to field + self.loc_field_to_head_transform = Matrix_4x4() # Transformation matrix from field to head + self.loc_rotation_head_to_field = Matrix_3x3() # Rotation matrix from head to field + self.loc_rotation_field_to_head = Matrix_3x3() # Rotation matrix from field to head + self.loc_head_position = np.zeros(3) # Absolute head position (m) + self.loc_head_position_history = deque(maxlen=40)# Absolute head position history (queue with up to 40 old positions at intervals of 0.04s, where index 0 is the previous position) + self.loc_head_velocity = np.zeros(3) # Absolute head velocity (m/s) (Warning: possibly noisy) + self.loc_head_orientation = 0 # Head orientation (deg) + self.loc_is_up_to_date = False # False if this is not a visual step, or not enough elements are visible + self.loc_last_update = 0 # World.time_local_ms when the localization was last updated + self.loc_head_position_last_update = 0 # World.time_local_ms when loc_head_position was last updated by vision or radio + self.radio_fallen_state = False # True if (radio says we fell) and (radio is significantly more recent than loc) + self.radio_last_update = 0 # World.time_local_ms when radio_fallen_state was last updated (and possibly loc_head_position) + + # Localization variables relative to torso + self.loc_torso_to_field_rotation = Matrix_3x3() # Rotation matrix from torso to field + self.loc_torso_to_field_transform = Matrix_4x4() # Transformation matrix from torso to field + self.loc_torso_roll = 0 # Torso roll (deg) + self.loc_torso_pitch = 0 # Torso pitch (deg) + self.loc_torso_orientation = 0 # Torso orientation (deg) + self.loc_torso_inclination = 0 # Torso inclination (deg) (inclination of z-axis in relation to field z-axis) + self.loc_torso_position = np.zeros(3) # Absolute torso position (m) + self.loc_torso_velocity = np.zeros(3) # Absolute torso velocity (m/s) + self.loc_torso_acceleration = np.zeros(3) # Absolute Coordinate acceleration (m/s2) + + # Other localization variables + self.cheat_abs_pos = np.zeros(3) # Absolute head position provided by the server as cheat (m) + self.cheat_ori = 0.0 # Absolute head orientation provided by the server as cheat (deg) + self.loc_CoM_position = np.zeros(3) # Absolute CoM position (m) + self.loc_CoM_velocity = np.zeros(3) # Absolute CoM velocity (m/s) + + # Localization special variables + ''' + self.loc_head_z is often equivalent to self.loc_head_position[2], but sometimes it differs. + There are situations in which the rotation and translation cannot be computed, + but the z-coordinate can still be found through vision, in which case: + self.loc_is_up_to_date is False + self.loc_head_z_is_up_to_date is True + It should be used in applications which rely on z as an independent coordinate, such + as detecting if the robot has fallen, or as an observation for machine learning. + It should NEVER be used for 3D transformations. + ''' + self.loc_head_z = 0 # Absolute head position (z) - see above for explanation (m) + self.loc_head_z_is_up_to_date = False # False if this is not a visual step, or not enough elements are visible + self.loc_head_z_last_update = 0 # World.time_local_ms when loc_head_z was last computed + self.loc_head_z_vel = 0 # Absolute head velocity (z) (m/s) + + # Localization + Gyroscope + # These variables are reliable. The gyroscope is used to update the rotation when waiting for the next visual cycle + self.imu_torso_roll = 0 # Torso roll (deg) (src: Localization + Gyro) + self.imu_torso_pitch = 0 # Torso pitch (deg) (src: Localization + Gyro) + self.imu_torso_orientation = 0 # Torso orientation (deg) (src: Localization + Gyro) + self.imu_torso_inclination = 0 # Torso inclination (deg) (src: Localization + Gyro) + self.imu_torso_to_field_rotation = Matrix_3x3() # Rotation matrix from torso to field (src: Localization + Gyro) + self.imu_last_visual_update = 0 # World.time_local_ms when the IMU data was last updated with visual information + + # Localization + Gyroscope + Accelerometer + # Warning: these variables are unreliable, since small errors in the Localization Orientation lead to + # wrong acceleration -> wrong velocity -> wrong position + self.imu_weak_torso_to_field_transform = Matrix_4x4() # Transformation matrix from torso to field (src: Localization + Gyro + Acc) + self.imu_weak_head_to_field_transform = Matrix_4x4() # Transformation matrix from head to field (src: Localization + Gyro + Acc) + self.imu_weak_field_to_head_transform = Matrix_4x4() # Transformation matrix from field to head (src: Localization + Gyro + Acc) + self.imu_weak_torso_position = np.zeros(3) # Absolute torso position (m) (src: Localization + Gyro + Acc) + self.imu_weak_torso_velocity = np.zeros(3) # Absolute torso velocity (m/s) (src: Localization + Gyro + Acc) + self.imu_weak_torso_acceleration = np.zeros(3) # Absolute torso acceleration (m/s2) (src: Localization + Gyro + Acc) + self.imu_weak_torso_next_position = np.zeros(3) # Absolute position in next step estimate (m) (src: Localization + Gyro + Acc) + self.imu_weak_torso_next_velocity = np.zeros(3) # Absolute velocity in next step estimate (m/s) (src: Localization + Gyro + Acc) + self.imu_weak_CoM_position = np.zeros(3) # Absolute CoM position (m) (src: Localization + Gyro + Acc) + self.imu_weak_CoM_velocity = np.zeros(3) # Absolute CoM velocity (m/s) (src: Localization + Gyro + Acc) + + + #Using explicit variables to enable IDE suggestions + self.J_HEAD_YAW = 0 + self.J_HEAD_PITCH = 1 + self.J_LLEG_YAW_PITCH = 2 + self.J_RLEG_YAW_PITCH = 3 + self.J_LLEG_ROLL = 4 + self.J_RLEG_ROLL = 5 + self.J_LLEG_PITCH = 6 + self.J_RLEG_PITCH = 7 + self.J_LKNEE = 8 + self.J_RKNEE = 9 + self.J_LFOOT_PITCH = 10 + self.J_RFOOT_PITCH = 11 + self.J_LFOOT_ROLL = 12 + self.J_RFOOT_ROLL = 13 + self.J_LARM_PITCH = 14 + self.J_RARM_PITCH = 15 + self.J_LARM_ROLL = 16 + self.J_RARM_ROLL = 17 + self.J_LELBOW_YAW = 18 + self.J_RELBOW_YAW = 19 + self.J_LELBOW_ROLL = 20 + self.J_RELBOW_ROLL = 21 + self.J_LTOE_PITCH = 22 + self.J_RTOE_PITCH = 23 + + + #------------------ parse robot xml + + dir = M.get_active_directory("/world/commons/robots/") + robot_xml_root = xmlp.parse(dir + robot_xml).getroot() + + joint_no = 0 + for child in robot_xml_root: + if child.tag == "bodypart": + self.body_parts[child.attrib['name']] = Body_Part(child.attrib['mass']) + elif child.tag == "joint": + self.joints_info[joint_no] = Joint_Info(child) + self.joints_position[joint_no] = 0.0 + ji = self.joints_info[joint_no] + + #save joint if body part is 1st anchor (to simplify model traversal in a single direction) + self.body_parts[ji.anchor0_part].joints.append(Robot.MAP_PERCEPTOR_TO_INDEX[ji.perceptor]) + + joint_no += 1 + if joint_no == self.no_of_joints: break #ignore extra joints + + else: + raise NotImplementedError + + assert joint_no == self.no_of_joints, "The Robot XML and the robot type don't match!" + + + def get_head_abs_vel(self, history_steps:int): + ''' + Get robot's head absolute velocity (m/s) + + Parameters + ---------- + history_steps : int + number of history steps to consider [1,40] + + Examples + -------- + get_head_abs_vel(1) is equivalent to (current abs pos - last abs pos) / 0.04 + get_head_abs_vel(2) is equivalent to (current abs pos - abs pos 0.08s ago) / 0.08 + get_head_abs_vel(3) is equivalent to (current abs pos - abs pos 0.12s ago) / 0.12 + ''' + assert 1 <= history_steps <= 40, "Argument 'history_steps' must be in range [1,40]" + + if len(self.loc_head_position_history) == 0: + return np.zeros(3) + + h_step = min(history_steps, len(self.loc_head_position_history)) + t = h_step * Robot.VISUALSTEP + + return (self.loc_head_position - self.loc_head_position_history[h_step-1]) / t + + + def _initialize_kinematics(self): + + #starting with head + parts={"head"} + sequential_body_parts = ["head"] + + while len(parts) > 0: + part = parts.pop() + + for j in self.body_parts[part].joints: + + p = self.joints_info[j].anchor1_part + + if len(self.body_parts[p].joints) > 0: #add body part if it is the 1st anchor of some joint + parts.add(p) + sequential_body_parts.append(p) + + self.fwd_kinematics_list = [(self.body_parts[part],j, self.body_parts[self.joints_info[j].anchor1_part] ) + for part in sequential_body_parts for j in self.body_parts[part].joints] + + #Fix symmetry issues 4/4 (kinematics) + for i in Robot.FIX_INDICES_LIST: + self.joints_info[i].axes *= -1 + aux = self.joints_info[i].min + self.joints_info[i].min = -self.joints_info[i].max + self.joints_info[i].max = -aux + + + def update_localization(self, localization_raw, time_local_ms): + + # parse raw data + loc = localization_raw.astype(float) #32bits to 64bits for consistency + self.loc_is_up_to_date = bool(loc[32]) + self.loc_head_z_is_up_to_date = bool(loc[34]) + + if self.loc_head_z_is_up_to_date: + time_diff = (time_local_ms - self.loc_head_z_last_update) / 1000 + self.loc_head_z_vel = (loc[33] - self.loc_head_z) / time_diff + self.loc_head_z = loc[33] + self.loc_head_z_last_update = time_local_ms + + # Save last position to history at every vision cycle (even if not up to date) (update_localization is only called at vision cycles) + self.loc_head_position_history.appendleft(np.copy(self.loc_head_position)) + + if self.loc_is_up_to_date: + time_diff = (time_local_ms - self.loc_last_update) / 1000 + self.loc_last_update = time_local_ms + self.loc_head_to_field_transform.m[:] = loc[0:16].reshape((4,4)) + self.loc_field_to_head_transform.m[:] = loc[16:32].reshape((4,4)) + + # extract data (related to the robot's head) + self.loc_rotation_head_to_field = self.loc_head_to_field_transform.get_rotation() + self.loc_rotation_field_to_head = self.loc_field_to_head_transform.get_rotation() + p = self.loc_head_to_field_transform.get_translation() + self.loc_head_velocity = (p - self.loc_head_position) / time_diff + self.loc_head_position = p + self.loc_head_position_last_update = time_local_ms + self.loc_head_orientation = self.loc_head_to_field_transform.get_yaw_deg() + self.radio_fallen_state = False + + # extract data (related to the center of mass) + p = self.loc_head_to_field_transform(self.rel_cart_CoM_position) + self.loc_CoM_velocity = (p - self.loc_CoM_position) / time_diff + self.loc_CoM_position = p + + # extract data (related to the robot's torso) + t = self.get_body_part_to_field_transform('torso') + self.loc_torso_to_field_transform = t + self.loc_torso_to_field_rotation = t.get_rotation() + self.loc_torso_orientation = t.get_yaw_deg() + self.loc_torso_pitch = t.get_pitch_deg() + self.loc_torso_roll = t.get_roll_deg() + self.loc_torso_inclination = t.get_inclination_deg() + p = t.get_translation() + self.loc_torso_velocity = (p - self.loc_torso_position) / time_diff + self.loc_torso_position = p + self.loc_torso_acceleration = self.loc_torso_to_field_rotation.multiply(self.acc) + Robot.GRAVITY + + + def head_to_body_part_transform(self, body_part_name, coords, is_batch=False): + ''' + If coord is a vector or list of vectors: + Convert cartesian coordinates that are relative to head to coordinates that are relative to a body part + + If coord is a Matrix_4x4 or a list of Matrix_4x4: + Convert pose that is relative to head to a pose that is relative to a body part + + Parameters + ---------- + body_part_name : `str` + name of body part (given by the robot's XML) + coords : array_like + One 3D position or list of 3D positions + is_batch : `bool` + Indicates if coords is a batch of 3D positions + + Returns + ------- + coord : `list` or ndarray + A numpy array is returned if is_batch is False, otherwise, a list of arrays is returned + ''' + head_to_bp_transform : Matrix_4x4 = self.body_parts[body_part_name].transform.invert() + + if is_batch: + return [head_to_bp_transform(c) for c in coords] + else: + return head_to_bp_transform(coords) + + + + def get_body_part_to_field_transform(self, body_part_name) -> Matrix_4x4: + ''' + Computes the transformation matrix from body part to field, from which we can extract its absolute position and rotation. + For best results, use this method when self.loc_is_up_to_date is True. Otherwise, the forward kinematics + will not be synced with the localization data and strange results may occur. + ''' + return self.loc_head_to_field_transform.multiply(self.body_parts[body_part_name].transform) + + def get_body_part_abs_position(self, body_part_name) -> np.ndarray: + ''' + Computes the absolute position of a body part considering the localization data and forward kinematics. + For best results, use this method when self.loc_is_up_to_date is True. Otherwise, the forward kinematics + will not be synced with the localization data and strange results may occur. + ''' + return self.get_body_part_to_field_transform(body_part_name).get_translation() + + def get_joint_to_field_transform(self, joint_index) -> Matrix_4x4: + ''' + Computes the transformation matrix from joint to field, from which we can extract its absolute position and rotation. + For best results, use this method when self.loc_is_up_to_date is True. Otherwise, the forward kinematics + will not be synced with the localization data and strange results may occur. + ''' + return self.loc_head_to_field_transform.multiply(self.joints_transform[joint_index]) + + def get_joint_abs_position(self, joint_index) -> np.ndarray: + ''' + Computes the absolute position of a joint considering the localization data and forward kinematics. + For best results, use this method when self.loc_is_up_to_date is True. Otherwise, the forward kinematics + will not be synced with the localization data and strange results may occur. + ''' + return self.get_joint_to_field_transform(joint_index).get_translation() + + def update_pose(self): + + if self.fwd_kinematics_list is None: + self._initialize_kinematics() + + for body_part, j, child_body_part in self.fwd_kinematics_list: + ji = self.joints_info[j] + self.joints_transform[j].m[:] = body_part.transform.m + self.joints_transform[j].translate(ji.anchor0_axes, True) + child_body_part.transform.m[:] = self.joints_transform[j].m + child_body_part.transform.rotate_deg(ji.axes, self.joints_position[j], True) + child_body_part.transform.translate(ji.anchor1_axes_neg, True) + + self.rel_cart_CoM_position = np.average([b.transform.get_translation() for b in self.body_parts.values()], 0, + [b.mass for b in self.body_parts.values()]) + + + def update_imu(self, time_local_ms): + + # update IMU + if self.loc_is_up_to_date: + self.imu_torso_roll = self.loc_torso_roll + self.imu_torso_pitch = self.loc_torso_pitch + self.imu_torso_orientation = self.loc_torso_orientation + self.imu_torso_inclination = self.loc_torso_inclination + self.imu_torso_to_field_rotation.m[:] = self.loc_torso_to_field_rotation.m + self.imu_weak_torso_to_field_transform.m[:] = self.loc_torso_to_field_transform.m + self.imu_weak_head_to_field_transform.m[:] = self.loc_head_to_field_transform.m + self.imu_weak_field_to_head_transform.m[:] = self.loc_field_to_head_transform.m + self.imu_weak_torso_position[:] = self.loc_torso_position + self.imu_weak_torso_velocity[:] = self.loc_torso_velocity + self.imu_weak_torso_acceleration[:] = self.loc_torso_acceleration + self.imu_weak_torso_next_position = self.loc_torso_position + self.loc_torso_velocity * Robot.STEPTIME + self.loc_torso_acceleration * (0.5 * Robot.SQ_STEPTIME) + self.imu_weak_torso_next_velocity = self.loc_torso_velocity + self.loc_torso_acceleration * Robot.STEPTIME + self.imu_weak_CoM_position[:] = self.loc_CoM_position + self.imu_weak_CoM_velocity[:] = self.loc_CoM_velocity + self.imu_last_visual_update = time_local_ms + else: + g = self.gyro / 50 # convert degrees per second to degrees per step + + self.imu_torso_to_field_rotation.multiply( Matrix_3x3.from_rotation_deg(g), in_place=True, reverse_order=True) + + self.imu_torso_orientation = self.imu_torso_to_field_rotation.get_yaw_deg() + self.imu_torso_pitch = self.imu_torso_to_field_rotation.get_pitch_deg() + self.imu_torso_roll = self.imu_torso_to_field_rotation.get_roll_deg() + + self.imu_torso_inclination = atan(sqrt(tan(self.imu_torso_roll/180*pi)**2+tan(self.imu_torso_pitch/180*pi)**2))*180/pi + + # Update position and velocity until 0.2 seconds has passed since last visual update + if time_local_ms < self.imu_last_visual_update + 200: + self.imu_weak_torso_position[:] = self.imu_weak_torso_next_position + if self.imu_weak_torso_position[2] < 0: self.imu_weak_torso_position[2] = 0 # limit z coordinate to positive values + self.imu_weak_torso_velocity[:] = self.imu_weak_torso_next_velocity * Robot.IMU_DECAY # stability tradeoff + else: + self.imu_weak_torso_velocity *= 0.97 # without visual updates for 0.2s, the position is locked, and the velocity decays to zero + + # convert proper acceleration to coordinate acceleration and fix rounding bias + self.imu_weak_torso_acceleration = self.imu_torso_to_field_rotation.multiply(self.acc) + Robot.GRAVITY + self.imu_weak_torso_to_field_transform = Matrix_4x4.from_3x3_and_translation(self.imu_torso_to_field_rotation,self.imu_weak_torso_position) + self.imu_weak_head_to_field_transform = self.imu_weak_torso_to_field_transform.multiply(self.body_parts["torso"].transform.invert()) + self.imu_weak_field_to_head_transform = self.imu_weak_head_to_field_transform.invert() + p = self.imu_weak_head_to_field_transform(self.rel_cart_CoM_position) + self.imu_weak_CoM_velocity = (p-self.imu_weak_CoM_position)/Robot.STEPTIME + self.imu_weak_CoM_position = p + + # Next Position = x0 + v0*t + 0.5*a*t^2, Next velocity = v0 + a*t + self.imu_weak_torso_next_position = self.imu_weak_torso_position + self.imu_weak_torso_velocity * Robot.STEPTIME + self.imu_weak_torso_acceleration * (0.5 * Robot.SQ_STEPTIME) + self.imu_weak_torso_next_velocity = self.imu_weak_torso_velocity + self.imu_weak_torso_acceleration * Robot.STEPTIME + + + + def set_joints_target_position_direct(self,indices,values:np.ndarray,harmonize=True,max_speed=7.03,tolerance=0.012,limit_joints=True) -> int: + ''' + Computes the speed of a list of joints, taking as argument the target position + + Parameters + ---------- + indices : `int`/`list`/`slice`/numpy array + joint indices + values : numpy array + target position for each listed joint index + harmonize : `bool` + if True, all joints reach target at same time + max_speed : `float` + max. speed for all joints in deg/step + Most joints have a maximum speed of 351.77 deg/s according to rcssserver3d/data/rsg/agent/nao/hingejoint.rsg + That translates as 7.0354 deg/step or 6.1395 rad/s + tolerance : `float` + angle error tolerance (in degrees) to return that target was reached (returns -1) + limit_joints : `bool` + limit values to the joints' range of motion + + Returns + ------- + remaining_steps : `int` + predicted number of remaining steps or -1 if target was already reached + + Examples + ------- + (let p[tx] be the joint position at t=x) + + Example for return value: moving joint[0] from 0deg to 10deg + pos[t0]: 0, speed[t0]: 7deg/step, ret=2 # target will predictedly be reached in 2 steps + pos[t1]: 7, speed[t1]: 3deg/step, ret=1 # target will predictedly be reached in 1 step (send final action) + pos[t2]: 10, speed[t2]: 0deg/step, ret=0 # target was predictedly already reached + pos[t3]: 10, speed[t3]: 0deg/step, ret=-1 # (best case scenario) server reported with delay, that target was reached (see tolerance) + pos[t?]: 10, speed[t?]: 0deg/step, ret=-1 # if there is friction, it may take some additional steps + + If everything worked as predicted we could stop calling this function when ret==1 + If we need precision, it is recommended to wait for ret==-1 + + Example 1: + set_joints_target_position_direct(range(2,4),np.array([10.0,5.0]),harmonize=True) + Joint[2] p[t0]: 0 target pos: 10 -> p[t1]=5, p[t2]=10 + Joint[3] p[t0]: 0 target pos: 5 -> p[t1]=2.5, p[t2]=5 + + Example 2: + set_joints_target_position_direct([2,3],np.array([10.0,5.0]),harmonize=False) + Joint[2] p[t0]: 0 target pos: 10 -> p[t1]=7, p[t2]=10 + Joint[3] p[t0]: 0 target pos: 5 -> p[t1]=5, p[t2]=5 + ''' + + assert type(values) == np.ndarray, "'values' argument must be a numpy array" + np.nan_to_num(values, copy=False) # Replace NaN with zero and infinity with large finite numbers + + # limit range of joints + if limit_joints: + if type(indices) == list or type(indices) == np.ndarray: + for i in range(len(indices)): + values[i] = np.clip(values[i], self.joints_info[indices[i]].min, self.joints_info[indices[i]].max) + elif type(indices) == slice: + info = self.joints_info[indices] + for i in range(len(info)): + values[i] = np.clip(values[i], info[i].min, info[i].max) + else: # int + values[0] = np.clip(values[0], self.joints_info[indices].min, self.joints_info[indices].max) + + #predicted_diff: predicted difference between reported position and actual position + + predicted_diff = self.joints_target_last_speed[indices] * 1.1459156 #rad/s to deg/step + predicted_diff = np.asarray(predicted_diff) + np.clip(predicted_diff,-7.03,7.03,out=predicted_diff) #saturate predicted movement in-place + + #reported_dist: difference between reported position and target position + + reported_dist = values - self.joints_position[indices] + if np.all((np.abs(reported_dist) < tolerance)) and np.all((np.abs(predicted_diff) < tolerance)): + self.joints_target_speed[indices] = 0 + return -1 + + deg_per_step = reported_dist - predicted_diff + + relative_max = np.max( np.abs(deg_per_step) ) / max_speed + remaining_steps = np.ceil( relative_max ) + + if remaining_steps == 0: + self.joints_target_speed[indices] = 0 + return 0 + + if harmonize: + deg_per_step /= remaining_steps + else: + np.clip(deg_per_step,-max_speed,max_speed,out=deg_per_step) #limit maximum speed + + self.joints_target_speed[indices] = deg_per_step * 0.87266463 #convert to rad/s + + return remaining_steps + + + + def get_command(self) -> bytes: + ''' + Builds commands string from self.joints_target_speed + ''' + j_speed = self.joints_target_speed * self.FIX_EFFECTOR_MASK #Fix symmetry issues 3/4 (effectors) + cmd = "".join(f"({self.joints_info[i].effector} {j_speed[i]:.5f})" for i in range(self.no_of_joints)).encode('utf-8') + + self.joints_target_last_speed = self.joints_target_speed #1. both point to the same array + self.joints_target_speed = np.zeros_like(self.joints_target_speed) #2. create new array for joints_target_speed + return cmd diff --git a/world/World.py b/world/World.py new file mode 100644 index 0000000..9c1f549 --- /dev/null +++ b/world/World.py @@ -0,0 +1,430 @@ +from collections import deque +from cpp.ball_predictor import ball_predictor +from cpp.localization import localization +from logs.Logger import Logger +from math import atan2, pi +from math_ops.Matrix_4x4 import Matrix_4x4 +from world.commons.Draw import Draw +from world.commons.Other_Robot import Other_Robot +from world.Robot import Robot +import numpy as np + + +class World(): + STEPTIME = 0.02 # Fixed step time + STEPTIME_MS = 20 # Fixed step time in milliseconds + VISUALSTEP = 0.04 # Fixed visual step time + VISUALSTEP_MS = 40 # Fixed visual step time in milliseconds + + # play modes in our favor + M_OUR_KICKOFF = 0 + M_OUR_KICK_IN = 1 + M_OUR_CORNER_KICK = 2 + M_OUR_GOAL_KICK = 3 + M_OUR_FREE_KICK = 4 + M_OUR_PASS = 5 + M_OUR_DIR_FREE_KICK = 6 + M_OUR_GOAL = 7 + M_OUR_OFFSIDE = 8 + + # play modes in their favor + M_THEIR_KICKOFF = 9 + M_THEIR_KICK_IN = 10 + M_THEIR_CORNER_KICK = 11 + M_THEIR_GOAL_KICK = 12 + M_THEIR_FREE_KICK = 13 + M_THEIR_PASS = 14 + M_THEIR_DIR_FREE_KICK = 15 + M_THEIR_GOAL = 16 + M_THEIR_OFFSIDE = 17 + + # neutral play modes + M_BEFORE_KICKOFF = 18 + M_GAME_OVER = 19 + M_PLAY_ON = 20 + + # play mode groups + MG_OUR_KICK = 0 + MG_THEIR_KICK = 1 + MG_ACTIVE_BEAM = 2 + MG_PASSIVE_BEAM = 3 + MG_OTHER = 4 # play on, game over + + FLAGS_CORNERS_POS = ((-15,-10,0), (-15,+10,0), (+15,-10,0), (+15,+10,0)) + FLAGS_POSTS_POS = ((-15,-1.05,0.8),(-15,+1.05,0.8),(+15,-1.05,0.8),(+15,+1.05,0.8)) + + def __init__(self,robot_type:int, team_name:str, unum:int, apply_play_mode_correction:bool, + enable_draw:bool, logger:Logger, host:str) -> None: + + self.team_name = team_name # Name of our team + self.team_name_opponent : str = None # Name of opponent team + self.apply_play_mode_correction = apply_play_mode_correction # True to adjust ball position according to play mode + self.step = 0 # Total number of received simulation steps (always in sync with self.time_local_ms) + self.time_server = 0.0 # Time, in seconds, as indicated by the server (this time is NOT reliable, use only for synchronization between agents) + self.time_local_ms = 0 # Reliable simulation time in milliseconds, use this when possible (it is incremented 20ms for every TCP message) + self.time_game = 0.0 # Game time, in seconds, as indicated by the server + self.goals_scored = 0 # Goals score by our team + self.goals_conceded = 0 # Goals conceded by our team + self.team_side_is_left : bool = None # True if our team plays on the left side (this value is later changed by the world parser) + self.play_mode = None # Play mode of the soccer game, provided by the server + self.play_mode_group = None # Certain play modes share characteristics, so it makes sense to group them + self.flags_corners : dict = None # corner flags, key=(x,y,z), always assume we play on the left side + self.flags_posts : dict = None # goal posts, key=(x,y,z), always assume we play on the left side + self.ball_rel_head_sph_pos = np.zeros(3) # Ball position relative to head (spherical coordinates) (m, deg, deg) + self.ball_rel_head_cart_pos = np.zeros(3) # Ball position relative to head (cartesian coordinates) (m) + self.ball_rel_torso_cart_pos = np.zeros(3) # Ball position relative to torso (cartesian coordinates) (m) + self.ball_rel_torso_cart_pos_history = deque(maxlen=20) # Ball position relative to torso history (queue with up to 20 old positions at intervals of 0.04s, where index 0 is the previous position) + self.ball_abs_pos = np.zeros(3) # Ball absolute position (up to date if self.ball_is_visible and self.robot.loc_is_up_to_date) (m) + self.ball_abs_pos_history = deque(maxlen=20) # Ball absolute position history (queue with up to 20 old positions at intervals of 0.04s, where index 0 is the previous position) + self.ball_abs_pos_last_update = 0 # World.time_local_ms when self.ball_abs_pos was last updated by vision or radio + self.ball_abs_vel = np.zeros(3) # Ball velocity vector based on the last 2 known values of self.ball_abs_pos (m/s) (Warning: noisy if ball is distant, use instead get_ball_abs_vel) + self.ball_abs_speed = 0 # Ball scalar speed based on the last 2 known values of self.ball_abs_pos (m/s) (Warning: noisy if ball is distant, use instead ||get_ball_abs_vel||) + self.ball_is_visible = False # True if the last server message contained vision information related to the ball + self.is_ball_abs_pos_from_vision = False # True if ball_abs_pos originated from vision, False if it originated from radio + self.ball_last_seen = 0 # World.time_local_ms when ball was last seen (note: may be different from self.ball_abs_pos_last_update) + self.ball_cheat_abs_pos = np.zeros(3) # Absolute ball position provided by the server as cheat (m) + self.ball_cheat_abs_vel = np.zeros(3) # Absolute velocity vector based on the last 2 values of self.ball_cheat_abs_pos (m/s) + self.ball_2d_pred_pos = np.zeros((1,2)) # prediction of current and future 2D ball positions* + self.ball_2d_pred_vel = np.zeros((1,2)) # prediction of current and future 2D ball velocities* + self.ball_2d_pred_spd = np.zeros(1) # prediction of current and future 2D ball linear speeds* + # *at intervals of 0.02 s until ball comes to a stop or gets out of bounds (according to prediction) + self.lines = np.zeros((30,6)) # Position of visible lines, relative to head, start_pos+end_pos (spherical coordinates) (m, deg, deg, m, deg, deg) + self.line_count = 0 # Number of visible lines + self.vision_last_update = 0 # World.time_local_ms when last vision update was received + self.vision_is_up_to_date = False # True if the last server message contained vision information + self.teammates = [Other_Robot(i, True ) for i in range(1,12)] # List of teammates, ordered by unum + self.opponents = [Other_Robot(i, False) for i in range(1,12)] # List of opponents, ordered by unum + self.teammates[unum-1].is_self = True # This teammate is self + self.draw = Draw(enable_draw, unum, host, 32769) # Draw object for current player + self.team_draw = Draw(enable_draw, 0, host, 32769) # Draw object shared with teammates + self.logger = logger + self.robot = Robot(unum, robot_type) + + + def log(self, msg:str): + ''' + Shortcut for: + + self.logger.write(msg, True, self.step) + + Parameters + ---------- + msg : str + message to be written after the simulation step + ''' + self.logger.write(msg, True, self.step) + + def get_ball_rel_vel(self, history_steps:int): + ''' + Get ball velocity, relative to torso (m/s) + + Parameters + ---------- + history_steps : int + number of history steps to consider [1,20] + + Examples + -------- + get_ball_rel_vel(1) is equivalent to (current rel pos - last rel pos) / 0.04 + get_ball_rel_vel(2) is equivalent to (current rel pos - rel pos 0.08s ago) / 0.08 + get_ball_rel_vel(3) is equivalent to (current rel pos - rel pos 0.12s ago) / 0.12 + ''' + assert 1 <= history_steps <= 20, "Argument 'history_steps' must be in range [1,20]" + + if len(self.ball_rel_torso_cart_pos_history) == 0: + return np.zeros(3) + + h_step = min(history_steps, len(self.ball_rel_torso_cart_pos_history)) + t = h_step * World.VISUALSTEP + + return (self.ball_rel_torso_cart_pos - self.ball_rel_torso_cart_pos_history[h_step-1]) / t + + def get_ball_abs_vel(self, history_steps:int): + ''' + Get ball absolute velocity (m/s) + + Parameters + ---------- + history_steps : int + number of history steps to consider [1,20] + + Examples + -------- + get_ball_abs_vel(1) is equivalent to (current abs pos - last abs pos) / 0.04 + get_ball_abs_vel(2) is equivalent to (current abs pos - abs pos 0.08s ago) / 0.08 + get_ball_abs_vel(3) is equivalent to (current abs pos - abs pos 0.12s ago) / 0.12 + ''' + assert 1 <= history_steps <= 20, "Argument 'history_steps' must be in range [1,20]" + + if len(self.ball_abs_pos_history) == 0: + return np.zeros(3) + + h_step = min(history_steps, len(self.ball_abs_pos_history)) + t = h_step * World.VISUALSTEP + + return (self.ball_abs_pos - self.ball_abs_pos_history[h_step-1]) / t + + def get_predicted_ball_pos(self, max_speed): + ''' + Get predicted 2D ball position when its predicted speed is equal to or less than `max_speed` + In case that position exceeds the prediction horizon, the last available prediction is returned + + Parameters + ---------- + max_speed : float + maximum speed at which the ball will be moving at returned future position + ''' + b_sp = self.ball_2d_pred_spd + index = len(b_sp) - max( 1, np.searchsorted(b_sp[::-1], max_speed, side='right') ) + return self.ball_2d_pred_pos[index] + + def get_intersection_point_with_ball(self, player_speed): + ''' + Get 2D intersection point with moving ball, based on `self.ball_2d_pred_pos` + + Parameters + ---------- + player_speed : float + average speed at which the robot will chase the ball + + Returns + ------- + 2D intersection point : ndarray + 2D intersection point with moving ball, assuming the robot moves at an avg. speed of `player_speed` + intersection distance : float + distance between current robot position and intersection point + ''' + + params = np.array([*self.robot.loc_head_position[:2], player_speed*0.02, *self.ball_2d_pred_pos.flat], np.float32) + pred_ret = ball_predictor.get_intersection(params) + return pred_ret[:2], pred_ret[2] + + def update(self): + r = self.robot + PM = self.play_mode + W = World + + # reset variables + r.loc_is_up_to_date = False + r.loc_head_z_is_up_to_date = False + + # update play mode groups + if PM in (W.M_PLAY_ON, W.M_GAME_OVER): # most common group + self.play_mode_group = W.MG_OTHER + elif PM in (W.M_OUR_KICKOFF, W.M_OUR_KICK_IN, W.M_OUR_CORNER_KICK, W.M_OUR_GOAL_KICK, + W.M_OUR_OFFSIDE, W.M_OUR_PASS, W.M_OUR_DIR_FREE_KICK, W.M_OUR_FREE_KICK): + self.play_mode_group = W.MG_OUR_KICK + elif PM in (W.M_THEIR_KICK_IN, W.M_THEIR_CORNER_KICK, W.M_THEIR_GOAL_KICK, W.M_THEIR_OFFSIDE, + W.M_THEIR_PASS, W.M_THEIR_DIR_FREE_KICK, W.M_THEIR_FREE_KICK, W.M_THEIR_KICKOFF): + self.play_mode_group = W.MG_THEIR_KICK + elif PM in (W.M_BEFORE_KICKOFF, W.M_THEIR_GOAL): + self.play_mode_group = W.MG_ACTIVE_BEAM + elif PM in (W.M_OUR_GOAL,): + self.play_mode_group = W.MG_PASSIVE_BEAM + elif PM is not None: + raise ValueError(f'Unexpected play mode ID: {PM}') + + r.update_pose() # update forward kinematics + if self.vision_is_up_to_date: # update vision based localization + + # Prepare all variables for localization + + feet_contact = np.zeros(6) + + lf_contact = r.frp.get('lf', None) + rf_contact = r.frp.get('rf', None) + if lf_contact is not None: + feet_contact[0:3] = Matrix_4x4( r.body_parts["lfoot"].transform ).translate( lf_contact[0:3] , True).get_translation() + if rf_contact is not None: + feet_contact[3:6] = Matrix_4x4( r.body_parts["rfoot"].transform ).translate( rf_contact[0:3] , True).get_translation() + + ball_pos = np.concatenate(( self.ball_rel_head_cart_pos, self.ball_cheat_abs_pos)) + + corners_list = [[key in self.flags_corners, 1.0, *key, *self.flags_corners.get(key,(0,0,0))] for key in World.FLAGS_CORNERS_POS] + posts_list = [[key in self.flags_posts , 0.0, *key, *self.flags_posts.get( key,(0,0,0))] for key in World.FLAGS_POSTS_POS] + all_landmarks = np.array(corners_list + posts_list, float) + + # Compute localization + + loc = localization.compute( + r.feet_toes_are_touching['lf'], + r.feet_toes_are_touching['rf'], + feet_contact, + self.ball_is_visible, + ball_pos, + r.cheat_abs_pos, + all_landmarks, + self.lines[0:self.line_count]) + + r.update_localization(loc, self.time_local_ms) + + # Update self in teammates list (only the most useful parameters, add as needed) + me = self.teammates[r.unum-1] + me.state_last_update = r.loc_last_update + me.state_abs_pos = r.loc_head_position + me.state_fallen = r.loc_head_z < 0.3 # uses same criterion as for other teammates - not as reliable as player.behavior.is_ready("Get_Up") + me.state_orientation = r.loc_torso_orientation + me.state_ground_area = (r.loc_head_position[:2],0.2) # relevant for localization demo + + # Save last ball position to history at every vision cycle (even if not up to date) + self.ball_abs_pos_history.appendleft(self.ball_abs_pos) # from vision or radio + self.ball_rel_torso_cart_pos_history.appendleft(self.ball_rel_head_cart_pos) + + ''' + Get ball position based on vision or play mode + Sources: + Corner kick position - rcssserver3d/plugin/soccer/soccerruleaspect/soccerruleaspect.cpp:1927 (May 2022) + Goal kick position - rcssserver3d/plugin/soccer/soccerruleaspect/soccerruleaspect.cpp:1900 (May 2022) + ''' + ball = None + if self.apply_play_mode_correction: + if PM == W.M_OUR_CORNER_KICK: + ball = np.array([15, 5.483 if self.ball_abs_pos[1] > 0 else -5.483, 0.042], float) + elif PM == W.M_THEIR_CORNER_KICK: + ball = np.array([-15, 5.483 if self.ball_abs_pos[1] > 0 else -5.483, 0.042], float) + elif PM in [W.M_OUR_KICKOFF, W.M_THEIR_KICKOFF, W.M_OUR_GOAL, W.M_THEIR_GOAL]: + ball = np.array([0, 0, 0.042], float) + elif PM == W.M_OUR_GOAL_KICK: + ball = np.array([-14, 0, 0.042], float) + elif PM == W.M_THEIR_GOAL_KICK: + ball = np.array([14, 0, 0.042], float) + + # Discard hard-coded ball position if robot is near that position (in favor of its own vision) + if ball is not None and np.linalg.norm(r.loc_head_position[:2] - ball[:2]) < 1: + ball = None + + if ball is None and self.ball_is_visible and r.loc_is_up_to_date: + ball = r.loc_head_to_field_transform( self.ball_rel_head_cart_pos ) + ball[2] = max(ball[2], 0.042) # lowest z = ball radius + if PM != W.M_BEFORE_KICKOFF: # for compatibility with tests without active soccer rules + ball[:2] = np.clip(ball[:2], [-15,-10], [15,10]) # force ball position to be inside field + + # Update internal ball position (also updated by Radio) + if ball is not None: + time_diff = (self.time_local_ms - self.ball_abs_pos_last_update) / 1000 + self.ball_abs_vel = (ball - self.ball_abs_pos) / time_diff + self.ball_abs_speed = np.linalg.norm(self.ball_abs_vel) + self.ball_abs_pos_last_update = self.time_local_ms + self.ball_abs_pos = ball + self.is_ball_abs_pos_from_vision = True + + # Velocity decay for teammates and opponents (it is later neutralized if the velocity is updated) + for p in self.teammates: + p.state_filtered_velocity *= p.vel_decay + for p in self.opponents: + p.state_filtered_velocity *= p.vel_decay + + # Update teammates and opponents + if r.loc_is_up_to_date: + for p in self.teammates: + if not p.is_self: # if teammate is not self + if p.is_visible: # if teammate is visible, execute full update + self.update_other_robot(p) + elif p.state_abs_pos is not None: # otherwise update its horizontal distance (assuming last known position) + p.state_horizontal_dist = np.linalg.norm(r.loc_head_position[:2] - p.state_abs_pos[:2]) + + for p in self.opponents: + if p.is_visible: # if opponent is visible, execute full update + self.update_other_robot(p) + elif p.state_abs_pos is not None: # otherwise update its horizontal distance (assuming last known position) + p.state_horizontal_dist = np.linalg.norm(r.loc_head_position[:2] - p.state_abs_pos[:2]) + + # Update prediction of ball position/velocity + if self.play_mode_group != W.MG_OTHER: # not 'play on' nor 'game over', so ball must be stationary + self.ball_2d_pred_pos = self.ball_abs_pos[:2].copy().reshape(1, 2) + self.ball_2d_pred_vel = np.zeros((1,2)) + self.ball_2d_pred_spd = np.zeros(1) + + elif self.ball_abs_pos_last_update == self.time_local_ms: # make new prediction for new ball position (from vision or radio) + + params = np.array([*self.ball_abs_pos[:2], *np.copy(self.get_ball_abs_vel(6)[:2])], np.float32) + pred_ret = ball_predictor.predict_rolling_ball(params) + sample_no = len(pred_ret) // 5 * 2 + self.ball_2d_pred_pos = pred_ret[:sample_no].reshape(-1, 2) + self.ball_2d_pred_vel = pred_ret[sample_no:sample_no*2].reshape(-1, 2) + self.ball_2d_pred_spd = pred_ret[sample_no*2:] + + elif len(self.ball_2d_pred_pos) > 1: # otherwise, advance to next predicted step, if available + self.ball_2d_pred_pos = self.ball_2d_pred_pos[1:] + self.ball_2d_pred_vel = self.ball_2d_pred_vel[1:] + self.ball_2d_pred_spd = self.ball_2d_pred_spd[1:] + + r.update_imu(self.time_local_ms) # update imu (must be executed after localization) + + if self.ball_is_visible: + # Compute ball position, relative to torso + self.ball_rel_torso_cart_pos = r.head_to_body_part_transform("torso",self.ball_rel_head_cart_pos) + + + def update_other_robot(self,other_robot : Other_Robot): + ''' + Update other robot state based on the relative position of visible body parts + (also updated by Radio, with the exception of state_orientation) + ''' + o = other_robot + r = self.robot + + # update body parts absolute positions + o.state_body_parts_abs_pos = o.body_parts_cart_rel_pos.copy() + for bp, pos in o.body_parts_cart_rel_pos.items(): + # Using the IMU could be beneficial if we see other robots but can't self-locate + o.state_body_parts_abs_pos[bp] = r.loc_head_to_field_transform( pos, False ) + + # auxiliary variables + bps_apos = o.state_body_parts_abs_pos # read-only shortcut + bps_2d_apos_list = [v[:2] for v in bps_apos.values()] # list of body parts' 2D absolute positions + avg_2d_pt = np.average(bps_2d_apos_list, axis=0) # 2D avg pos of visible body parts + head_is_visible = 'head' in bps_apos + + # evaluate robot's state (unchanged if head is not visible) + if head_is_visible: + o.state_fallen = bps_apos['head'][2] < 0.3 + + # compute velocity if head is visible + if o.state_abs_pos is not None: + time_diff = (self.time_local_ms - o.state_last_update) / 1000 + if head_is_visible: + # if last position is 2D, we assume that the z coordinate did not change, so that v.z=0 + old_p = o.state_abs_pos if len(o.state_abs_pos)==3 else np.append(o.state_abs_pos, bps_apos['head'][2]) + velocity = (bps_apos['head'] - old_p) / time_diff + decay = o.vel_decay # neutralize decay in all axes + else: # if head is not visible, we only update the x & y components of the velocity + velocity = np.append( (avg_2d_pt - o.state_abs_pos[:2]) / time_diff, 0) + decay = (o.vel_decay,o.vel_decay,1) # neutralize decay (except in the z-axis) + # apply filter + if np.linalg.norm(velocity - o.state_filtered_velocity) < 4: # otherwise assume it was beamed + o.state_filtered_velocity /= decay # neutralize decay + o.state_filtered_velocity += o.vel_filter * (velocity-o.state_filtered_velocity) + + # compute robot's position (preferably based on head) + if head_is_visible: + o.state_abs_pos = bps_apos['head'] # 3D head position, if head is visible + else: + o.state_abs_pos = avg_2d_pt # 2D avg pos of visible body parts + + # compute robot's horizontal distance (head distance, or avg. distance of visible body parts) + o.state_horizontal_dist = np.linalg.norm(r.loc_head_position[:2] - o.state_abs_pos[:2]) + + # compute orientation based on pair of lower arms or feet, or average of both + lr_vec = None + if 'llowerarm' in bps_apos and 'rlowerarm' in bps_apos: + lr_vec = bps_apos['rlowerarm'] - bps_apos['llowerarm'] + + if 'lfoot' in bps_apos and 'rfoot' in bps_apos: + if lr_vec is None: + lr_vec = bps_apos['rfoot'] - bps_apos['lfoot'] + else: + lr_vec = (lr_vec + (bps_apos['rfoot'] - bps_apos['lfoot'])) / 2 + + if lr_vec is not None: + o.state_orientation = atan2(lr_vec[1],lr_vec[0]) * 180 / pi + 90 + + # compute projection of player area on ground (circle) + if o.state_horizontal_dist < 4: # we don't need precision if the robot is farther than 4m + max_dist = np.max(np.linalg.norm(bps_2d_apos_list - avg_2d_pt, axis=1)) + else: + max_dist = 0.2 + o.state_ground_area = (avg_2d_pt,max_dist) + + # update timestamp + o.state_last_update = self.time_local_ms \ No newline at end of file diff --git a/world/commons/Body_Part.py b/world/commons/Body_Part.py new file mode 100644 index 0000000..2187480 --- /dev/null +++ b/world/commons/Body_Part.py @@ -0,0 +1,7 @@ +from math_ops.Matrix_4x4 import Matrix_4x4 + +class Body_Part(): + def __init__(self, mass) -> None: + self.mass = float(mass) + self.joints = [] + self.transform = Matrix_4x4() # body part to head transformation matrix \ No newline at end of file diff --git a/world/commons/Draw.py b/world/commons/Draw.py new file mode 100644 index 0000000..952dc22 --- /dev/null +++ b/world/commons/Draw.py @@ -0,0 +1,345 @@ +import socket +from math_ops.Math_Ops import Math_Ops as M +import numpy as np + +class Draw(): + _socket = None + + def __init__(self, is_enabled:bool, unum:int, host:str, port:int) -> None: + self.enabled = is_enabled + self._is_team_right = None + self._unum = unum + self._prefix = f'?{unum}_'.encode() # temporary prefix that should never be used in normal circumstances + + #Create one socket for all instances + if Draw._socket is None: + Draw._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM ) + Draw._socket.connect((host, port)) + Draw.clear_all() + + + def set_team_side(self, is_right): + ''' Called by world parser to switch side ''' + ''' + Generate an appropriate player ID + RoboViz has a bug/feature: we send "swap buffers for player: 'l_1' and RoboViz + will swap every buffer that contains 'l_1' in the name, including + 'l_10' and 'l_11'. To avoid that, we swap the separator to 'l-10', 'l-11' + ''' + self._is_team_right = is_right + self._prefix = f"{'r' if is_right else 'l'}{'_' if self._unum < 10 else '-'}{self._unum}_".encode() #e.g. b'l_5', b'l-10' + + + @staticmethod + def _send(msg, id, flush): + ''' Private method to send message if RoboViz is accessible ''' + try: + if flush: + Draw._socket.send(msg + id + b'\x00\x00\x00' + id + b'\x00') + else: + Draw._socket.send(msg + id + b'\x00') + except ConnectionRefusedError: + pass + + + def circle(self, pos2d, radius, thickness, color:bytes, id:str, flush=True): + ''' + Draw circle + + Examples + ---------- + Circle in 2D (z=0): circle((-1,2), 3, 2, Draw.Color.red, "my_circle") + ''' + if not self.enabled: return + assert type(color)==bytes, "The RGB color must be a bytes object, e.g. red: b'\xFF\x00\x00'" + assert not np.isnan(pos2d).any(), "Argument 'pos2d' contains 'nan' values" + + if self._is_team_right: + pos2d = (-pos2d[0],-pos2d[1]) + + msg = b'\x01\x00' + ( + f'{f"{pos2d[0] :.4f}":.6s}' + f'{f"{pos2d[1] :.4f}":.6s}' + f'{f"{radius :.4f}":.6s}' + f'{f"{thickness :.4f}":.6s}').encode() + color + + Draw._send(msg, self._prefix + id.encode(), flush) + + + def line(self, p1, p2, thickness, color:bytes, id:str, flush=True): + ''' + Draw line + + Examples + ---------- + Line in 3D: line((0,0,0), (0,0,2), 3, Draw.Color.red, "my_line") + Line in 2D (z=0): line((0,0), (0,1), 3, Draw.Color.red, "my_line") + ''' + if not self.enabled: return + assert type(color)==bytes, "The RGB color must be a bytes object, e.g. red: b'\xFF\x00\x00'" + assert not np.isnan(p1).any(), "Argument 'p1' contains 'nan' values" + assert not np.isnan(p2).any(), "Argument 'p2' contains 'nan' values" + + z1 = p1[2] if len(p1)==3 else 0 + z2 = p2[2] if len(p2)==3 else 0 + + if self._is_team_right: + p1 = (-p1[0],-p1[1],p1[2]) if len(p1)==3 else (-p1[0],-p1[1]) + p2 = (-p2[0],-p2[1],p2[2]) if len(p2)==3 else (-p2[0],-p2[1]) + + msg = b'\x01\x01' + ( + f'{f"{p1[0] :.4f}":.6s}' + f'{f"{p1[1] :.4f}":.6s}' + f'{f"{z1 :.4f}":.6s}' + f'{f"{p2[0] :.4f}":.6s}' + f'{f"{p2[1] :.4f}":.6s}' + f'{f"{z2 :.4f}":.6s}' + f'{f"{thickness :.4f}":.6s}').encode() + color + + Draw._send(msg, self._prefix + id.encode(), flush) + + + def point(self, pos, size, color:bytes, id:str, flush=True): + ''' + Draw point + + Examples + ---------- + Point in 3D: point((1,1,1), 3, Draw.Color.red, "my_point") + Point in 2D (z=0): point((1,1), 3, Draw.Color.red, "my_point") + ''' + if not self.enabled: return + assert type(color)==bytes, "The RGB color must be a bytes object, e.g. red: b'\xFF\x00\x00'" + assert not np.isnan(pos).any(), "Argument 'pos' contains 'nan' values" + + z = pos[2] if len(pos)==3 else 0 + + if self._is_team_right: + pos = (-pos[0],-pos[1],pos[2]) if len(pos)==3 else (-pos[0],-pos[1]) + + msg = b'\x01\x02' + ( + f'{f"{pos[0] :.4f}":.6s}' + f'{f"{pos[1] :.4f}":.6s}' + f'{f"{z :.4f}":.6s}' + f'{f"{size :.4f}":.6s}').encode() + color + + Draw._send(msg, self._prefix + id.encode(), flush) + + + def sphere(self, pos, radius, color:bytes, id:str, flush=True): + ''' + Draw sphere + + Examples + ---------- + Sphere in 3D: sphere((1,1,1), 3, Draw.Color.red, "my_sphere") + Sphere in 2D (z=0): sphere((1,1), 3, Draw.Color.red, "my_sphere") + ''' + if not self.enabled: return + assert type(color)==bytes, "The RGB color must be a bytes object, e.g. red: b'\xFF\x00\x00'" + assert not np.isnan(pos).any(), "Argument 'pos' contains 'nan' values" + + z = pos[2] if len(pos)==3 else 0 + + if self._is_team_right: + pos = (-pos[0],-pos[1],pos[2]) if len(pos)==3 else (-pos[0],-pos[1]) + + msg = b'\x01\x03' + ( + f'{f"{pos[0] :.4f}":.6s}' + f'{f"{pos[1] :.4f}":.6s}' + f'{f"{z :.4f}":.6s}' + f'{f"{radius :.4f}":.6s}').encode() + color + + Draw._send(msg, self._prefix + id.encode(), flush) + + + def polygon(self, vertices, color:bytes, alpha:int, id:str, flush=True): + ''' + Draw polygon + + Examples + ---------- + Polygon in 3D: polygon(((0,0,0),(1,0,0),(0,1,0)), Draw.Color.red, 255, "my_polygon") + ''' + if not self.enabled: return + assert type(color)==bytes, "The RGB color must be a bytes object, e.g. red: b'\xFF\x00\x00'" + assert 0<=alpha<=255, "The alpha channel (degree of opacity) must be in range [0,255]" + + if self._is_team_right: + vertices = [(-v[0],-v[1],v[2]) for v in vertices] + + msg = b'\x01\x04' + bytes([len(vertices)]) + color + alpha.to_bytes(1,'big') + + for v in vertices: + msg += ( + f'{f"{v[0] :.4f}":.6s}' + f'{f"{v[1] :.4f}":.6s}' + f'{f"{v[2] :.4f}":.6s}').encode() + + Draw._send(msg, self._prefix + id.encode(), flush) + + + def annotation(self, pos, text, color:bytes, id:str, flush=True): + ''' + Draw annotation + + Examples + ---------- + Annotation in 3D: annotation((1,1,1), "SOMEtext!", Draw.Color.red, "my_annotation") + Annotation in 2D (z=0): annotation((1,1), "SOMEtext!", Draw.Color.red, "my_annotation") + ''' + if not self.enabled: return + if type(text) != bytes: text = str(text).encode() + assert type(color)==bytes, "The RGB color must be a bytes object, e.g. red: b'\xFF\x00\x00'" + z = pos[2] if len(pos)==3 else 0 + + if self._is_team_right: + pos = (-pos[0],-pos[1],pos[2]) if len(pos)==3 else (-pos[0],-pos[1]) + + msg = b'\x02\x00' + ( + f'{f"{pos[0] :.4f}":.6s}' + f'{f"{pos[1] :.4f}":.6s}' + f'{f"{z :.4f}":.6s}').encode() + color + text + b'\x00' + + Draw._send(msg, self._prefix + id.encode(), flush) + + + def arrow(self, p1, p2, arrowhead_size, thickness, color:bytes, id:str, flush=True): + ''' + Draw arrow + + Examples + ---------- + Arrow in 3D: arrow((0,0,0), (0,0,2), 0.1, 3, Draw.Color.red, "my_arrow") + Arrow in 2D (z=0): arrow((0,0), (0,1), 0.1, 3, Draw.Color.red, "my_arrow") + ''' + if not self.enabled: return + assert type(color)==bytes, "The RGB color must be a bytes object, e.g. red: b'\xFF\x00\x00'" + + # No need to invert sides, the called shapes will handle that + if len(p1)==2: p1 = M.to_3d(p1) + else: p1 = np.asarray(p1) + if len(p2)==2: p2 = M.to_3d(p2) + else: p2 = np.asarray(p2) + + vec = p2-p1 + vec_size = np.linalg.norm(vec) + if vec_size == 0: return #return without warning/error + if arrowhead_size > vec_size: arrowhead_size = vec_size + + ground_proj_perpendicular = np.array([ vec[1], -vec[0], 0 ]) + + if np.all(ground_proj_perpendicular == 0): #vertical arrow + ground_proj_perpendicular = np.array([ arrowhead_size/2, 0, 0 ]) + else: + ground_proj_perpendicular *= arrowhead_size/2 / np.linalg.norm(ground_proj_perpendicular) + + head_start = p2 - vec * (arrowhead_size/vec_size) + head_pt1 = head_start + ground_proj_perpendicular + head_pt2 = head_start - ground_proj_perpendicular + + self.line(p1,p2,thickness,color,id,False) + self.line(p2,head_pt1,thickness,color,id,False) + self.line(p2,head_pt2,thickness,color,id,flush) + + + def flush(self, id): + ''' Flush specific drawing by ID ''' + if not self.enabled: return + + Draw._send(b'\x00\x00', self._prefix + id.encode(), False) + + def clear(self, id): + ''' Clear specific drawing by ID ''' + if not self.enabled: return + + Draw._send(b'\x00\x00', self._prefix + id.encode(), True) #swap buffer twice + + + def clear_player(self): + ''' Clear all drawings made by this player ''' + if not self.enabled: return + + Draw._send(b'\x00\x00', self._prefix, True) #swap buffer twice + + + @staticmethod + def clear_all(): + ''' Clear all drawings of all players ''' + if Draw._socket is not None: + Draw._send(b'\x00\x00\x00\x00\x00',b'',False) #swap buffer twice using no id + + + class Color(): + ''' + Based on X11 colors + The names are restructured to make better suggestions + ''' + pink_violet = b'\xC7\x15\x85' + pink_hot = b'\xFF\x14\x93' + pink_violet_pale = b'\xDB\x70\x93' + pink = b'\xFF\x69\xB4' + pink_pale = b'\xFF\xB6\xC1' + + red_dark = b'\x8B\x00\x00' + red = b'\xFF\x00\x00' + red_brick = b'\xB2\x22\x22' + red_crimson = b'\xDC\x14\x3C' + red_indian = b'\xCD\x5C\x5C' + red_salmon = b'\xFA\x80\x72' + + orange_red = b'\xFF\x45\x00' + orange = b'\xFF\x8C\x00' + orange_ligth = b'\xFF\xA5\x00' + + yellow_gold = b'\xFF\xD7\x00' + yellow = b'\xFF\xFF\x00' + yellow_light = b'\xBD\xB7\x6B' + + brown_maroon =b'\x80\x00\x00' + brown_dark = b'\x8B\x45\x13' + brown = b'\xA0\x52\x2D' + brown_gold = b'\xB8\x86\x0B' + brown_light = b'\xCD\x85\x3F' + brown_pale = b'\xDE\xB8\x87' + + green_dark = b'\x00\x64\x00' + green = b'\x00\x80\x00' + green_lime = b'\x32\xCD\x32' + green_light = b'\x00\xFF\x00' + green_lawn = b'\x7C\xFC\x00' + green_pale = b'\x90\xEE\x90' + + cyan_dark = b'\x00\x80\x80' + cyan_medium = b'\x00\xCE\xD1' + cyan = b'\x00\xFF\xFF' + cyan_light = b'\xAF\xEE\xEE' + + blue_dark = b'\x00\x00\x8B' + blue = b'\x00\x00\xFF' + blue_royal = b'\x41\x69\xE1' + blue_medium = b'\x1E\x90\xFF' + blue_light = b'\x00\xBF\xFF' + blue_pale = b'\x87\xCE\xEB' + + purple_violet = b'\x94\x00\xD3' + purple_magenta = b'\xFF\x00\xFF' + purple_light = b'\xBA\x55\xD3' + purple_pale = b'\xDD\xA0\xDD' + + white = b'\xFF\xFF\xFF' + gray_10 = b'\xE6\xE6\xE6' + gray_20 = b'\xCC\xCC\xCC' + gray_30 = b'\xB2\xB2\xB2' + gray_40 = b'\x99\x99\x99' + gray_50 = b'\x80\x80\x80' + gray_60 = b'\x66\x66\x66' + gray_70 = b'\x4C\x4C\x4C' + gray_80 = b'\x33\x33\x33' + gray_90 = b'\x1A\x1A\x1A' + black = b'\x00\x00\x00' + + @staticmethod + def get(r,g,b): + ''' Get RGB color (0-255) ''' + return bytes([int(r),int(g),int(b)]) diff --git a/world/commons/Joint_Info.py b/world/commons/Joint_Info.py new file mode 100644 index 0000000..5fac4f1 --- /dev/null +++ b/world/commons/Joint_Info.py @@ -0,0 +1,24 @@ +import numpy as np + +class Joint_Info(): + def __init__(self, xml_element) -> None: + self.perceptor = xml_element.attrib['perceptor'] + self.effector = xml_element.attrib['effector'] + self.axes = np.array([ + float(xml_element.attrib['xaxis']), + float(xml_element.attrib['yaxis']), + float(xml_element.attrib['zaxis'])]) + self.min = int(xml_element.attrib['min']) + self.max = int(xml_element.attrib['max']) + + self.anchor0_part = xml_element[0].attrib['part'] + self.anchor0_axes = np.array([ + float(xml_element[0].attrib['y']), + float(xml_element[0].attrib['x']), + float(xml_element[0].attrib['z'])]) #x and y axes are switched + + self.anchor1_part = xml_element[1].attrib['part'] + self.anchor1_axes_neg = np.array([ + -float(xml_element[1].attrib['y']), + -float(xml_element[1].attrib['x']), + -float(xml_element[1].attrib['z'])]) #x and y axes are switched diff --git a/world/commons/Other_Robot.py b/world/commons/Other_Robot.py new file mode 100644 index 0000000..5b17763 --- /dev/null +++ b/world/commons/Other_Robot.py @@ -0,0 +1,28 @@ +import numpy as np + +#Note: When other robot is seen, all previous body part positions are deleted +# E.g. we see 5 body parts at 0 seconds -> body_parts_cart_rel_pos contains 5 elements +# we see 1 body part at 1 seconds -> body_parts_cart_rel_pos contains 1 element + + +class Other_Robot(): + def __init__(self, unum, is_teammate) -> None: + self.unum = unum # convenient variable to indicate uniform number (same as other robot's index + 1) + self.is_self = False # convenient flag to indicate if this robot is self + self.is_teammate = is_teammate # convenient variable to indicate if this robot is from our team + self.is_visible = False # True if this robot was seen in the last message from the server (it doesn't mean we know its absolute location) + self.body_parts_cart_rel_pos = dict() # cartesian relative position of the robot's visible body parts + self.body_parts_sph_rel_pos = dict() # spherical relative position of the robot's visible body parts + self.vel_filter = 0.3 # EMA filter coefficient applied to self.state_filtered_velocity + self.vel_decay = 0.95 # velocity decay at every vision cycle (neutralized if velocity is updated) + + + # State variables: these are computed when this robot is visible and when the original robot is able to self-locate + self.state_fallen = False # true if the robot is lying down (updated when head is visible) + self.state_last_update = 0 # World.time_local_ms when the state was last updated + self.state_horizontal_dist = 0 # horizontal head distance if head is visible, otherwise, average horizontal distance of visible body parts (the distance is updated by vision or radio when state_abs_pos gets a new value, but also when the other player is not visible, by assuming its last position) + self.state_abs_pos = None # 3D head position if head is visible, otherwise, 2D average position of visible body parts, or, 2D radio head position + self.state_orientation = 0 # orientation based on pair of lower arms or feet, or average of both (WARNING: may be older than state_last_update) + self.state_ground_area = None # (pt_2d,radius) projection of player area on ground (circle), not precise if farther than 3m (for performance), useful for obstacle avoidance when it falls + self.state_body_parts_abs_pos = dict() # 3D absolute position of each body part + self.state_filtered_velocity = np.zeros(3) # 3D filtered velocity (m/s) (if the head is not visible, the 2D part is updated and v.z decays) diff --git a/world/commons/Path_Manager.py b/world/commons/Path_Manager.py new file mode 100644 index 0000000..7cc3443 --- /dev/null +++ b/world/commons/Path_Manager.py @@ -0,0 +1,583 @@ +from cpp.a_star import a_star +from math_ops.Math_Ops import Math_Ops as M +from world.World import World +import math +import numpy as np + + +class Path_Manager(): + MODE_CAUTIOUS = 0 + MODE_DRIBBLE = 1 # safety margins are increased + MODE_AGGRESSIVE = 2 # safety margins are reduced for opponents + + STATUS_SUCCESS = 0 # the pathfinding algorithm was executed normally + STATUS_TIMEOUT = 1 # timeout before the target was reached (may be impossible) + STATUS_IMPOSSIBLE = 2 # impossible to reach target (all options were tested) + STATUS_DIRECT = 3 # no obstacles between start and target (path contains only 2 points: the start and target) + + HOT_START_DIST_WALK = 0.05 # hot start prediction distance (when walking) + HOT_START_DIST_DRIBBLE = 0.10 # hot start prediction distance (when dribbling) + + def __init__(self, world : World) -> None: + self.world = world + + self._draw_obstacles = False # enabled by function 'draw_options' + self._draw_path = False # enabled by function 'draw_options' + self._use_team_channel = False # enabled by function 'draw_options' + + # internal variables to bootstrap the path to start from a prediction (to reduce path instability) + self.last_direction_rad = None + self.last_update = 0 + self.last_start_dist = None + + def draw_options(self, enable_obstacles, enable_path, use_team_drawing_channel=False): + ''' + Enable or disable drawings, and change drawing channel + If self.world.draw.enable is False, these options are ignored + + Parameters + ---------- + enable_obstacles : bool + draw relevant obstacles for path planning + enable_path : bool + draw computed path + use_team_drawing_channel : bool + True to use team drawing channel, otherwise use individual channel + Using individual channels for each player means that drawings with the same name can coexist + With the team channel, drawings with the same name will replace previous drawings, even if drawn by a teammate + ''' + self._draw_obstacles = enable_obstacles + self._draw_path = enable_path + self._use_team_channel = use_team_drawing_channel + + def get_obstacles(self, include_teammates, include_opponents, include_play_mode_restrictions, max_distance = 4, max_age = 500, + ball_safety_margin = 0, goalpost_safety_margin = 0, mode = MODE_CAUTIOUS, priority_unums=[]): + ''' + Parameters + ---------- + include_teammates : bool + include teammates in the returned list of obstacles + include_opponents : bool + include opponents in the returned list of obstacles + max_distance : float + teammates or opponents are only considered if they are closer than `max_distance` (meters) + max_age : float + teammates or opponents are only considered if they were seen in the last `max_age` (milliseconds) + ball_safety_margin : float + minimum value for the ball's soft repulsion radius + this value is increased when the game is stopped, and when the ball is almost out of bounds + default is zero, the ball is ignored + goalpost_safety_margin : float + hard repulsion radius around the opponents' goalposts + default is zero, uses the minimum margin + mode : int + overall attitude towards safety margins (concerns teammates and opponents) + priority_unums : list + list of teammates to avoid (since their role is more important) + + Returns + ------- + obstacles : list + list of obstacles, where each obstacle is a tuple of 5 floats (x, y, hard radius, soft radius, repulsive force) + ''' + w = self.world + + ball_2d = w.ball_abs_pos[:2] + obstacles = [] + + # 'comparator' is a variable local to the lambda, which captures the current value of (w.time_local_ms - max_age) + check_age = lambda last_update, comparator = w.time_local_ms - max_age : last_update > 0 and last_update >= comparator + + #---------------------------------------------- Get recently seen close teammates + if include_teammates: + soft_radius = 1.1 if mode == Path_Manager.MODE_DRIBBLE else 0.6 # soft radius: repulsive force is max at center and fades + + def get_hard_radius(t): + if t.unum in priority_unums: + return 1.0 # extra distance for priority roles + else: + return t.state_ground_area[1]+0.2 + + # Get close teammates (center, hard radius, soft radius, force) + obstacles.extend( (*t.state_ground_area[0], + get_hard_radius(t), + 1.5 if t.unum in priority_unums else soft_radius, + 1.0) # repulsive force + for t in w.teammates if not t.is_self and check_age(t.state_last_update) and t.state_horizontal_dist < max_distance) + + #---------------------------------------------- Get recently seen close opponents + if include_opponents: + + # soft radius: repulsive force is max at center and fades + if mode == Path_Manager.MODE_AGGRESSIVE: + soft_radius = 0.6 + hard_radius = lambda o : 0.2 + elif mode == Path_Manager.MODE_DRIBBLE: + soft_radius = 2.3 + hard_radius = lambda o : o.state_ground_area[1]+0.9 + else: + soft_radius = 1.0 + hard_radius = lambda o : o.state_ground_area[1]+0.2 + + # Get close opponents (center, hard radius, soft radius, force) + obstacles.extend( (*o.state_ground_area[0], + hard_radius(o), + soft_radius, + 1.5 if o.unum == 1 else 1.0) # repulsive force (extra for their GK) + for o in w.opponents if o.state_last_update > 0 and w.time_local_ms - o.state_last_update <= max_age and o.state_horizontal_dist < max_distance) + + #---------------------------------------------- Get play mode restrictions + if include_play_mode_restrictions: + if w.play_mode == World.M_THEIR_GOAL_KICK: + obstacles.extend((15,i,2.1,0,0) for i in range(-2,3)) # 5 circular obstacles to cover their goal area + elif w.play_mode == World.M_THEIR_PASS: + obstacles.append((*ball_2d, 1.2, 0, 0)) + elif w.play_mode in [World.M_THEIR_KICK_IN,World.M_THEIR_CORNER_KICK,World.M_THEIR_FREE_KICK,World.M_THEIR_DIR_FREE_KICK, World.M_THEIR_OFFSIDE]: + obstacles.append((*ball_2d, 2.5, 0, 0)) + + #---------------------------------------------- Get ball + if ball_safety_margin > 0: + + # increase ball safety margin in certain game scenarios + if (w.play_mode_group != w.MG_OTHER) or abs(ball_2d[1])>9.5 or abs(ball_2d[0])>14.5: + ball_safety_margin += 0.12 + + obstacles.append((*ball_2d, 0, ball_safety_margin, 8)) + + #---------------------------------------------- Get goal posts + if goalpost_safety_margin > 0: + obstacles.append((14.75, 1.10,goalpost_safety_margin,0,0)) + obstacles.append((14.75,-1.10,goalpost_safety_margin,0,0)) + + #---------------------------------------------- Draw obstacles + if self._draw_obstacles: + d = w.team_draw if self._use_team_channel else w.draw + if d.enabled: + for o in obstacles: + if o[3] > 0: d.circle(o[:2],o[3],o[4]/2, d.Color.orange, "path_obstacles", False) + if o[2] > 0: d.circle(o[:2],o[2],1, d.Color.red, "path_obstacles", False) + d.flush("path_obstacles") + + return obstacles + + def _get_hot_start(self, start_distance): + ''' + Get hot start position for path (considering the previous path) + (as opposed to a cold start, where the path starts at the player) + ''' + if self.last_update > 0 and self.world.time_local_ms - self.last_update == 20 and self.last_start_dist == start_distance: + return self.world.robot.loc_head_position[:2] + M.vector_from_angle(self.last_direction_rad, is_rad = True) * start_distance + else: + return self.world.robot.loc_head_position[:2] # return cold start if start_distance was different or the position was not updated in the last step + + def _update_hot_start(self, next_dir_rad, start_distance): + ''' Update hot start position for next run ''' + self.last_direction_rad = next_dir_rad + self.last_update = self.world.time_local_ms + self.last_start_dist = start_distance + + def _extract_target_from_path(self, path, path_len, ret_segments): + ret_seg_ceil = math.ceil(ret_segments) + + if path_len >= ret_seg_ceil: + i = ret_seg_ceil * 2 # path index of ceil point (x) + if ret_seg_ceil == ret_segments: + return path[i:i+2] + else: + floor_w = ret_seg_ceil-ret_segments + return path[i-2:i] * floor_w + path[i:i+2] * (1-floor_w) + else: + return path[-2:] # path end + + + def get_path_to_ball(self, x_ori = None, x_dev = -0.2, y_dev = 0, torso_ori = None, torso_ori_thrsh = 1, + priority_unums:list=[], is_aggressive=True, safety_margin = 0.25, timeout = 3000): + ''' + Get next target from path to ball (next absolute position + next absolute orientation) + If the robot is an active player, and close to the ball, it makes sense to be aggressive + If the robot is far, it should follow the role_position instead to predict the intersection with ball + + + Parameters + ---------- + x_ori : float + (This variable allows the specification of a target position, relative to the ball, in a custom reference frame.) + absolute orientation of the custom reference frame's x-axis + if None, the orientation is given by the vector (robot->ball) + x_dev : float + (This variable allows the specification of a target position, relative to the ball, in a custom reference frame.) + target position deviation, in the custom reference frame's x-axis + y_dev : float + (This variable allows the specification of a target position, relative to the ball, in a custom reference frame.) + target position deviation, in the custom reference frame's y-axis + torso_ori : float + torso's target absolute orientation (see `torso_ori_thrsh`) + if None, the orientation is given by the vector (robot->target) + torso_ori_thrsh : float + `torso_ori` will only be applied when the distance between robot and final target is < `torso_ori_thrsh` meters + otherwise, the robot will orient itself towards the final target + priority_unums : list + list of teammates to avoid (since their role is more important) + is_aggressive : bool + if True, safety margins are reduced for opponents + safety_margin : float + repulsion radius around ball to avoid colliding with it + timeout : float + maximum execution time (in microseconds) + + Returns + ------- + next_pos : ndarray + next absolute position from path to ball + next_ori : float + next absolute orientation + distance : float + minimum between (distance to final target) and (distance to ball) + + + Example + ------- + ---------------------------------------------------------------------------------------------- + x_ori | x_dev | y_dev | torso_ori | OBS + -------------+---------+---------+-------------+---------------------------------------------- + None => | - | !0 | - | Not recommended. Will not converge. + (orient. of: | 0 | 0 | None | Frontal ball chase, expected* slow approach + robot->ball) | 0 | 0 | value | Oriented ball chase, expected* slow approach + | >0 | 0 | - | Not recommended. Will not converge. + | <0 | 0 | None | Frontal ball chase until distance == x_dev + | <0 | 0 | value | Oriented ball chase until distance == x_dev + -------------+---------+---------+-------------+---------------------------------------------- + value | - | - | None | Frontal point chase + | - | - | value | Oriented point chase + ---------------------------------------------------------------------------------------------- + * it depends on the caller function (expected slow walking near target) + `torso_ori` will only be applied when the distance between robot and final target is < `torso_ori_thrsh` meters + ''' + + w = self.world + r = w.robot + dev = np.array([x_dev,y_dev]) + dev_len = np.linalg.norm(dev) + dev_mult = 1 + + # use ball prediction if we are further than 0.5 m and in PlayOn + if np.linalg.norm(w.ball_abs_pos[:2] - r.loc_head_position[:2]) > 0.5 and w.play_mode_group == w.MG_OTHER: + ball_2d = w.get_intersection_point_with_ball(0.4)[0] # intersection point, while moving at 0.4 m/s + else: + ball_2d = w.ball_abs_pos[:2] + + # custom reference frame orientation + vec_me_ball = ball_2d - r.loc_head_position[:2] + if x_ori is None: + x_ori = M.vector_angle(vec_me_ball) + + distance_boost = 0 # boost for returned distance to target + if torso_ori is not None and dev_len > 0: + approach_ori_diff = abs(M.normalize_deg( r.imu_torso_orientation - torso_ori )) + if approach_ori_diff > 15: # increase walking speed near target if robot is far from approach orientation + distance_boost = 0.15 + if approach_ori_diff > 30: # increase target distance to ball if robot is far from approach orientation + dev_mult = 1.3 + if approach_ori_diff > 45: # increase safety margin around ball if robot is far from approach orientation + safety_margin = max(0.32,safety_margin) + + #------------------------------------------- get target + + front_unit_vec = M.vector_from_angle(x_ori) + left_unit_vec = np.array([-front_unit_vec[1], front_unit_vec[0]]) + + rel_target = front_unit_vec * dev[0] + left_unit_vec * dev[1] + target = ball_2d + rel_target * dev_mult + target_vec = target - r.loc_head_position[:2] + target_dist = np.linalg.norm(target_vec) + + if self._draw_path: + d = self.world.team_draw if self._use_team_channel else self.world.draw + d.point(target, 4, d.Color.red, "path_target") # will not draw if drawing object is internally disabled + + #------------------------------------------- get obstacles + + # Ignore ball if we are on the same side of the target (with small margin) + if dev_len>0 and np.dot(vec_me_ball, rel_target) < -0.10: + safety_margin = 0 + + obstacles = self.get_obstacles(include_teammates = True, include_opponents = True, include_play_mode_restrictions = True, + ball_safety_margin = safety_margin, + mode = Path_Manager.MODE_AGGRESSIVE if is_aggressive else Path_Manager.MODE_CAUTIOUS, + priority_unums = priority_unums) + + # Add obstacle on the side opposite to the target + if dev_len>0 and safety_margin > 0: + center = ball_2d - M.normalize_vec( rel_target ) * safety_margin + obstacles.append((*center, 0, safety_margin*0.9, 5)) + if self._draw_obstacles: + d = w.team_draw if self._use_team_channel else w.draw + if d.enabled: + d.circle(center,safety_margin*0.8,2.5, d.Color.orange, "path_obstacles_1") + + #------------------------------------------- get path + + # see explanation for the context at the hot start update section below + start_pos = self._get_hot_start(Path_Manager.HOT_START_DIST_WALK) if target_dist > 0.4 else self.world.robot.loc_head_position[:2] + + path, path_len, path_status, path_cost = self.get_path(start_pos, True, obstacles, target, timeout) + path_end = path[-2:] # last position allowed by A* + + #------------------------------------------- get relevant distances + + if w.ball_last_seen > w.time_local_ms - w.VISUALSTEP_MS: # ball is in FOV + raw_ball_dist = np.linalg.norm(w.ball_rel_torso_cart_pos[:2]) # - distance between torso center and ball center + else: # otherwise use absolute coordinates to compute distance + raw_ball_dist = np.linalg.norm(vec_me_ball) # - distance between head center and ball center + + avoid_touching_ball = (w.play_mode_group != w.MG_OTHER) + distance_to_final_target = np.linalg.norm(path_end - r.loc_head_position[:2]) + distance_to_ball = max(0.07 if avoid_touching_ball else 0.14, raw_ball_dist - 0.13) + caution_dist = min(distance_to_ball,distance_to_final_target) + + #------------------------------------------- get next target position + + next_pos = self._extract_target_from_path( path, path_len, ret_segments=1 if caution_dist < 1 else 2 ) + + #------------------------------------------ get next target orientation + + # use given orientation if it exists, else target's orientation if far enough, else current orientation + if torso_ori is not None: + + if caution_dist > torso_ori_thrsh: + next_ori = M.vector_angle(target_vec) + else: + mid_ori = M.normalize_deg( M.vector_angle(vec_me_ball) - M.vector_angle(-dev) - x_ori + torso_ori ) + mid_ori_diff = abs(M.normalize_deg(mid_ori - r.imu_torso_orientation)) + final_ori_diff = abs(M.normalize_deg(torso_ori - r.imu_torso_orientation)) + next_ori = mid_ori if mid_ori_diff + 10 < final_ori_diff else torso_ori + + elif target_dist > 0.1: + next_ori = M.vector_angle(target_vec) + else: + next_ori = r.imu_torso_orientation + + #------------------------------------------ update hot start for next run + + ''' Defining the hot start distance: + - if path_len is zero, there is no hot start, because we are already there (dist=0) + - if the target is close, the hot start is not applied (see above) + - if the next pos is very close (due to hard obstacle), the hot start is the next pos (disttarget) + priority_unums : list + list of teammates to avoid (since their role is more important) + is_aggressive : bool + if True, safety margins are reduced for opponents + timeout : float + maximum execution time (in microseconds) + ''' + + w = self.world + + #------------------------------------------- get target + + target_vec = target - w.robot.loc_head_position[:2] + target_dist = np.linalg.norm(target_vec) + + #------------------------------------------- get obstacles + + obstacles = self.get_obstacles(include_teammates = True, include_opponents = True, include_play_mode_restrictions = True, + mode = Path_Manager.MODE_AGGRESSIVE if is_aggressive else Path_Manager.MODE_CAUTIOUS, priority_unums = priority_unums) + + #------------------------------------------- get path + + # see explanation for the context at the hot start update section below + start_pos = self._get_hot_start(Path_Manager.HOT_START_DIST_WALK) if target_dist > 0.4 else self.world.robot.loc_head_position[:2] + + path, path_len, path_status, path_cost = self.get_path(start_pos, True, obstacles, target, timeout) + path_end = path[-2:] # last position allowed by A* + + #------------------------------------------- get next target position + next_pos = self._extract_target_from_path(path, path_len, ret_segments) + + #------------------------------------------ get next target orientation + + # use given orientation if it exists, else target's orientation if far enough, else current orientation + if torso_ori is not None: + next_ori = torso_ori + elif target_dist > 0.1: + next_ori = M.vector_angle(target_vec) + else: + next_ori = w.robot.imu_torso_orientation + + #------------------------------------------ update hot start for next run + + ''' Defining the hot start distance: + - if path_len is zero, there is no hot start, because we are already there (dist=0) + - if the target is close, the hot start is not applied (see above) + - if the next pos is very close (due to hard obstacle), the hot start is the next pos (dist + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/world/commons/robots/nao1.xml b/world/commons/robots/nao1.xml new file mode 100644 index 0000000..f6297a7 --- /dev/null +++ b/world/commons/robots/nao1.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/world/commons/robots/nao2.xml b/world/commons/robots/nao2.xml new file mode 100644 index 0000000..59a279c --- /dev/null +++ b/world/commons/robots/nao2.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/world/commons/robots/nao3.xml b/world/commons/robots/nao3.xml new file mode 100644 index 0000000..bb35848 --- /dev/null +++ b/world/commons/robots/nao3.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/world/commons/robots/nao4.xml b/world/commons/robots/nao4.xml new file mode 100644 index 0000000..e728ff0 --- /dev/null +++ b/world/commons/robots/nao4.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +